Skip to content

vt-p-spec-board

Read-only cross-project Kanban over every registered project's specs and bugs. One board with nine columns (Needs attention, Done, Archiviert/Parked, Review/Finalize, In Progress · live / · no session, Blocked, Ready, Backlog), dependency waves, owners, badges, the live worktree and Claude-session overlay, and eight integrity checks. Writes Spec-Board-.md and .json to Obsidian or docs/pm-dashboards/ and prints a terminal summary. Use when you need one view of all work in flight across repos, or to find drift, dangling references, cycles and tombstones before a planning round.

Plugin: vt-pm-intake
Category: Other
Command: /vt-p-spec-board


/vt-p-spec-board — Spec-Board

One Kanban over the specs and bugs of every project in intake/projects.yaml, plus the toolkit itself. The board only reads: it writes nothing but its output files (FR-8).

When to Use

  • Before a planning or PM round, to see every card in flight across all repos on one page.
  • To find what needs a human: unknown status literals, registry entries without a directory, refused registry entries, oversized state.yaml files, bypassed gates without a reason.
  • To see which in-progress work has a live Claude session and which has none.

Not for: a single project's wave table (use /vt-d-activate --status), or a domain's meeting dashboard with owner rounds (use /vt-p-domain-dashboard).

Prerequisites

  • intake/projects.yaml in the toolkit, listing the projects. An entry is on the board only when its board: is absent, empty or true (in any case). Every other value keeps it off: false silently, anything else such as no or off with a kept off the board notice on stderr. yes, on, ~ and null stop the board instead (see Error Handling): YAML reads them as true or absent, the registry parser as off. The toolkit itself is always on the board as V025.
  • Each project's working copy reachable at its path: with a .design-state.yaml.
  • python3 for every run: the registry is checked with the toolkit's YAML reader before the board reads it, and the JSON file is written with it. The Markdown and the terminal summary need Bash and awk besides.
  • Optional: OBSIDIAN_VAULT_PATH → the files land in <vault>/PM-Dashboards/, else in TOOLKIT_ROOT/docs/pm-dashboards/.
  • Optional: the claude CLI on PATH for the session overlay. Without it the board still renders and says live probe: unavailable(not-found).

Execution

Step 1: Run the board

Call the script through $CLAUDE_PLUGIN_ROOT. If that is not set, use scripts/board.sh relative to this skill file's real directory (deployed skills are symlinks into the toolkit checkout). Never call it by a repo-relative plugins/… path — reason: that path exists only when the current project is the toolkit, and the board is meant to run from any project (BUG-347).

bash "$CLAUDE_PLUGIN_ROOT/skills/spec-board/scripts/board.sh" [flags]
Flag Meaning
--domain NAME / --project ID Only the projects of that domain, or that one project
--no-live Skip the worktree and session probes; two runs with the same --date are byte-identical
--format md\|json\|terminal\|all Which outputs (default all)
--group-by project\|capability\|type Row order inside each column (default: project, priority, number)
--stale-days N Age after which a worktree index is badged wt-stale(Nd) (default 14)
--date YYYY-MM-DD The date in the file names and the header (default today)
--output-dir DIR / --projects-yaml FILE / --toolkit-root DIR Override the defaults above
--agents-json FILE Read a captured claude agents --json answer instead of calling claude

Exit 0 on every render; exit 2 on a bad argument, a missing library or a stage that failed (see Error Handling) — report what the script printed and stop. The session probe is killed once more than BOARD_PROBE_TIMEOUT whole seconds have passed on the clock, so between that limit and one second later, and gets SIGKILL one to two seconds after SIGTERM (default 10; a value that is not digits or has more than 6 digits counts as the default).

Step 2: Present the result

Show the terminal summary as printed: the count per column, the count gate line and the live-probe status. Then name the files the script reported with wrote ….

Columns (first rule wins): Needs attention — the board cannot place the card; Done; Archiviert/Parked; Review/Finalize — in progress with a review or finalize phase; In Progress · live / · no session — split only when the session probe answered (otherwise one In Progress column); Blocked — a card that would be Ready (a specified spec, a reopened bug, a fix-ready bug with investigation.md) while one of its blockers is still open — a wontfix bug counts as open, and so does a duplicate one until its duplicate_of target resolves (SPEC-197 D1); Ready; Backlog. Only those cards move to Blocked: an open blocker on an in-progress card shows as a deps-open: badge, and a proposed spec or a triaged bug with blockers stays in Backlog.

Header line 3 — Count gate: PASS|FAIL|SKIPPED compares the rows rendered with the registry entries read, per type. A FAIL names the type: a card was lost between the registry and the board. SKIPPED (specs 0/0, bugs 0/0) means no entry was read at all, for example a --project that matches nothing — there was nothing to compare, which is not a pass.

Header line 4 — WIP: <owner> N (<project> N, …) counts the In Progress and Review/Finalize cards per known owner (WIP: — when there are none). With the worktree probe on, it ends in · unmapped worktrees: N: linked worktrees that match no card.

Badges on a card, in this order:

Badge Meaning
bypass The card has a gate_bypass
forced The card was activated over open blockers (dependency_override)
drift(reg=<status>) The registry says <status>, the card's state.yaml something else
registry-only The card has a directory but no readable state.yaml; the status comes from the registry
untriaged A reported bug, not yet triaged
no-investigation A fix-ready bug without investigation.md, kept in Backlog
deps-open:<ids> An in-progress card whose blockers are still open
wt:<project>/<dir> The card's worktree; a trailing ? when it was matched by directory name only
wt-stale(Nd) That worktree's index is older than --stale-days
live:<status>(<waiting for>)×N A Claude session in that worktree, the most urgent status first
no-worktree An in-progress card without a worktree
blocks:N N cards not yet ended list this one as a blocker

Step 3: Read Needs attention

Each Needs-attention row carries a reason and the file to open:

Reason What it means Open
status-unknown:"<literal>" The status is not in the vocabulary the card's state.yaml (or .design-state.yaml for a registry-only card)
no-column:<literal> A declared literal the column table does not place scripts/board.sh beside this skill file (the column table)
tombstone:no-directory A registry entry without a directory the project's .design-state.yaml
state-yaml-oversized state.yaml past 256 KB, not trusted the card's state.yaml
phase-unknown:"<phase>?" spec_phase marked the phase ?: a gate status the vocabulary does not declare, or the phase could not be read — the column would rest on a guess the card's state.yaml (the gate's status:)
project tombstone: path-missing The registered path does not exist the toolkit's intake/projects.yaml
project tombstone: registry-missing, registry-symlink, registry-unreadable, refused:N The project's registry is absent, a symlink, unreadable, or holds N entries the parser refuses the project's .design-state.yaml

Step 4: Read the checks in the JSON

The JSON's checks[] holds eight checks: drift, ref-integrity, cycle, unknown-status, tombstone, bypass-without-reason, work-without-activation, count-gate. Only three of them show in the Markdown: count-gate in header line 3, unknown-status and tombstone as Needs-attention rows. A failing drift, ref-integrity, cycle, bypass-without-reason or work-without-activation is visible only in the JSON, so read it after every run that wrote one:

python3 -c 'import json, sys
for c in json.load(open(sys.argv[1]))["checks"]:
    print(c["gate"], c["status"], c["failed"]); [print("   ", d) for d in c["details"] if c["status"] == "FAIL"]' "<the .json path from wrote …>"

Report every check with status: FAIL and its details; a SKIPPED check could not run (for example work-without-activation under --no-live). The contract is specs/196-spec-board-v1-read-only/contracts/board-json.md in the toolkit.

Edge Cases

  • Probe unavailable (claude missing, timeout, non-zero exit, unreadable or unexpected output): the board renders with one In Progress column and says why in the header.
  • Several sessions on one worktree: the most urgent status shows, with a count (live:busy×2).
  • A project directory inside another repository has no worktrees of its own.
  • An alias literal (bug open, closed) stays visible on the card; the column follows its canonical literal.

Error Handling

  • Exit 2 with the usage text → a bad argument or a missing projects.yaml; show the usage text and do not retry with guessed flags.
  • board: reading project <id> failed, board: placement failed, board: render failed → a stage that decides the columns failed (exit 2) and nothing was written. A board without that stage would have put blocked cards in Ready with the count gate at PASS. Report the line and the awk error before it; do not re-run with --no-live to get past it.
  • board: reading projects.yaml failed → the registry could not be read or parsed (exit 2), so the board would have shown only the projects it read, with the count gate at PASS. Report the error before it, for example a file without read permission.
  • board: projects.yaml line N: YAML refuses it (<construct>) — … → the toolkit's YAML reader refuses the registry (exit 2): an anchor, alias, tag, merge or complex key, a tab, a duplicate key (a second projects: block too), a control character or a line break other than LF and CRLF. Name the line; rewrite it plainly.
  • board: projects.yaml item N: <field>: is <x> to the registry parser and <y> to YAML — … (or the parser and YAML read a different number of items, or board: on and off) → YAML and the line parser the toolkit reads the registry with disagree about that item (exit 2). The parser reads lines, not YAML, so any disagreement can hide a board: false and publish a private project; the board refuses the registry rather than guess. Typical causes: an item that does not start with path:, id: or name:, a nested list or mapping, a value continued on the next line, a comment or single quotes on id, name, domain or path, trailing blanks, a quoted top-level "projects":. Rewrite the item as flat key: value lines with plain or double-quoted values.
  • board: projects.yaml item N: the key <k>: is not one the registry writer uses — … → an item key outside the ones registry_write.sh writes (path, name, id, parent, type, domain, spec_root, board, registered, location, last_synced) (exit 2). Both readers skip an unknown key, so a misspelt or lookalike board: or path: (Board:, an invisible character in the key) would publish a private project. Retype the key, or remove a key added by hand.
  • board: projects.yaml not checked (python3 failed) → the registry check could not run (exit 2); report the Python error before it.
  • board: projects.yaml lists <path> both on the board (<id>) and off it → one directory has a public and a private entry (exit 2), also when one path: is another name for the same directory (a trailing /, ~/, a symlink). The board will not pick one; name both and remove the wrong one.
  • board: column table row […] disagrees with the status vocabulary or board: status vocabulary unavailable → the toolkit's vocabulary and the board's column table no longer agree (exit 2); report it as a toolkit defect.
  • board: JSON not written (python3 failed) → the Markdown is already written; report both facts.
  • board: <path> is a directory, not written → a directory sits at an output file's name (exit 2); report the path. A symlink at that name is removed and replaced, never written through.
  • board: projects.yaml entry <id> kept off the board → that entry's board: is neither true nor false; the project stays private. Report it, so the owner can write true or false.

Success Criteria

  • The terminal summary is shown, and every wrote … path is named.
  • Every Needs-attention reason the user sees comes with the file to open.
  • Every JSON check with status: FAIL is reported with its details.

Next actions

  1. Open the Markdown file the script reported (wrote …/Spec-Board-<date>.md).
  2. For a card in Ready: run /vt-d-activate SPEC-NNN (or BUG-NNN) in the owning project.
  3. For each Needs-attention row: open the file named in the table above for its reason and fix the entry there.
  4. For each failing JSON check from Step 4: fix the card its detail names in the owning project.
  5. For a file you want to compare: re-run with --no-live --date <date> — the output is then deterministic. Do not commit the board to the toolkit: docs/ deploys to the public docs site, so the default folder's Spec-Board-* files are gitignored.