feat(board): --voyage reports the Voyage briefs in flight

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>
This commit is contained in:
Kjell Tore Guttormsen 2026-08-31 18:11:04 +02:00
commit 85cd628c3e
5 changed files with 701 additions and 21 deletions

View file

@ -16,7 +16,10 @@ description: >-
"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". Trigger even when the
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.
@ -55,7 +58,9 @@ malformed argument — read stderr and fix it rather than retrying.
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 three seconds.
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
@ -66,11 +71,45 @@ rule exists to stop. The run costs about three seconds.
| `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