board read STATE lines and knew nothing about a brief, so a programme running Voyage across several repos had no shared surface: nobody could answer which briefs were in flight, in what phase, and who was waiting on whom. Order 20260831T135934Z-696228720 fired the programme-level B1 row once /trekplan had been delivered in llm-ingestion-okf, so the fields were chosen against a run that had gone the whole way rather than guessed. Adds board.sh --voyage (a sixth rendering of the same scan, read-only) and a VOY column beside ORDRE and FLY, never summed with them and never in the sort. Detection is by PROPERTY, never by directory name: a directory holding brief.md or brief.md.draft under any of the three planning locations the convention recognises. It walks the FILESYSTEM, never the git index - llm-ingestion-okf gitignores .claude/projects/ (087be0b), so an index-based detector would report ZERO briefs in the one repo actually running one. The phase ladder measures ARTIFACTS, not sessions, and the legend says so: /trekexecute leaves a file behind only in its multi-session form, so a plan executed in one session leaves nothing and `plan` is the last thing the filesystem can prove. Nothing here claims a session is alive - the same refusal FLY carries. Three absences that must not borrow the shape of a measurement: brief_quality is read from the frontmatter block only and an absent field reads `-`, never `complete` (only 8 of ~40 briefs on the real tree carry it); research=- (never started) is distinct from research=0 (a directory holding nothing); and --voyage always prints its own denominator rather than rendering as an empty page. Blocking decisions are counted at DECLARATION SITES, not mentions: the pattern occurs on four lines of the real brief, of which one declares it, so a bare grep -c answers 4 where the honest answer is 1. The age is the NEWEST artifact, inverting the oldest-wins rule ORDRE and FLY carry - an order queue's problem is the oldest item still waiting, a project's problem is that its most recent activity is old. Mutation-verified three ways: newest->oldest turns exactly one check red, anchored pattern->bare string turns three red (the known-negative control among them), filesystem->git ls-files turns 23 red. Live-verified against the completed run: fase=plan, kvalitet=complete, blokkerende=1, gate=S4, research=5 - the only project of 51 across 14 repos with an open blocking decision. Cost measured rather than assumed: 0.31.0 takes 6.3s over the real 52-repo tree, with VOY 7.7-9.2s. The first cut called stat once per artifact and took 13.4s; the batched form is what makes the column affordable. CLAUDE.md's old "~3s" claim did not survive the measurement and is corrected. Bounded gap, stated rather than closed: whether a detected project is still "in flight" is not decided here. That needs a threshold, and a threshold would make the board decide that work is abandoned - the identical thing the order queue is already forbidden from doing. board-selftest.sh: 325 -> 360 checks. Co-Authored-By: Claude <claude-opus-5>
346 lines
19 KiB
Markdown
346 lines
19 KiB
Markdown
---
|
|
name: board
|
|
description: >-
|
|
Answer "which repository deserves the next session" across every repo at once,
|
|
using the local cross-repo attention board (`board.sh`) as the evidence. Use
|
|
whenever the user asks what to work on next, who is waiting on them, what
|
|
unblocks the most other work, what is cheapest to move under quota pressure, or
|
|
for a cross-repo status read: "what should I work on", "who is waiting on me",
|
|
"what unblocks the most", "show the board", "what is cheapest to move",
|
|
"cross-repo status", "where is the leverage", "which repo is blocked". Also
|
|
covers planning a whole day rather than picking one repo: "plan my day", "which
|
|
repos should I open tabs for", "day plan" — including a day with a subject:
|
|
"focus day on X", "today I am only working on X", "which repos does X touch".
|
|
Also triggers on Norwegian phrasings:
|
|
"hva skal jeg jobbe med", "hvem venter på meg", "hva løsner mest", "vis tavlen",
|
|
"hva er billigst å flytte", "hvor bør jeg begynne", "status på tvers av repo",
|
|
"hva er blokkert", "lag en dagsplan", "planlegg dagen", "hvilke repo skal jeg
|
|
åpne i dag", "fokusdag på X", "i dag jobber jeg bare med X", "hvilke repo
|
|
gjelder X". Also covers Voyage briefs in flight across repos: "which briefs are
|
|
running", "what phase is that brief in", "which brief is blocked on a decision",
|
|
"show the voyage board", "hvilke briefer er i gang", "hvilken fase ligger den i",
|
|
"hvilke briefer venter paa en beslutning", "vis Voyage-oversikten". Trigger even when the
|
|
user names no repo and no tool — choosing *between* repos is this skill. Not for
|
|
"where were we" inside the current repo: that is this repo's own STATE.md,
|
|
already injected at session start.
|
|
version: "0.31.0"
|
|
---
|
|
|
|
# board — which repo deserves the next session
|
|
|
|
This skill turns "what should I work on" into a decision backed by measurement.
|
|
`board.sh` is the engine; this skill is the ranking and the answer. The script is
|
|
read-only by construction — it never writes to a repo, a STATE.md, or the mailbox
|
|
— so running it is always safe and needs no confirmation.
|
|
|
|
The failure mode this skill exists to prevent is dumping the table. The board
|
|
prints one line per repo across every repo on the machine; pasting that back is
|
|
not an answer, it is the question again in table form. Run it, read it, name one
|
|
repo.
|
|
|
|
## The engine
|
|
|
|
BOARD="${CLAUDE_PLUGIN_ROOT}/scripts/board.sh"
|
|
|
|
There is no deployed copy anywhere else and no fallback path. One entry point is
|
|
the whole point: two entries pointed at two copies and made it impossible to know
|
|
which one actually ran. If this fails to resolve, the fix is this expression —
|
|
never restoring a copy under `~/.claude/scripts/`.
|
|
|
|
"$BOARD" # every repo under the default roots
|
|
"$BOARD" --roots <dir>[,<dir>...] # scan somewhere else
|
|
|
|
`BOARD_ROOTS` overrides the default scan roots and `CLAUDE_COORD_DIR` the mailbox
|
|
root. Exit 0 is normal (0 discovered repos also exits 0, silently); exit 2 means a
|
|
malformed argument — read stderr and fix it rather than retrying.
|
|
|
|
**Re-run it every single invocation.** Never answer from a board you ran earlier
|
|
in the session, from a number quoted in a document, or from memory. Inbox counts,
|
|
uncommitted files and board lines all change between turns, and a recommendation
|
|
built on a stale count is the exact defect the operator's premise-verification
|
|
rule exists to stop. The run costs about eight seconds over ~50 repos (measured 2026-08-31; the
|
|
older "about three seconds" figure predates both the current tree size and
|
|
the `VOY` column).
|
|
|
|
## What the columns mean
|
|
|
|
| Column | Meaning |
|
|
|---|---|
|
|
| `STATUS` | `planned` / `in-progress` / `blocked` / `deferred` / `done`, or `blocked>X` naming the repo it waits on. `?` means the STATE.md has no board line. |
|
|
| `KOST` | Model/effort for the next step, from the rubric row table in `route.sh` (the `route` skill writes it; this one only reads it). |
|
|
| `INN` | Unhandled inbox: **other repos are waiting on THIS one**. An obligation it owes outward. |
|
|
| `ORDRE` | Pending orders: **authorized work is waiting on this repo**, unclaimed and pickable. |
|
|
| `FLY` | Orders in flight (claimed). Someone TOOK the order — never proof a session is still alive. |
|
|
| `VOY` | Voyage projects (briefs in flight). `N:Md` = how many, and how long since the **stalest** one's newest artifact. A bare `0` means none. Per-project detail lives in `--voyage`. |
|
|
| `DRT` | Uncommitted files. |
|
|
| `ALDER` | Days since STATE.md last changed — the age of the *plan*. `-` where the repo has none. |
|
|
| `SISTE` | Days since the last commit — the age of the *work*. `-` where the repo has no commits yet. |
|
|
| `NESTE` | First line of the STATE.md next-step block, truncated. |
|
|
|
|
## Briefs in flight — `--voyage`
|
|
|
|
"$BOARD" --voyage
|
|
|
|
Answers what no single `STATE.md` can: **which Voyage briefs are running, in what
|
|
phase, and who is waiting on whom.** One `key=value` block per project.
|
|
|
|
`fase` is the only field here that cannot be derived from a STATE.md at all, and
|
|
it is why the view exists: `brief-draft` (the `/trekbrief` review gate has not
|
|
cleared) → `brief` → `research` → `plan` → `execute` → `review`.
|
|
|
|
Read these three the way the engine means them, and never soften them:
|
|
|
|
- **`fase` measures ARTIFACTS, not sessions.** A plan executed in one session
|
|
leaves no file, so `plan` is the last thing the filesystem can prove. Nothing
|
|
here says a session is alive — the same refusal `FLY` carries.
|
|
- **`kvalitet=-` means the `brief_quality` field is ABSENT, never that the brief
|
|
is complete.** Only 8 of ~40 briefs on the real tree carry it. `partial` vs
|
|
`complete` is exactly what three presence-greps cannot tell apart.
|
|
- **`research=-` and `research=0` are different facts.** `-` is no research
|
|
directory (never started); `0` is a directory that exists and holds nothing —
|
|
a research step with null output.
|
|
|
|
`venter=operatoerbeslutning` means the brief declares an open
|
|
`[BLOCKING DECISION, before S<n>]`, and `blokkerende_gate` names the step it
|
|
gates. That is the one form of "who waits on whom" the files can prove; the
|
|
`ordre_id=` lines beside it carry the repo's pending orders, which is what lets a
|
|
reader go from "this brief is standing still" to "this order is pending".
|
|
|
|
Report what the blocks say. Do **not** infer that a brief is abandoned, that a
|
|
decision has since been resolved, or that a session is running — the view
|
|
reports and refuses the inference, and so should you.
|
|
|
|
**`ORDRE` and `FLY` are never summed, and `FLY` is never read as "busy."** They
|
|
are the same queue in two states. Before `FLY` existed, a repo with one order in
|
|
flight and a repo with no orders at all both printed `ORDRE 0` — the same digit
|
|
for two opposite facts, which is how two tabs sat idle for 45 hours holding
|
|
finished orders with nothing on the board reporting it. What `FLY` still cannot
|
|
tell you is whether a session is *running*: nothing un-claims an order when the
|
|
session that claimed it dies (one order on the live mailbox had been claimed for
|
|
117 hours). Say "an order is claimed here", never "a session is working here".
|
|
The board inspects no processes and will not start.
|
|
|
|
**`INN` never means "this repo is waiting on someone."** It means the opposite:
|
|
messages arrived and were not handled. The mailbox format carries no reply-to or
|
|
thread field, so outbound waiting is not derivable from it at all — `blocked>X` is
|
|
the only source for who a repo waits *on*. Conflating the two axes is a known
|
|
defect class here; keep them apart in every sentence you write.
|
|
|
|
## How to rank
|
|
|
|
Apply in order and stop at the first that discriminates. Always say which rule
|
|
fired — the rule is the justification, and it is what lets the operator disagree.
|
|
|
|
1. **Unblocks the most.** A repo that others are `blocked>` on. Moving it converts
|
|
several blocked repos to active: the highest leverage per token spent. Follow
|
|
the chain — if A is blocked on B and B is blocked on C, the answer is C.
|
|
2. **Cheap and blocking.** Among unblockers, prefer the lowest `KOST`. This is the
|
|
standing rule under quota pressure: take the cheap blocking one first.
|
|
3. **Owes the most outward.** High `INN` means other sessions are stalled waiting
|
|
for a reply. Answering is procedurally mandatory anyway, so this work is owed
|
|
regardless of what else is on the board.
|
|
4. **Uncommitted risk.** High `DRT` is not urgent work, but it is exposure worth
|
|
naming: the config backup mirrors commits, not working trees, so uncommitted
|
|
files exist in exactly one place.
|
|
|
|
**Neither `ALDER` nor `SISTE` is a ranking input.** An old STATE.md often means a
|
|
finished repo, not a neglected one, and an old last commit can mean the same.
|
|
Report either only if the user asks about staleness directly — the four rules
|
|
above stay four.
|
|
|
|
Read them as a **pair**, though: they answer different questions, and the gap
|
|
between them is the reading neither gives alone. A fresh `ALDER` beside a long
|
|
`SISTE` is a repo that keeps being re-planned without anything landing, which no
|
|
other column on the board reports.
|
|
|
|
`deferred` is a deliberate choice, not neglect — do not surface a deferred repo as
|
|
a candidate unless nothing else qualifies, and say that it was deferred if you do.
|
|
|
|
## Read the winner's STATE.md before answering
|
|
|
|
The `NESTE` column is truncated to fit the table, so it is a pointer, not the
|
|
action. Once ranking picks a repo, open that repo's `STATE.md` and read the actual
|
|
next-step block. Report the real first action from that block. A truncated
|
|
fragment quoted as if it were the next step is a wrong answer that looks right.
|
|
|
|
## The answer
|
|
|
|
Short prose, roughly five to ten lines. No table, no per-repo rundown, no top-five
|
|
list — "top five with their columns" is still a dump.
|
|
|
|
- **The repo**, named once.
|
|
- **Why it won**, as the rule that fired.
|
|
- **The concrete first action**, from that repo's STATE.md next-step block.
|
|
- **What it costs** — the `KOST` value, so the operator can set model and effort
|
|
before starting.
|
|
- **Runner-up in one clause**, only if it is genuinely close.
|
|
|
|
If the user asked a narrower question ("who is waiting on me", "what is cheapest"),
|
|
answer that question directly from the same run instead of forcing the full
|
|
ranking onto them.
|
|
|
|
## When the ask is a day, not a repo
|
|
|
|
"Hvilke repo skal jeg åpne tabber for i dag", "lag en dagsplan", "planlegg dagen",
|
|
"hva står på programmet" — that is a different question from "which repo wins",
|
|
and it has its own rendering:
|
|
|
|
"$BOARD" --plan
|
|
|
|
This is the one case where a list *is* the answer and the no-dumping rule does not
|
|
apply — the user asked for the day, and a day has more than one repo in it. Pass
|
|
the plan through with a short framing line; do not re-rank it, re-order it, or trim
|
|
it. The order is the engine's position and it is deterministic — five ordered
|
|
groups, each a lookup, no weights:
|
|
|
|
1. chain-root credit (repos released transitively, most first)
|
|
2. unhandled inbox (most owed first, whatever the status)
|
|
3. planned
|
|
4. in-progress
|
|
5. no declared status (last, and labelled)
|
|
|
|
Within a group: that group's own quantity, then a Sonnet `next-cost`, then oldest
|
|
plan first. `planned` sits above `in-progress` by operator decision at 0.20.0 —
|
|
turning a decision into motion is the slow step; live work is already moving.
|
|
|
|
**Chain-root credit is the term worth understanding before you explain an order
|
|
to the operator.** For every `blocked` repo the engine follows `blocked-on` to
|
|
the ROOT of the chain and credits only the root, transitively. Opening a blocked
|
|
repo releases nobody — its next step is by definition waiting; opening the root
|
|
releases everything behind it. A cycle or a `blocked-on` naming an unscanned repo
|
|
credits nobody. This is engine rule 1 above, now computed rather than eyeballed.
|
|
|
|
Debt is **uncapped and never excluded** by decision: owing a reply is the other
|
|
axis from a repo's own next step, and answering is often what unblocks a chain.
|
|
Sitting one group below chain-root credit is not a cap — a debtor keeps its tab,
|
|
its most-owed-first position among the other debtors, and its `why=inbox:N`.
|
|
|
|
`why=` names the **group** that put the repo in the plan, so a block can read
|
|
`why=unblocks:2` even though the repo also owes mail. Read it as "what opening
|
|
this releases", not as the only reason it qualified. Substituting your own judgement for the order makes
|
|
the plan unreproducible and costs the property that makes it trustworthy.
|
|
|
|
Two things to say out loud when you hand it over:
|
|
|
|
- **A `done` or `blocked` repo in the plan is not a bug.** It is there because it
|
|
owes mail, and owing an answer is a different axis from whether its own next step
|
|
can move. `why=inbox:N` says exactly that.
|
|
- **`command_missing=` means that repo has no usable route line**, so there is no
|
|
command to paste. Name those repos rather than letting the operator discover it
|
|
per tab. Fixing them is the `route` skill's job, in *that* repo — never a side
|
|
quest here.
|
|
- **`ledig_antall=N` and the `ledig=` lines are free capacity, not tabs.** They
|
|
name the repos that can take NEW work: nothing owed, nothing queued, nothing in
|
|
flight, clean tree, at `done` or `deferred`. They carry no `tab=` and no
|
|
command on purpose — there is no next step to start, so the operator decides
|
|
what to send there. Read them out when the ask is about capacity ("hvem kan ta
|
|
mer arbeid", "hvor har jeg ledig kapasitet") and whenever the plan is short.
|
|
`status=done` alone is **not** the same set: on the real tree 4 of 17
|
|
done/deferred repos were not free. Never derive this list yourself from the
|
|
table — the engine joins four fields you would have to join by hand.
|
|
- **`fly=N` on a tab block means that repo already holds a claimed order.** Say
|
|
so before the operator opens the pane. It is not proof a session is live, and
|
|
it is not a reason to drop the tab — it is a reason to look first.
|
|
|
|
### When the day has a subject ("fokusdag")
|
|
|
|
"I dag jobber jeg bare med X", "fokusdag på X", "hvilke repo gjelder X" — pass the
|
|
user's own words through, untouched:
|
|
|
|
"$BOARD" --plan --focus "<the user's phrasing, verbatim>"
|
|
|
|
**Do not tokenize, normalize or translate the phrase into a slug yourself.** The
|
|
engine resolves prose against the topic markers the scanned `STATE.md` files
|
|
actually declare, and it reports what it resolved to. Guessing a slug here would
|
|
put a judgement call in front of a lookup that is already deterministic — and
|
|
`fokus=` in the output tells the user what the phrase resolved to, which your
|
|
guess would silently replace.
|
|
|
|
This is the only rendering that *hides* repos, so three lines carry weight and
|
|
must be relayed, never trimmed as noise:
|
|
|
|
- **`fokus_utenfor=`** — repos that *mention* the subject with no marker line.
|
|
They are named, not counted, because that is where the misses live: a repo can
|
|
be a heavy participant and still never have written a marker. Say "nevner",
|
|
never "dekker" — this reports text found in a file, not relevance.
|
|
- **`fokus_droppet=`** — how many blocks the cutoff removed.
|
|
- **`fokus_rekkevidde=`** — how many `STATE.md` were searched. The scan opens no
|
|
other file, so a repo whose evidence lives in a `README` is invisible to it.
|
|
|
|
If `fokus_ikke_brukt=` appears, the phrase matched no declared subject and the
|
|
**full** plan came back. Say so plainly — the user asked for a narrow day and got
|
|
a wide one, and presenting it as focused would be the one failure this whole
|
|
rendering is built to avoid.
|
|
|
|
Blocks are `key=value` and comment lines start with `#`, because a separate driver
|
|
repo consumes this output. That format is a contract with a consumer outside this
|
|
repo: never reformat it, and never hand-edit a block before passing it on.
|
|
|
|
**Point the operator at `paste=`, not at `dir=` + `command=`.** Those two exist
|
|
for a driver, which cd's the pane itself and then types the command; `paste=` is
|
|
the same thing already joined, and it is the only line a human should ever copy.
|
|
Telling someone to "cd to `dir=` and then paste `command=`" asks them to assemble
|
|
two fields by hand, which is exactly how a session gets started in the wrong repo.
|
|
A block with `command_missing=` has no `paste=` at all — say so for those repos
|
|
rather than letting the operator hunt for a line that is not there.
|
|
|
|
## Coverage gaps you must name
|
|
|
|
A ranking is only as honest as its inputs, and two gaps are invisible in the answer
|
|
unless you state them:
|
|
|
|
- **Repos with no board line** render `?` in status and cost, so they drop out of
|
|
exactly the sort the board exists for. The script counts them in its closing
|
|
note. Pass that count through as one clause ("N repos have no board line and are
|
|
outside this ranking"), and never let it grow into a side quest — adding board
|
|
lines to other repos is that repo's work, not this session's.
|
|
- **`MALFORMED` status tokens** mean a board line outside the closed vocabulary.
|
|
The script warns; repeat the warning and name the repo, because a malformed line
|
|
silently misplaces a repo in the wrong bucket.
|
|
|
|
Repos with no STATE.md at all appear in their own group. They are not candidates —
|
|
there is no next step to read — but a large uncommitted count in one is worth a
|
|
sentence under rule 4.
|
|
|
|
## Boundaries
|
|
|
|
- **Read-only, both ways.** The script writes nothing, and neither should the
|
|
skill. Do not fix a missing board line, edit another repo's STATE.md, or send a
|
|
coord message as a side effect of ranking. Recommend; the operator decides.
|
|
- **Not a session-start hook.** Injecting the board into every session costs real
|
|
context about repos the session is not in. It runs when asked, never on a timer.
|
|
- **Coordination metadata is private.** Repo names, blocking chains and inbox
|
|
counts are local operational data and never belong on a public surface.
|
|
|
|
## Examples
|
|
|
|
Placeholder names throughout — resolve real ones from the run, never from a
|
|
document.
|
|
|
|
**Example 1 — the ordinary ask**
|
|
Input: "hva skal jeg jobbe med nå?"
|
|
Action: run the board. `repo-a` is `blocked>repo-b`, and `repo-b` is active at a
|
|
low cost. Answer: `repo-b`, rule 1 + 2 (cheap and unblocks `repo-a`), the first
|
|
action read from `repo-b/STATE.md`, and its `KOST`.
|
|
|
|
**Example 2 — quota pressure**
|
|
Input: "jeg har lite kvote igjen, hva er billigst å flytte?"
|
|
Action: run the board, restrict to candidates whose `KOST` is at the low end of the
|
|
rubric, and prefer one that also unblocks something. Name the cost explicitly.
|
|
|
|
**Example 3 — the obligation question**
|
|
Input: "hvem venter på meg?"
|
|
Action: read `INN` only. List the repos with unhandled mail and say that this is
|
|
mail owed outward, not repos this one is waiting on. Do not rank; the question was
|
|
narrower than the ranking.
|
|
|
|
**Example 4 — chain following**
|
|
Input: "what unblocks the most?"
|
|
Action: build the `blocked>` chain from the run, and name the repo at its root,
|
|
plus how many repos come free when it moves.
|
|
|
|
**Example 5 — nothing qualifies**
|
|
Input: "vis tavlen"
|
|
Action: if every repo is `done` or `deferred`, say so plainly and name the largest
|
|
uncommitted exposure instead of inventing a candidate. An empty board is a real
|
|
answer.
|