Not in the top 1,000 by GitHub stars

OthmanAdi/plandeck

The visual Kanban for long-running AI agents. Watch your agent's plan organize itself: dependencies unlock into Ready, the critical path lights up, the one next move is obvious. Nobody wants to read a markdown plan.

History

Review weekly changes to SKILL.md before updating. Dates mark the weeks in which we observed each version; checks apply to the recorded commits.

View weekly changes

SKILL.md

Week of Nov 2, 2026 · +0 / −0 lines

Before Network or broad shell → After Network or broad shell

7d3f52dcc5bc → 7d3f52dcc5bc

Compare on GitHub

Week of Oct 26, 2026 · Line counts unavailable

Before Not checked yet → After Network or broad shell

First observation → 7d3f52dcc5bc

No earlier version was observed for comparison.

Compare on GitHub

View on GitHubAdd to my skills

Verified commit 7d3f52dcc5bc53e7829a4f21b979d7cb162318c1

68GitHub stars
0stars in 7 days
1skill in this repository
0commits in 30 days

Heat

0.3
  • Star growth, last 7 daysNot available
  • Star growth, last 24 hoursNot available
  • Rank movement, last 7 daysNot available
  • Commits, last 30 daysNot available
  • Accounts asking about it through MCP, last 7 daysNot available
  • Hot votes this weekNot available

Heat never changes the order by stars, and sponsorship never touches it. How heat works

Stars over time

Oct 25, 2026Oct 25, 202668

30-day change not yet available: not enough history.

Individual skills in this repository

How it works

plandeck

When your agent uses it

A live visual Kanban board and continuity layer for long-running agentic tasks. Nobody wants to read a markdown plan or stare at raw HTML while an agent grinds for an hour, so Plandeck turns the plan into a board that organizes itself. Use it when a task is multi-step, long-running, or needs visual planning. Break the work into cards, declare which card depends on which, and Plandeck computes the critical path, auto-promotes cards to Ready as their dependencies clear, rolls up story-point estimates into an honest percent complete, and names the one next action. Plans, observed transitions, and last-known-good snapshots stay as plain files on disk, so they survive /clear and context resets. After a reset, run plandeck next to recover the single next move and its recent history. If the plan stops parsing, run plandeck doctor before using git. Renders a live board in the browser over SSE. Triggers on plan a project, break down a task, kanban board, task board, visual plan, long-running agent, agentic task, track dependencies, critical path, story-point estimates, what is next, mission journal, recover plan, plandeck.

What it loads into context

Frontmatter

253 tokens

Always available to the agent

SKILL.md

4,090 tokens

When the skill is activated

Reference documents

10,075 tokens

Loaded on request

Token counts are estimates. Scripts run separately; they are not counted as reference context.

The playbook

  1. When to use it
  2. When not to use it
  3. Files in a plan directory
  4. Card schema
  5. Commands
  6. Continuity contract for agents
  7. The deterministic brain (no AI)
  8. The working loop
  9. After a /clear
  10. The board
  11. Where it runs
  12. Lineage

Scripts executed separately

  • package.json
  • scripts/board.mjs
  • scripts/check-plan.mjs
  • scripts/cli.mjs
  • scripts/doctor.mjs
  • scripts/lib/continuity.mjs
  • scripts/lib/deck.mjs
  • scripts/lib/intelligence.mjs
  • scripts/lib/journal.mjs
  • scripts/lib/render.mjs
  • scripts/lib/snapshot.mjs
  • scripts/screenshot.mjs
  • test/all.test.mjs
  • test/deck.test.mjs
  • test/doctor.test.mjs
  • test/journal.test.mjs

Files

  • .gitattributesasset101 bytes
  • .github/ISSUE_TEMPLATE/agent_compatibility.mddoc445 bytes
  • .github/ISSUE_TEMPLATE/bug_report.mddoc384 bytes
  • .github/PULL_REQUEST_TEMPLATE.mddoc385 bytes
  • .github/workflows/test.ymlasset503 bytes
  • .gitignoreasset401 bytes
  • .npmignoreasset118 bytes
  • CHANGELOG.mddoc5,261 bytes
  • CONTRIBUTING.mddoc2,153 bytes
  • LICENSEasset1,079 bytes
  • NOTICEasset697 bytes
  • README.mddoc18,470 bytes
  • SKILL.mdcore16,595 bytes
  • SKILL.mdcore16,595 bytes
  • agents/judge.mddoc2,234 bytes
  • agents/pm.mddoc2,611 bytes
  • agents/scout.mddoc2,178 bytes
  • agents/worker.mddoc2,500 bytes
  • examples/README.mddoc1,118 bytes
  • examples/ship-onboarding-flow/cards/C001-onboarding-map.mddoc870 bytes
  • examples/ship-onboarding-flow/plan.mddoc1,061 bytes
  • examples/ship-onboarding-flow/plan.yamlasset2,970 bytes
  • media/banner.pngasset1,299,391 bytes
  • media/board-preview.htmlasset36,602 bytes
  • media/board.pngasset137,036 bytes
  • media/plandeck-mark.svgasset449 bytes
  • package.jsonruns1,478 bytes
  • scripts/board.mjsruns30,470 bytes
  • scripts/check-plan.mjsruns3,032 bytes
  • scripts/cli.mjsruns9,578 bytes
  • scripts/doctor.mjsruns5,205 bytes
  • scripts/lib/continuity.mjsruns617 bytes
  • scripts/lib/deck.mjsruns32,582 bytes
  • scripts/lib/intelligence.mjsruns10,160 bytes
  • scripts/lib/journal.mjsruns9,225 bytes
  • scripts/lib/render.mjsruns26,431 bytes
  • scripts/lib/snapshot.mjsruns3,864 bytes
  • scripts/screenshot.mjsruns3,011 bytes
  • templates/card.mddoc323 bytes
  • templates/plan.mddoc934 bytes
  • templates/plan.yamlasset2,004 bytes
  • test/all.test.mjsruns173 bytes
  • test/deck.test.mjsruns43,229 bytes
  • test/doctor.test.mjsruns9,620 bytes
  • test/journal.test.mjsruns8,872 bytes
Read SKILL.md

# Plandeck

Plandeck keeps a plan on disk as `plan.yaml` and renders it as a live Kanban board that reorganizes itself as you edit. You author cards (id, estimate, dependencies, next action). Plandeck computes the rest: which cards are Ready, the critical path, progress rollups, and the single next move. It grows out of planning-with-files (crash-proof file plans plus a completion gate). What is new in Plandeck is the live visual board plus a deterministic intelligence layer over the yaml. It contains no AI and no LLM, only pure, tested functions.

## When to use it

Reach for Plandeck when the work is multi-step and worth tracking:

- A task that needs 5+ steps, spans a long session, or will outlive a `/clear`.
- Work with real dependencies, where order matters and one card unblocks others.
- Anything you want to see: a board with lanes, estimates, and a critical path.

## When not to use it
View the complete source on GitHub

Skills included

  • plandeck

Repository facts

License
MIT
Created
Jul 12, 2026
Last pushed
Aug 3, 2026
Forks
1
Open issues
0
Project activity
Slowing

Before you install

Network or broad shell

Automated signals, not a guarantee

Checked Nov 4, 2026

Not fully analyzed: some files were too long, unreadable or missing. This never lowers the level.

Show 2 notes
  • SKILL.md:1 Analysis incomplete: Referenced executable target unavailable; not analyzed
  • media/board-preview.html:224 Analysis incomplete: physical or joined line exceeds character limit
Show all 99 signals by file

CONTRIBUTING.md

  • Network accessline 19
    git clone https://github.com/OthmanAdi/plandeck

NOTICE

  • Network accessline 4
    Lineage ------------------------------------------------------------------------------- planning-with-files  —  https://github.com/OthmanAdi/planning-with-files
  • Network accessline 5
    ------------------------------------------------------------------------------- planning-with-files  —  https://github.com/OthmanAdi/planning-with-files Copyright (c) 2026 Ahmad Othman Ammar Adi
  • Network accessline 6
    planning-with-files  —  https://github.com/OthmanAdi/planning-with-files

README.md

  • Network accessline 8
    npx skills add https://github.com/othmanadi/plandeck --skill plandeck
  • Network accessline 20
    ![License MIT](https://img.shields.io/badge/license-MIT-informational) ![Node >=18](https://img.shields.io/badge/node-%3E%3D18-informational) ![SKILL.md standard](https://img.shields.io/badge/SKILL.md-standard-informational) ![Zero dependen
  • Network accessline 34
    Plandeck grows out of **planning-with-files** ([OthmanAdi/planning-with-files](https://github.com/OthmanAdi/planning-with-files), MIT), which gave crash-proof markdown plans that survive `/clear` plus a deterministic completion gate. Plande
  • Network accessline 44
    # 2. Open the URL it prints, e.g. #    http://plandeck.localhost:41747/ship-onboarding-flow/
  • Network accessline 45
    #    http://plandeck.localhost:41747/ship-onboarding-flow/
  • Network accessline 134
    - Live board: http://plandeck.localhost:41747/ship-onboarding-flow/
  • Network accessline 198
    risk: med     depends_on: [C002]     verify: ["curl -f localhost:3000/api/signup -d @test/fixtures/user.json"]
  • Network accessline 199
    depends_on: [C002]     verify: ["curl -f localhost:3000/api/signup -d @test/fixtures/user.json"]     next_action: "Wire the POST /signup handler to the users table, return 201 with the new id."
  • Network accessline 200
    verify: ["curl -f localhost:3000/api/signup -d @test/fixtures/user.json"]
  • Network accessline 234
    Plandeck grows out of **planning-with-files** by [OthmanAdi](https://github.com/OthmanAdi/planning-with-files) (MIT): durable, crash-proof plans on disk and a deterministic completion gate. Everything else, the board server, the YAML reader

SKILL.md

  • Network accessline 171
    - Live board: http://plandeck.localhost:41747/ship-onboarding-flow/

examples/ship-onboarding-flow/plan.yaml

  • Network accessline 50
    risk: med     depends_on: [C002]     verify: ["npm test -- test/auth/signup.test.ts", "curl -s localhost:3000/api/signup"]
  • Network accessline 51
    depends_on: [C002]     verify: ["npm test -- test/auth/signup.test.ts", "curl -s localhost:3000/api/signup"]     next_action: "Finish POST /api/signup: hash password, insert user, emit onboarding.started."
  • Network accessline 52
    verify: ["npm test -- test/auth/signup.test.ts", "curl -s localhost:3000/api/signup"]

media/board-preview.html

  • Network accessline 332
    async function boot(){   if(window.__PLANDECK_STATIC__){render(window.__PLANDECK_STATIC__);setLive(true);return;}   try{const r=await fetch("./api/board",{cache:"no-store"});render(await r.json())}catch(e){}
  • Network accessline 333
    if(window.__PLANDECK_STATIC__){render(window.__PLANDECK_STATIC__);setLive(true);return;}   try{const r=await fetch("./api/board",{cache:"no-store"});render(await r.json())}catch(e){}   connect();
  • Network accessline 334
    try{const r=await fetch("./api/board",{cache:"no-store"});render(await r.json())}catch(e){}

media/plandeck-mark.svg

  • Network accessline 1
    <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Plandeck">

package.json

  • Executable scriptline 1
    Executable file
  • Network accessline 51
    "repository": {     "type": "git",     "url": "git+https://github.com/OthmanAdi/plandeck.git"
  • Network accessline 52
    "type": "git",     "url": "git+https://github.com/OthmanAdi/plandeck.git"   },
  • Network accessline 53
    "url": "git+https://github.com/OthmanAdi/plandeck.git"
  • Network accessline 54
    },   "homepage": "https://github.com/OthmanAdi/plandeck#readme",   "bugs": "https://github.com/OthmanAdi/plandeck/issues",
  • Network accessline 55
    "homepage": "https://github.com/OthmanAdi/plandeck#readme",
  • Network accessline 56
    "bugs": "https://github.com/OthmanAdi/plandeck/issues",

scripts/board.mjs

  • Executable scriptline 1
    Executable file
  • Network accessline 174
    const timer = setTimeout(() => controller.abort(), 1000);   try {     const target = `http://${urlHost(connectHost(host))}:${port}/api/boards`;
  • Network accessline 175
    try {     const target = `http://${urlHost(connectHost(host))}:${port}/api/boards`;     const handshake = await fetch(target, { signal: controller.signal });
  • Network accessline 176
    const target = `http://${urlHost(connectHost(host))}:${port}/api/boards`;     const handshake = await fetch(target, { signal: controller.signal });     if (handshake.status !== 200) return null;
  • Network accessline 177
    const handshake = await fetch(target, { signal: controller.signal });
  • Network accessline 179
    const state = await handshake.json();     if (!state || state.hub !== true) return null;     const response = await fetch(target, {
  • Network accessline 180
    if (!state || state.hub !== true) return null;     const response = await fetch(target, {       method: "POST",
  • Network accessline 181
    const response = await fetch(target, {
  • Network accessline 202
    try {     const origin = new URL(summary.url).origin;     await fetch(`${origin}/api/boards?path=${encodeURIComponent(summary.boardPath)}`, { method: "DELETE" });
  • Network accessline 203
    const origin = new URL(summary.url).origin;     await fetch(`${origin}/api/boards?path=${encodeURIComponent(summary.boardPath)}`, { method: "DELETE" });   } catch {
  • Network accessline 204
    await fetch(`${origin}/api/boards?path=${encodeURIComponent(summary.boardPath)}`, { method: "DELETE" });
  • Network accessline 264
    async function handleRequest(hub, req, res) {   const requestUrl = new URL(req.url || "/", `http://${BIND_HOST}`);
  • Network accessline 265
    const requestUrl = new URL(req.url || "/", `http://${BIND_HOST}`);
  • Network accessline 756
    function hubOrigin(hub) {   return `http://${urlHost(hub.publicHost)}:${hub.port}`;
  • Network accessline 757
    return `http://${urlHost(hub.publicHost)}:${hub.port}`;

scripts/check-plan.mjs

  • Executable scriptline 1
    Executable file

scripts/cli.mjs

  • Executable scriptline 1
    #!/usr/bin/env node
  • Network accessline 104
    atomicWriteFile(dest, nextMarkdown(       payload,       `http://plandeck.localhost:${PORT}/${payload.plan.slug}/`,
  • Network accessline 105
    payload,       `http://plandeck.localhost:${PORT}/${payload.plan.slug}/`,       recentForNext(root, 5),
  • Network accessline 106
    `http://plandeck.localhost:${PORT}/${payload.plan.slug}/`,

scripts/doctor.mjs

  • Executable scriptline 1
    Executable file

scripts/lib/continuity.mjs

  • Executable scriptline 1
    Executable file

scripts/lib/deck.mjs

  • Executable scriptline 1
    Executable file

scripts/lib/intelligence.mjs

  • Executable scriptline 1
    Executable file

scripts/lib/journal.mjs

  • Executable scriptline 1
    Executable file

scripts/lib/render.mjs

  • Executable scriptline 1
    Executable file
  • Network accessline 428
    async function boot(){   if(window.__PLANDECK_STATIC__){render(window.__PLANDECK_STATIC__);setLive(true);return;}   try{const r=await fetch("./api/board",{cache:"no-store"});render(await r.json())}catch(e){}
  • Network accessline 429
    if(window.__PLANDECK_STATIC__){render(window.__PLANDECK_STATIC__);setLive(true);return;}   try{const r=await fetch("./api/board",{cache:"no-store"});render(await r.json())}catch(e){}   connect();
  • Network accessline 430
    try{const r=await fetch("./api/board",{cache:"no-store"});render(await r.json())}catch(e){}

scripts/lib/snapshot.mjs

  • Executable scriptline 1
    Executable file

scripts/screenshot.mjs

  • Executable scriptline 1
    #!/usr/bin/env node

test/all.test.mjs

  • Executable scriptline 1
    Executable file

test/deck.test.mjs

  • Executable scriptline 1
    Executable file
  • Network accessline 362
    const base = "http://127.0.0.1:" + hub.port;     const state = await fetch(base + "/api/boards").then((response) => response.json());
  • Network accessline 363
    const state = await fetch(base + "/api/boards").then((response) => response.json());
  • Network accessline 369
    const secondPayload = await fetch(base + "/x-2/api/board").then((response) => response.json());
  • Network accessline 372
    const index = await fetch(base + "/").then((response) => response.text());
  • Network accessline 378
    const wrongType = await fetch(base + "/api/boards", {
  • Network accessline 397
    const removed = await fetch(base + "/api/boards?path=" + encodeURIComponent("/x-2/"), {
  • Network accessline 400
    });     assert.equal(removed.status, 200);     const afterDelete = await fetch(base + "/api/boards").then((response) => response.json());
  • Network accessline 401
    assert.equal(removed.status, 200);     const afterDelete = await fetch(base + "/api/boards").then((response) => response.json());     assert.deepEqual(afterDelete.boards.map((board) => board.boardPath), ["/x/"]);
  • Network accessline 402
    const afterDelete = await fetch(base + "/api/boards").then((response) => response.json());
  • Network accessline 405
    unlinkSync(join(firstDir, "plan.yaml"));     await waitFor(async () => {       const current = await fetch(base + "/api/boards").then((response) => response.json());
  • Network accessline 406
    await waitFor(async () => {       const current = await fetch(base + "/api/boards").then((response) => response.json());       return current.boards.length === 0;
  • Network accessline 407
    const current = await fetch(base + "/api/boards").then((response) => response.json());
  • Network accessline 411
    const unknown = await fetch(base + "/not-a-board");
  • Network accessline 453
    const state = await fetch(`http://127.0.0.1:${hub.port}/api/boards`).then((response) => response.json());
  • Network accessline 474
    });     hub = await quietly(() => runBoard({ planDir: root, port: 0, host: "127.0.0.1" }));     const base = `http://127.0.0.1:${hub.port}`;
  • Network accessline 475
    hub = await quietly(() => runBoard({ planDir: root, port: 0, host: "127.0.0.1" }));     const base = `http://127.0.0.1:${hub.port}`;     const response = await fetch(base + "/api/boards", {
  • Network accessline 476
    const base = `http://127.0.0.1:${hub.port}`;     const response = await fetch(base + "/api/boards", {       method: "POST",
  • Network accessline 477
    const response = await fetch(base + "/api/boards", {
  • Network accessline 486
    const healthy = await fetch(base + "/api/boards");
  • Network accessline 741
    unlinkSync(join(root, "cards", "A.md"));     const payload = await waitFor(async () => {       const current = await fetch(boardServer.url + "api/board").then((response) => response.json());
  • Network accessline 742
    const payload = await waitFor(async () => {       const current = await fetch(boardServer.url + "api/board").then((response) => response.json());       return current.warnings.some((warning) => warning.kind === "missing-note") ? current : n
  • Network accessline 743
    const current = await fetch(boardServer.url + "api/board").then((response) => response.json());
  • Network accessline 748
    const state = await fetch(`http://127.0.0.1:${boardServer.port}/api/boards`).then((response) => response.json());
  • Network accessline 766
    boardServer = await quietly(() => runBoard({ planDir: root, port: 0, host: "127.0.0.1" }));     const endpoint = boardServer.url + "api/board";     const good = await fetch(endpoint).then((response) => response.json());
  • Network accessline 767
    const endpoint = boardServer.url + "api/board";     const good = await fetch(endpoint).then((response) => response.json());     assert.ok(good.columns.length > 0);
  • Network accessline 768
    const good = await fetch(endpoint).then((response) => response.json());
  • Network accessline 771
    writeFileSync(join(root, "plan.yaml"), "version: 1\n plan:\n");     const stale = await waitFor(async () => {       const payload = await fetch(endpoint).then((response) => response.json());
  • Network accessline 772
    const stale = await waitFor(async () => {       const payload = await fetch(endpoint).then((response) => response.json());       return payload.error ? payload : null;
  • Network accessline 773
    const payload = await fetch(endpoint).then((response) => response.json());
  • Network accessline 887
    await new Promise((resolveWait) => setTimeout(resolveWait, 5));     const second = appendJournalEntry(root, { cardId: "A", field: "column", from: "ready", to: "doing", actor: "second" });     const response = await fetch(`${boardServer.url}
  • Network accessline 888
    const second = appendJournalEntry(root, { cardId: "A", field: "column", from: "ready", to: "doing", actor: "second" });     const response = await fetch(`${boardServer.url}api/journal?since=${encodeURIComponent(second.ts)}&limit=5`);     as
  • Network accessline 889
    const response = await fetch(`${boardServer.url}api/journal?since=${encodeURIComponent(second.ts)}&limit=5`);
  • Network accessline 893
    assert.equal(body.entries.length, 1);     assert.deepEqual(body.entries[0], second);     const post = await fetch(`${boardServer.url}api/journal`, { method: "POST" });
  • Network accessline 894
    assert.deepEqual(body.entries[0], second);     const post = await fetch(`${boardServer.url}api/journal`, { method: "POST" });     assert.equal(post.status, 405);
  • Network accessline 895
    const post = await fetch(`${boardServer.url}api/journal`, { method: "POST" });

test/doctor.test.mjs

  • Executable scriptline 1
    Executable file

test/journal.test.mjs

  • Executable scriptline 1
    Executable file

How the signals fit together

Facts from a static read of the files at the pinned commit. They describe what the code does, not why.

Some files could not be read; the facts below still hold.

Signal confidence: 89 facts

and 39 more

Works with

Agent compatibility, from the files in this repository
AgentStatusEvidenceInstall to
Claude CodeCompatibleStandard SKILL.md SKILL.md.claude/skills/<skill-name>/
CodexCompatibleStandard SKILL.md SKILL.md.agents/skills/<skill-name>/
CursorCompatibleStandard SKILL.md SKILL.md.cursor/skills/<skill-name>/
Gemini CLICompatibleStandard SKILL.md SKILL.md.gemini/skills/<skill-name>/
GitHub CopilotCompatibleStandard SKILL.md SKILL.md.github/skills/<skill-name>/
OpenCodeCompatibleStandard SKILL.md SKILL.md.opencode/skills/<skill-name>/
  • Native The repository ships a manifest, folder or frontmatter field for this agent.
  • Compatible A standard SKILL.md that this agent can read.
  • Unknown No evidence either way.

Derived automatically from paths, manifests and frontmatter. Not tested on each agent.