repo-mailbox/skills/board/SKILL.md
Kjell Tore Guttormsen 5e5bc4a66e feat(route,board): strike the advisor rule, add board.sh --row <repo>
Order 20260912T202210Z-7588027378-from-.claude, operator decision
2026-09-12 (helhetlig vurdering av arbeidssystemet, cut row 3 and the
board.sh --row improvement row). One order, two parts, one version bump.

THE ADVISOR RULE IS STRUCK. route.sh and board.sh --dispatch emit no
--advisor at all. The rule fired per ROW on a need - always on the Sonnet
rows (a capability lift, which is what made the quota fallback safe to
take), and on the Opus rows at reversibility=costly|one-way - and it read
well. It was killed by a MEASUREMENT, not by taste: of 54 dispatches the
PM issued 08.-12.09, ZERO carried the flag, because sessions are started
by hand from the model and effort rather than from the whole emitted
line. A rule nothing honours is not a policy, and an emitted value nobody
acts on is decoration in a field whose only job is to be evidence. The
advisor is now what it already was in practice: an operator decision per
session, said in one sentence in route.sh --help.

The comments that rested on the rule were REWRITTEN, not left standing.
board.sh --dispatch still refuses a --model/--effort pair, but the reason
is no longer "the advisor is a property of the ROW": it is that the rubric
has exactly one copy, and a dispatch taking the model directly would be a
second, unscored way to reach the same decision - recording no traits, no
rationale and no next-cost, so nothing afterwards could say whether the
routing or the scoring was wrong. A comment defending a removed mechanism
is how the next session restores it. Both skills carry the correction.

Pinned as an ABSENCE over the whole trait space - 81 combinations, every
line of output, with a known-positive control proving the sweep's grep
can find a planted advisor - rather than on four sampled rows, because
the claim is that no path emits it. board.sh --dispatch at
reversibility=costly is pinned separately: that is the exact input a
reintroduced rule would fire on. The literal string is absent from
route.sh entirely, including the paragraph recording what was struck (it
says "an opus advisor flag" in words), because a blunt grep cannot tell a
description from a specification. Backward compatibility is pinned rather
than assumed: a route line carrying a legacy advisor= field still parses
and still yields a command - measured, 0 of 48 route lines in ~/repos
carry one, but a reader that broke on an unknown field would turn last
month's STATE.md into "that repo has no route line". The three CLI gates
section 14 carried went with the rule; the suite no longer depends on the
installed claude at all.

board.sh --row <repo> IS THE SEVENTH RENDERING of the same scan, never a
second scan, read-only like every other one. (The order calls it the
sixth; by this file's own numbering --inbox-plan is the fourth and
--dispatch the fifth. Corrected rather than carried wrong.) It exists
because the columns WERE misread: on 11.09 the PM read FLY off the table
by eye and got it wrong, while every other rendering a program consumes
is already key=value. inn, ordre and fly are three separate fields
because they are three separate facts; status is the bare token, never
the table's blocked>target display, with blocked-on beside it; neste is
last and uncut. An unknown repo exits 2 and writes NOTHING to stdout - an
empty block would read as a repo whose every column is blank, which is a
real and different state.

upushet is the ONE field that is not a rendering of the scan, and it is
named rather than blended in: nothing in the scan measures it, so it is
read once, for the named repo only, and never enters the table, the plan
or the briefing. It reads the remote-TRACKING ref, not the remote, so
upushet=N honestly means "the local ref says N"; a repo with no upstream
reports ?, never 0.

The row fixture's three counts are three DIFFERENT integers (3/2/1), and
that is the finding worth recording. Built first with 2/1/1, it was
mutation-tested by making fly read the ORDRE field - the exact 11.09
misreading - and the check stayed GREEN, because the two fields held the
same digit. A fixture that cannot tell two columns apart is the defect
wearing a passing test, inside the section written to prevent it.

Suites under /bin/bash 3.2, before -> after: coord 257 -> 257, board
393 -> 427, route 73 -> 73 (13 advisor checks and 3 CLI gates out, 15
absence/legacy checks in, and it no longer varies with claude being on
PATH), orders 116 -> 116, state-line-guard 54 -> 54. Sum 893 -> 927,
README badge updated to the measured sum. npm test 12/12, fail 0.

Verified live against the real tree, not only fixtures: --row
repo-mailbox reports fly=1 beside ordre=0 (the distinction that was
misread), --row on the nested key from-ai-to-chitta/content-sadhguru
resolves, and an unknown repo exits 2.

No tag, no push, no catalog change - that is the operator's
release-plugin.mjs run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:45:47 +02:00

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.34.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.