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.yamlfiles, 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.yamlin the toolkit, listing the projects. An entry is on the board only when itsboard:is absent, empty ortrue(in any case). Every other value keeps it off:falsesilently, anything else such asnooroffwith akept off the boardnotice on stderr.yes,on,~andnullstop 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 asV025.- Each project's working copy reachable at its
path:with a.design-state.yaml. python3for 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 inTOOLKIT_ROOT/docs/pm-dashboards/. - Optional: the
claudeCLI onPATHfor the session overlay. Without it the board still renders and sayslive 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).
| 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 (
claudemissing, 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-liveto 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 secondprojects: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, orboard: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 aboard: falseand publish a private project; the board refuses the registry rather than guess. Typical causes: an item that does not start withpath:,id:orname:, a nested list or mapping, a value continued on the next line, a comment or single quotes onid,name,domainorpath, trailing blanks, a quoted top-level"projects":. Rewrite the item as flatkey: valuelines 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 onesregistry_write.shwrites (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 lookalikeboard:orpath:(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 onepath: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 vocabularyorboard: 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'sboard:is neithertruenorfalse; the project stays private. Report it, so the owner can writetrueorfalse.
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: FAILis reported with its details.
Next actions¶
- Open the Markdown file the script reported (
wrote …/Spec-Board-<date>.md). - For a card in Ready: run
/vt-d-activate SPEC-NNN(orBUG-NNN) in the owning project. - For each Needs-attention row: open the file named in the table above for its reason and fix the entry there.
- For each failing JSON check from Step 4: fix the card its detail names in the owning project.
- 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'sSpec-Board-*files are gitignored.