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

102
CLAUDE.md
View file

@ -411,7 +411,7 @@ marketplace plugin. Three components, one boundary:
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
next-step blocks + board lines, `git status`, and mailbox pending counts, and
prints one line per repo. Read-only by construction: it writes to no repo, no
STATE.md and no mailbox. Pinned by `board-selftest.sh` (325 checks).
STATE.md and no mailbox. Pinned by `board-selftest.sh` (360 checks).
**It lives here because the mailbox is one of its three inputs, and it carries
the same axis distinction the mailbox does.** A pending count means *others
@ -1074,6 +1074,98 @@ marketplace plugin. Three components, one boundary:
way `--dispatch` requires `--target-pane` to be measured by the caller and
passed in. Not built: not ordered, and keeping it out keeps the dependency
running one way.
- **`--voyage` is the SIXTH rendering, and it exists because `board` read STATE
lines and knew nothing about a brief.** Order 20260831T135934Z-696228720
(`.claude`, 2026-08-31) fired the programme-level B1 row once `/trekplan` had
been delivered in `llm-ingestion-okf`, so the fields could be chosen against a
run that had gone the whole way (`brief.md.draft` 30.08 -> `brief.md` -> four
research topics + README 31.08 14:19-14:54 -> `plan.md` 15:30) rather than
guessed. A programme with Voyage in several repos had no shared surface at
all: nobody could answer which briefs were in flight, in what phase, and who
was waiting on whom. Read-only holds untouched - every field is a read.
**Detection is by PROPERTY, never by directory name, and never through the git
index.** The global CLAUDE.md says plans and briefs are defined by properties
and recognises three locations (`.claude/projects/`, `docs/`,
`features/<n>-<name>/`), so a directory counts iff it HOLDS `brief.md` or
`brief.md.draft` under one of them. The index-based alternative was refused on
a measurement the order supplied and this repo re-checked: `llm-ingestion-okf`
gitignores `.claude/projects/` (commit 087be0b), so `git ls-files` would report
ZERO briefs in the one repo actually running one - a broken query returning a
positive-looking null, which is ansikt 4 pointed at our own tooling.
Mutation-verified: swapping the filesystem test for `git ls-files` turns 23
checks red, and the fixture asserts its own ground truth (`git check-ignore`
really does ignore it) before anything depends on it. The known-negative
control the location rule already passes is a real one - `maskinrommet`'s
`serier/*/brief.md` are content briefs, not Voyage projects, and sit outside
all three locations.
**The phase ladder measures ARTIFACTS, not sessions, and the legend says so in
those words.** `brief-draft` -> `brief` -> `research` -> `plan` -> `execute` ->
`review`, furthest artifact present wins. The bound is stated rather than
hidden: `/trekexecute` leaves a file behind only in its MULTI-session form
(`.session-state.local.json`), 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, and pinned the same way, as
a check on the legend TEXT.
**Three fields where an ABSENCE must not borrow the shape of a measurement.**
`brief_quality` is read from the FRONTMATTER BLOCK ONLY (a body line is
untrusted text and must not speak for the brief, the same rule
`coord-count.sh` applies to `reply-expected`), and an absent field reads `-`,
never `complete`: measured across the real tree 2026-08-31, only 8 of ~40
briefs carry it at all. `research=-` (no directory, never started) and
`research=0` (a directory holding nothing) are different facts - the completed
run's `research/README.md` opened with "this directory held 0 files when the
session started", and no board could show that. And `--voyage` always prints
its own denominator; a tree with no projects says `ingen=0 ... funnet i N
skannede repo` rather than rendering as an empty page.
**Blocking decisions are counted at DECLARATION SITES, not mentions.** The
pattern `[BLOCKING DECISION, before S<n>]` occurs on FOUR lines of the real
brief, of which ONE declares it and three refer to it in prose, so a bare
`grep -c` answers 4 where the honest answer is 1. The bracket must open the
line's content after list markers, headings and bold. Mutation-verified:
replacing the anchored pattern with the bare string turns exactly three checks
red, the known-negative control (a brief whose only occurrences are prose must
count 0) among them. It reports what the brief DECLARES and infers nothing
about resolution - board reports and refuses the inference.
**The age is the NEWEST artifact, which INVERTS the oldest-wins rule ORDRE and
FLY carry, deliberately.** An order queue's problem is the oldest item still
waiting; a project's problem is that its most recent activity is old
("nothing has happened here for N days"). Both answer the same question about
a different object. Mutation-verified: flipping `-gt` to `-lt` turns exactly
the one oldest/newest check red with every control green. The repo-level `VOY`
cell then takes the STALEST project's age, which is the oldest-wins rule
applied one level up.
**`VOY` is display-only, never summed with `ORDRE`/`FLY`, and never in the
sort** - the standing rule that a new ranking axis would silently reorder a
parser living in another repo. It is not in `--plan`'s `key=value` blocks
either: the requirement is that a human reading the board can see a brief is
standing still, and the table is where that is read. The per-project detail
lives in `--voyage` alone, because one cell cannot carry a phase when a repo
holds seven briefs (`portfolio-optimiser` holds 20).
**The cost was measured rather than assumed, and this file's old "~3s" claim
did not survive it.** Shipped 0.31.0 takes **6.3s** over the real 52-repo tree;
with `VOY` it is **7.7-9.2s**. The first cut called `stat` once per artifact
and took 13.4s - the batched form (`find ... -exec stat -f %m {} +`, one
`find` and one `stat` per project) is what makes the column affordable beside
the table. Live verification against the completed run reproduced every field
the order named: `fase=plan`, `kvalitet=complete`, `blokkerende=1`,
`blokkerende_gate=S4`, `research=5`, and it is the only project of 51 across
14 repos with an open blocking decision.
**Bounded gap, stated rather than closed:** whether a detected project is
still "in flight" is NOT decided here. Distinguishing a live brief from a
finished one needs a threshold, and encoding one would make the board decide
that work is abandoned - the identical thing the order queue is already
forbidden from doing ("nothing here expires anything"). The board reports
phase and age and leaves the judgement to the reader, which is why 51
projects are listed rather than some filtered subset.
- **Skills (`skills/coord-send/`, `skills/board/`, `skills/route/`, `skills/dispatch/`):** natural-language front
doors mapping user intent to engine invocations. No mailbox logic lives here
either. `board` additionally owns the *ranking* — which repo wins and why —
@ -1122,7 +1214,7 @@ obligations in another repo.
builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (242/242),
`bash scripts/board-selftest.sh` must exit 0 (325/325),
`bash scripts/board-selftest.sh` must exit 0 (360/360),
`bash scripts/route-selftest.sh` must exit 0 (69/69),
`bash scripts/orders-selftest.sh` must exit 0 (110/110) and
`bash scripts/state-line-guard-selftest.sh` must exit 0 (54/54).
@ -1156,7 +1248,11 @@ obligations in another repo.
- Hook smoke test: `node hooks/scripts/session-start.mjs` (expects JSON on stdout)
- State-line-guard smoke test: `echo '{"tool_name":"Write","tool_input":{"file_path":"/tmp/STATE.md","content":"x\n"}}' | node hooks/scripts/pre-state-line-guard.mjs; echo $?`
(expects exit 0, no output — a one-line STATE.md is under the limit)
- Board smoke test: `bash scripts/board.sh` (read-only, ~3s over the real tree)
- Board smoke test: `bash scripts/board.sh` (read-only, ~8s over the real
52-repo tree - measured 2026-08-31; the older "~3s" figure predated both
the current tree size and the VOY column)
- Voyage smoke test: `bash scripts/board.sh --voyage` (read-only, writes
nothing; ~22s, since it greps every detected brief)
- Briefing smoke test: `bash scripts/board.sh --brief` (read-only, writes
nothing). `brief-nightly.sh` DOES write — it overwrites `$CLAUDE_BRIEF_FILE`
(default `~/.claude/briefing.md`), so point that at a scratch path when

View file

@ -114,6 +114,7 @@ Ownership is explicit rather than implied. An order is pending until a session *
coord-count.sh [--exclude <mailbox>] # per mailbox: pending + replies owed, delivering nothing
coord-sweep.sh [--write] [--days <n>] [--log <path>] # close aged notices machine-wide (dry-run by default)
board.sh [--roots <dir>[,<dir>...]] [--brief|--plan] [--focus "<prose>"] # cross-repo attention board (read-only)
board.sh --voyage # Voyage briefs in flight (read-only)
board.sh --dispatch --repo <name> --order-id <id> \
--target-pane <yes|no> --path <v> ... --rationale "<why>" # startup command for a session in <name>
coord-order-send.sh --to <repo> --subject "<s>" --prompt-file <abs path> # deliver a work order into <repo>'s queue
@ -133,6 +134,8 @@ The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to …
It makes **zero model calls**, which is the point rather than a detail. Under subscription auth a headless session draws from the same quota pool as interactive work, and `--max-budget-usd` is a runaway brake rather than a pre-flight gate — measured against 2.1.220, it aborts *after* the first turn, never before it. `board.sh --brief` writes nothing; the file write lives in `brief-nightly.sh`, which renders to a temp file and renames it into place, and refuses to replace a good briefing with an empty render. `launchd/` holds a sample agent that runs it nightly; it points at a checkout, never at the version-pinned plugin cache.
**`board.sh --voyage` reports the Voyage briefs in flight**, a sixth rendering of the same scan. `board` reads STATE lines, which say nothing about a brief, so a programme running Voyage across several repos had no shared surface: nobody could answer which briefs were running, in what phase, and who was waiting on whom. 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 (`.claude/projects/`, `docs/`, `features/<n>-<name>/`) — and it walks the **filesystem, never the git index**: a repo that gitignores `.claude/projects/` would otherwise report zero briefs while actually running one. The phase ladder measures *artifacts*, not sessions: a plan executed in a single session leaves no file behind, so `plan` is the last thing the filesystem can prove, and nothing here claims a session is alive. `brief_quality` is read out of the brief's frontmatter and an absent field reads `-`, never `complete`; a research directory that exists and holds nothing reads `0`, distinct from the `-` that means no research step was ever started. The table carries a matching `VOY` column beside `ORDRE` and `FLY` — the same class of durable filesystem fact, and never summed with them.
**`board.sh --plan` renders the day plan**, a *third* rendering of that same scan and the only one that takes a position: which repos to open a tab for today, in what order, and the command to start each. The order is the position, and there is no cutoff — nothing is hidden, and one deterministic score decides it: `40 ×` repos released transitively, `15 ×` unhandled inbox messages, plus small bonuses for live work and for a cheap `next-cost` row. Four hard buckets preceded it and could not express "this repo owes one message and releases two others" — which is how a blocked chain's root ended up ranked *below* the repos waiting on it. **Chain-root credit** follows `blocked-on` transitively to the first repo that is not itself blocked and credits only that root: opening a blocked repo releases nobody, since its own next step is by definition waiting. A cycle, or a `blocked-on` naming a repo the scan never produced, credits nobody rather than inventing a root — a plan that looks correct while sending you to the wrong repo is worse than one that says nothing. Repos owing mail still rank high *whatever their status*, and debt is deliberately **uncapped**: excluding `blocked` or `done` is a statement about a repo's own next step, which cannot be moved, while owing an answer is a different axis and answering is often what unblocks it. Repos with no board line come last and labelled — the table already prints a note about those, so a plan that dropped them silently would repeat exactly that defect. `why=` names the dominant term, so a block reads `unblocks:2` rather than the `inbox:N` every block used to repeat. Still zero model calls, still read-only, and still cross-checked against `coord-count.sh`.
**`--focus "<prose>"` narrows that plan to one subject, and reports what it held back.** It is the only cutoff this format has, which is why the report is a condition of the feature rather than a refinement of it: `--plan` documents that it hides nothing and labels what it cannot rank, so a silent filter would break a property already written down. The same run prints the subjects the prose resolved to, how many blocks were removed, how many `STATE.md` were searched, and — named, not counted — the repos that *mention* a resolved subject without declaring a marker line. That last class is enumerated because it is where the misses live: a repo can be a heavy participant and never have written a marker, and no string measurement finds it until the held-back population is listed. Each surviving block carries the declaration it survived on. Prose matching nothing prints the *full* plan and says so, since the phrase arrives verbatim from a person and a typo must not empty the day. The subject vocabulary is read from the scanned `STATE.md` themselves, so the "reads `STATE.md` and no other file" invariant is untouched.
@ -180,7 +183,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Development
bash scripts/coord-selftest.sh # 220 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 237 checks against a throwaway repo tree
bash scripts/board-selftest.sh # 360 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 69 checks, incl. the route->board round trip
bash scripts/orders-selftest.sh # 99 checks, incl. the 20-way barriered claim race
bash scripts/state-line-guard-selftest.sh # 40 checks, incl. the Edit replace_all projection and the ratchet

267
scripts/board-selftest.sh Executable file → Normal file
View file

@ -2673,6 +2673,273 @@ check "orderage: order age is display-only - it does not reorder the board" $?
/bin/rm -rf "$AGROOT" "$AGCOORD" 2>/dev/null
# --- 29. VOYAGE: briefs in flight (ORDRE 20260831T135934Z-696228720) --------
# board read STATE lines and knew nothing about Voyage briefs, so a program
# running Voyage in several repos had no shared surface: nobody could answer
# "which briefs are in flight, in what phase, and who is waiting on whom".
#
# The fields below are the ones that carried information in the ONE completed
# run (llm-ingestion-okf, .claude/projects/2026-08-30-door-b-concept-granularity,
# brief.md.draft 30.08 -> brief.md -> research/ 31.08 14:19-14:54 -> plan.md
# 31.08 15:30), measured against the files themselves rather than inherited
# from the order.
#
# DETECTION IS BY PROPERTY, NEVER BY DIRECTORY NAME: a directory holding
# brief.md or brief.md.draft, under any of the three locations the global
# CLAUDE.md recognises (.claude/projects/, docs/, features/<n>-<name>/).
# It walks the FILESYSTEM, never `git ls-files`: llm-ingestion-okf gitignores
# .claude/projects/ (commit 087be0b), so an index-based detector would see
# ZERO briefs in the one repo actually running one. Pinned below with a
# known-positive control that asserts git really does ignore the fixture.
VROOT="$(mktemp -d)"
VCOORD="$(mktemp -d)"
V_OLD10="$(date -v-10d +%Y%m%d%H%M)"
V_OLD15="$(date -v-15d +%Y%m%d%H%M)"
V_OLD20="$(date -v-20d +%Y%m%d%H%M)"
mk_v_repo() {
mkrepo "$VROOT/$1"
{
echo "# STATE - $1"
echo ""
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=sonnet/xhigh -->"
echo "Et steg."
} > "$VROOT/$1/STATE.md"
}
# A brief with real frontmatter. $1=path $2=brief_quality ("-" for no field)
mk_v_brief() {
{
echo "---"
echo "type: trekbrief"
echo "created: 2026-08-30"
[ "$2" = "-" ] || echo "brief_quality: $2"
echo "---"
echo ""
echo "# Task: a thing"
} > "$1"
}
# vy-full: the shape of the completed run - brief + research + plan, one
# BLOCKING DECISION declared and referenced three more times in prose.
mk_v_repo vy-full
mkdir -p "$VROOT/vy-full/.claude/projects/2026-08-30-full/research"
mk_v_brief "$VROOT/vy-full/.claude/projects/2026-08-30-full/brief.md" complete
{
echo "## Open Questions"
echo ""
echo "- **[BLOCKING DECISION, before S4] Which form carries identity?**"
echo "- **S4b, written per branch, because the BLOCKING DECISION is open.**"
echo 'open\":** the \`[BLOCKING DECISION, before S4]\` form is an open question.'
echo "# ([BLOCKING DECISION, before S4] above). Research does not depend on it."
} >> "$VROOT/vy-full/.claude/projects/2026-08-30-full/brief.md"
echo "# Research" > "$VROOT/vy-full/.claude/projects/2026-08-30-full/research/README.md"
echo "# T1" > "$VROOT/vy-full/.claude/projects/2026-08-30-full/research/topic-1.md"
echo "# T2" > "$VROOT/vy-full/.claude/projects/2026-08-30-full/research/topic-2.md"
echo "# Plan" > "$VROOT/vy-full/.claude/projects/2026-08-30-full/plan.md"
find "$VROOT/vy-full/.claude/projects/2026-08-30-full" -type f -exec touch -t "$V_OLD10" {} \;
# vy-draft: /trekbrief ran, the review gate never cleared. brief.md.draft and
# no brief.md is the state the completed run sat in for a full day, and it is
# the one phase no STATE.md line can express.
mk_v_repo vy-draft
mkdir -p "$VROOT/vy-draft/.claude/projects/2026-08-01-draft"
echo "# draft" > "$VROOT/vy-draft/.claude/projects/2026-08-01-draft/brief.md.draft"
# vy-partial: docs/ location, brief_quality partial. Three presence-greps
# cannot tell this from vy-full's `complete` - that is the whole reason the
# field is read out of the frontmatter and not inferred from the file list.
mk_v_repo vy-partial
mkdir -p "$VROOT/vy-partial/docs/some-plan"
mk_v_brief "$VROOT/vy-partial/docs/some-plan/brief.md" partial
# vy-feature: features/<n>-<name>/ location, and a research directory that
# exists holding NOTHING. A research step with null output is ansikt 4 at
# programme level, so the count is reported rather than the directory's
# existence being taken as delivery.
mk_v_repo vy-feature
mkdir -p "$VROOT/vy-feature/features/01-thing/research"
mk_v_brief "$VROOT/vy-feature/features/01-thing/brief.md" -
# vy-review: the far end of the ladder.
mk_v_repo vy-review
mkdir -p "$VROOT/vy-review/.claude/projects/2026-07-01-rev"
mk_v_brief "$VROOT/vy-review/.claude/projects/2026-07-01-rev/brief.md" complete
echo "# Plan" > "$VROOT/vy-review/.claude/projects/2026-07-01-rev/plan.md"
echo "# Review" > "$VROOT/vy-review/.claude/projects/2026-07-01-rev/review.md"
# vy-prose: the known-NEGATIVE control for the blocking-decision count. The
# string appears, but only mid-line in prose - never as a declaration. A
# detector that greps for the bare string reads 2 here and is wrong.
mk_v_repo vy-prose
mkdir -p "$VROOT/vy-prose/.claude/projects/2026-07-02-prose"
mk_v_brief "$VROOT/vy-prose/.claude/projects/2026-07-02-prose/brief.md" complete
{
echo 'The plan stays gated on the \`[BLOCKING DECISION, before S4]\` in the brief.'
echo "# ([BLOCKING DECISION, before S9] above). Nothing is declared here."
} >> "$VROOT/vy-prose/.claude/projects/2026-07-02-prose/brief.md"
# vy-ignored: .claude/projects/ is gitignored, exactly as llm-ingestion-okf
# has it. Detection must still find the project.
mk_v_repo vy-ignored
echo ".claude/projects/" > "$VROOT/vy-ignored/.gitignore"
mkdir -p "$VROOT/vy-ignored/.claude/projects/2026-08-29-hidden"
mk_v_brief "$VROOT/vy-ignored/.claude/projects/2026-08-29-hidden/brief.md" complete
# vy-multi: two projects, one fresh and one stale. The repo's VOY age is the
# STALEST project's.
mk_v_repo vy-multi
mkdir -p "$VROOT/vy-multi/.claude/projects/2026-08-31-fresh"
mk_v_brief "$VROOT/vy-multi/.claude/projects/2026-08-31-fresh/brief.md" complete
mkdir -p "$VROOT/vy-multi/.claude/projects/2026-08-16-stale"
mk_v_brief "$VROOT/vy-multi/.claude/projects/2026-08-16-stale/brief.md" complete
touch -t "$V_OLD15" "$VROOT/vy-multi/.claude/projects/2026-08-16-stale/brief.md"
# vy-newest: one 20-day-old artifact and one fresh one in the SAME project.
# The age is the NEWEST artifact - "nothing has happened here for N days" -
# which INVERTS the oldest-wins rule ORDRE and FLY carry, deliberately: an
# order queue's problem is the oldest item still waiting, a project's problem
# is that its most recent activity is old. Flipping this to oldest reads 20d.
mk_v_repo vy-newest
mkdir -p "$VROOT/vy-newest/.claude/projects/2026-08-11-newest"
mk_v_brief "$VROOT/vy-newest/.claude/projects/2026-08-11-newest/brief.md" complete
touch -t "$V_OLD20" "$VROOT/vy-newest/.claude/projects/2026-08-11-newest/brief.md"
echo "# Plan" > "$VROOT/vy-newest/.claude/projects/2026-08-11-newest/plan.md"
# vy-none: no Voyage project anywhere. VOY must read a bare 0.
mk_v_repo vy-none
mkdir -p "$VROOT/vy-none/docs"
echo "# just docs" > "$VROOT/vy-none/docs/notes.md"
# A pending order in vy-full, for the brief-phase <-> order-id coupling.
mkdir -p "$VCOORD/vy-full/orders"
printf -- '---\nfrom: x\nto: vy-full\norder-id: 20260831T120000Z-1111\n---\nDo a thing.\n' \
> "$VCOORD/vy-full/orders/20260831T120000Z-1111.md"
# --- Ground truth FIRST. Section 11 of state-line-guard-selftest.sh went
# vacuously green because its fixture was never created; assert the fixture
# really is what the checks below depend on before depending on it.
[ -f "$VROOT/vy-full/.claude/projects/2026-08-30-full/plan.md" ] \
&& [ -f "$VROOT/vy-draft/.claude/projects/2026-08-01-draft/brief.md.draft" ] \
&& [ ! -f "$VROOT/vy-draft/.claude/projects/2026-08-01-draft/brief.md" ]
check "voyage: GROUND TRUTH - the fixture tree really holds plan.md and a bare brief.md.draft" $?
git -C "$VROOT/vy-ignored" check-ignore -q ".claude/projects/2026-08-29-hidden/brief.md"
check "voyage: GROUND TRUTH - git really does ignore vy-ignored's brief (the index would see zero)" $?
vage="$(( ( $(date +%s) - $(stat -f %m "$VROOT/vy-newest/.claude/projects/2026-08-11-newest/brief.md") ) / 86400 ))"
[ "$vage" -ge 19 ] && [ "$vage" -le 21 ]
check "voyage: GROUND TRUTH - the stale artifact really is ~20 days old (touch -t worked)" $?
VOUT="$(CLAUDE_COORD_DIR="$VCOORD" "$BOARD" --roots "$VROOT" --voyage 2>/dev/null)"
VTAB="$(CLAUDE_COORD_DIR="$VCOORD" "$BOARD" --roots "$VROOT" 2>/dev/null)"
v_block() { printf '%s' "$VOUT" | awk -v p="prosjekt=$1" '$0==p{f=1;next} /^prosjekt=/{f=0} f'; }
# --- Detection reaches all three recognised locations -----------------------
printf '%s' "$VOUT" | grep -q '^prosjekt=vy-full/\.claude/projects/2026-08-30-full$'
check "voyage: detects a project under .claude/projects/" $?
printf '%s' "$VOUT" | grep -q '^prosjekt=vy-partial/docs/some-plan$'
check "voyage: detects a project under docs/" $?
printf '%s' "$VOUT" | grep -q '^prosjekt=vy-feature/features/01-thing$'
check "voyage: detects a project under features/<n>-<name>/" $?
printf '%s' "$VOUT" | grep -q '^prosjekt=vy-ignored/\.claude/projects/2026-08-29-hidden$'
check "voyage: a GITIGNORED project is still found (filesystem, never git ls-files)" $?
# Known-positive control for the negative: a repo with no brief must not be
# invented into the listing.
printf '%s' "$VOUT" | grep -q '^prosjekt=vy-none/' && rc=1 || rc=0
check "voyage: a repo with no brief produces no project block (control)" $rc
# --- Phase ladder -----------------------------------------------------------
v_block "vy-draft/.claude/projects/2026-08-01-draft" | grep -q '^fase=brief-draft$'
check "voyage: brief.md.draft with no brief.md reads fase=brief-draft" $?
v_block "vy-partial/docs/some-plan" | grep -q '^fase=brief$'
check "voyage: brief.md with no research and no plan reads fase=brief" $?
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^fase=plan$'
check "voyage: plan.md present reads fase=plan" $?
v_block "vy-review/.claude/projects/2026-07-01-rev" | grep -q '^fase=review$'
check "voyage: review.md present reads fase=review" $?
# --- brief_quality: read from the FRONTMATTER, absent is never `complete` ---
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^kvalitet=complete$'
check "voyage: brief_quality=complete is read from the frontmatter" $?
v_block "vy-partial/docs/some-plan" | grep -q '^kvalitet=partial$'
check "voyage: brief_quality=partial is distinguished from complete" $?
v_block "vy-feature/features/01-thing" | grep -q '^kvalitet=-$'
check "voyage: a brief with NO brief_quality field reads '-', never 'complete'" $?
# --- Blocking decisions: declarations, not mentions --------------------------
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^blokkerende=1$'
check "voyage: one declared BLOCKING DECISION plus three prose references counts 1" $?
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^blokkerende_gate=S4$'
check "voyage: the blocking decision's gate (before S4) is reported" $?
v_block "vy-prose/.claude/projects/2026-07-02-prose" | grep -q '^blokkerende=0$'
check "voyage: KNOWN-NEGATIVE - prose mentions alone count 0, not 2" $?
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^venter=operatoerbeslutning$'
check "voyage: a project with an open blocking decision waits on the OPERATOR" $?
v_block "vy-prose/.claude/projects/2026-07-02-prose" | grep -q '^venter=-$'
check "voyage: a project with no blocking decision waits on nobody (control)" $?
# --- Research denominator ---------------------------------------------------
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^research=3$'
check "voyage: a delivered research step reports its file count" $?
v_block "vy-feature/features/01-thing" | grep -q '^research=0$'
check "voyage: a research directory holding NOTHING reports 0, not absence" $?
v_block "vy-partial/docs/some-plan" | grep -q '^research=-$'
check "voyage: no research directory at all reports '-', distinct from 0" $?
# --- Order coupling (field 4) ------------------------------------------------
v_block "vy-full/.claude/projects/2026-08-30-full" | grep -q '^ordre_id=20260831T120000Z-1111$'
check "voyage: the repo's pending order id is carried beside the brief phase" $?
v_block "vy-partial/docs/some-plan" | grep -q '^ordre=0$'
check "voyage: a repo with no pending order reports ordre=0 (control)" $?
# --- Denominator: never a silent null ---------------------------------------
printf '%s' "$VOUT" | grep -qE '^# nevner: [0-9]+ repo skannet, [0-9]+ med minst ett Voyage-prosjekt, [0-9]+ prosjekter$'
check "voyage: the listing reports its own denominator" $?
VEMPTY="$(mktemp -d)"; mkrepo "$VEMPTY/plain"
VNONE="$("$BOARD" --roots "$VEMPTY" --voyage 2>/dev/null)"; rc=$?
[ "$rc" -eq 0 ]
check "voyage: a tree with no Voyage project at all still exits 0" $?
printf '%s' "$VNONE" | grep -q '^ingen=0 Voyage-prosjekter funnet'
check "voyage: no projects found SAYS so - it never renders as an empty page" $?
/bin/rm -rf "$VEMPTY" 2>/dev/null
# --- The legend is engine behaviour, exactly as FLY's liveness denial is -----
printf '%s' "$VOUT" | grep -qi 'artefakter, ikke sesjoner'
check "voyage: the legend states the phase measures ARTIFACTS, not sessions" $?
# --- The VOY table column ----------------------------------------------------
printf '%s' "$VTAB" | grep -q 'VOY'
check "voyage: the table carries a VOY column" $?
[ "$(printf '%s' "$VTAB" | awk '$1=="vy-none"{print $7}')" = "0" ]
check "voyage: a repo with no project prints a bare 0, never '0:0d'" $?
[ "$(printf '%s' "$VTAB" | awk '$1=="vy-multi"{print $7}')" = "2:15d" ]
check "voyage: VOY carries count and the STALEST project's age" $?
[ "$(printf '%s' "$VTAB" | awk '$1=="vy-newest"{print $7}')" = "1:0d" ]
check "voyage: the age is the NEWEST artifact in a project, not the oldest" $?
# --- Display only: VOY must not reorder the board ---------------------------
# vy-multi and vy-newest are both in-progress with identical STATE mtimes, so
# ordering stays alphabetical. Putting the age in the ranking would silently
# reorder a parser living in another repo (board.sh:87-116).
v_first="$(printf '%s' "$VTAB" | grep -E '^vy-(multi|newest) ' | head -1 | awk '{print $1}')"
[ "$v_first" = "vy-multi" ]
check "voyage: VOY is display-only - it does not reorder the board" $?
# --- board stays READ-ONLY ---------------------------------------------------
vsum_before="$(find "$VROOT" -type f | sort | wc -l)"
CLAUDE_COORD_DIR="$VCOORD" "$BOARD" --roots "$VROOT" --voyage >/dev/null 2>&1
CLAUDE_COORD_DIR="$VCOORD" "$BOARD" --roots "$VROOT" >/dev/null 2>&1
vsum_after="$(find "$VROOT" -type f | sort | wc -l)"
[ "$vsum_before" = "$vsum_after" ]
check "voyage: board wrote nothing to the scanned tree (read-only preserved)" $?
/bin/rm -rf "$VROOT" "$VCOORD" 2>/dev/null
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1

View file

@ -207,7 +207,7 @@
# see the dispatch() comment for what happens when it is guessed.
#
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain]
# [--brief|--plan|--inbox-plan|--dispatch ...]
# [--brief|--plan|--inbox-plan|--dispatch|--voyage ...]
# [--focus "<prose>"]
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# BOARD_ROOTS overrides the default scan roots.
@ -223,6 +223,7 @@ PLAN=0
INBOX_PLAN=0
FOCUS=""
DISPATCH=0
VOYAGE=0
D_REPO=""; D_PROMPT=""; D_PANE=""; D_ORDER=""
D_PATH=""; D_VER=""; D_REV=""; D_SCOPE=""; D_RAT=""
@ -238,9 +239,9 @@ while [ $# -gt 0 ]; do
--roots) [ $# -ge 2 ] || { echo "board: --roots requires a value" >&2; exit 2; }
ROOTS="$2"; shift 2 ;;
# Four renderings of one scan, so exactly one may be selected: last wins.
--brief) BRIEF=1; PLAN=0; INBOX_PLAN=0; shift ;;
--plan) PLAN=1; BRIEF=0; INBOX_PLAN=0; shift ;;
--inbox-plan) INBOX_PLAN=1; BRIEF=0; PLAN=0; shift ;;
--brief) BRIEF=1; PLAN=0; INBOX_PLAN=0; VOYAGE=0; shift ;;
--plan) PLAN=1; BRIEF=0; INBOX_PLAN=0; VOYAGE=0; shift ;;
--inbox-plan) INBOX_PLAN=1; BRIEF=0; PLAN=0; VOYAGE=0; shift ;;
# Raw operator prose, forwarded verbatim by the driver: it does not
# tokenize, match or normalize, so every bit of that work is here. Same
# `shift 2` guard as --roots, for the same bash 3.2 reason.
@ -248,7 +249,8 @@ while [ $# -gt 0 ]; do
FOCUS="$2"; shift 2 ;;
# Dispatch rendering: one repo, one prompt file, one startup command. Same
# `shift 2` guard as every other value flag, for the same bash 3.2 reason.
--dispatch) DISPATCH=1; BRIEF=0; PLAN=0; INBOX_PLAN=0; shift ;;
--dispatch) DISPATCH=1; BRIEF=0; PLAN=0; INBOX_PLAN=0; VOYAGE=0; shift ;;
--voyage) VOYAGE=1; BRIEF=0; PLAN=0; INBOX_PLAN=0; DISPATCH=0; shift ;;
--repo) [ $# -ge 2 ] || { echo "board: --repo requires a value" >&2; exit 2; }
D_REPO="$2"; shift 2 ;;
--prompt-file) [ $# -ge 2 ] || { echo "board: --prompt-file requires a value" >&2; exit 2; }
@ -403,6 +405,169 @@ oldest_order_age() {
printf '%dd' $(( (NOW - ooa_oldest) / 86400 ))
}
# --- Voyage project detection (ORDRE 20260831T135934Z-696228720) ------------
# board read STATE lines and knew nothing about Voyage briefs, so a programme
# running Voyage across several repos had no shared surface: nobody could
# answer "which briefs are in flight, in what phase, and who waits on whom".
#
# Detection is by PROPERTY, never by directory name. The global CLAUDE.md says
# plans and briefs are defined by properties and recognises three locations
# (docs/, .claude/projects/, features/<n>-<name>/), so a directory counts as a
# Voyage project iff it HOLDS brief.md or brief.md.draft under one of them.
#
# It walks the FILESYSTEM and never `git ls-files`. Measured: llm-ingestion-okf
# gitignores .claude/projects/ (commit 087be0b, "keep .claude/projects/
# local-only"), so an index-based detector would report ZERO briefs in the one
# repo actually running a Voyage - a broken query returning a positive-looking
# null. Selftest section 29 pins it with a fixture whose ignore status is
# asserted as ground truth first.
voyage_dirs() {
for vd_g in "$1"/.claude/projects/*/ "$1"/docs/*/ "$1"/features/*/; do
[ -d "$vd_g" ] || continue
vd_p="${vd_g%/}"
if [ -f "$vd_p/brief.md" ] || [ -f "$vd_p/brief.md.draft" ]; then
printf '%s\n' "$vd_p"
fi
done
}
# The phase ladder, furthest artifact present wins. This is the one field that
# cannot be derived from a STATE.md at all, and it is the whole reason the row
# exists: brief.md.draft (the review gate has not cleared) -> brief.md ->
# research/*.md -> plan.md -> execute -> review.md.
#
# IT MEASURES ARTIFACTS, NOT SESSIONS, and the bound is stated rather than
# hidden: /trekexecute leaves a file behind only in its MULTI-session form
# (.session-state.local.json), so a plan executed in one session leaves nothing
# and `plan` is the last thing the filesystem can prove. Nothing here says a
# session is alive - the same refusal FLY carries, for the same reason.
voyage_phase() {
[ -f "$1/review.md" ] && { printf 'review'; return 0; }
[ -f "$1/.session-state.local.json" ] && { printf 'execute'; return 0; }
[ -f "$1/plan.md" ] && { printf 'plan'; return 0; }
for vp_f in "$1"/research/*.md; do
[ -f "$vp_f" ] && { printf 'research'; return 0; }
done
[ -f "$1/brief.md" ] && { printf 'brief'; return 0; }
[ -f "$1/brief.md.draft" ] && { printf 'brief-draft'; return 0; }
printf '?'
}
# Days since the NEWEST artifact in the project - "nothing has happened here
# for N days". This INVERTS the oldest-wins rule ORDRE and FLY carry, and the
# inversion is deliberate rather than an oversight: an order queue's problem is
# the oldest item still waiting, while a project's problem is that its most
# recent activity is old. Both answer "how long has this been a problem".
# `?` when a mtime could not be read, reported for the WHOLE reading: an
# unmeasured file could be the newest one, which makes "newest" a claim this
# function is no longer entitled to make.
voyage_age() {
# One `find` and one BATCHED `stat` (-exec ... +), never a stat per file: the
# per-file form forked once per artifact and cost 10s over the real tree,
# which would have made the plain table 4x slower for a column beside it.
va_files="$(find "$1" -type f 2>/dev/null)"
va_n="$(printf '%s\n' "$va_files" | grep -c . | tr -d ' ')"
[ "${va_n:-0}" -gt 0 ] || { printf '?'; return 0; }
va_ms="$(find "$1" -type f -exec stat -f %m {} + 2>/dev/null | grep '^[0-9][0-9]*$')"
va_mn="$(printf '%s\n' "$va_ms" | grep -c . | tr -d ' ')"
# A file counted but not dated means the reading is short by an unknown
# amount, and the missing one could be the newest - so "newest" stops being a
# claim this function is entitled to make. Reported for the WHOLE reading.
[ "${va_mn:-0}" -eq "${va_n:-0}" ] || { printf '?'; return 0; }
va_new="$(printf '%s\n' "$va_ms" | sort -n | tail -1)"
case "$va_new" in ''|*[!0-9]*) printf '?'; return 0 ;; esac
printf '%dd' $(( (NOW - va_new) / 86400 ))
}
# brief_quality out of the FRONTMATTER BLOCK ONLY, never the body - a body line
# is untrusted text and must not be able to speak for the brief, the same rule
# coord-count.sh applies to `reply-expected`. Measured across the real tree
# 2026-08-31: only 8 of ~40 briefs carry the field at all, so ABSENT must read
# `-` and never `complete`. `partial` vs `complete` is precisely what three
# presence-greps could not tell apart (the order's field 2).
voyage_quality() {
vq_f="$1/brief.md"
[ -f "$vq_f" ] || vq_f="$1/brief.md.draft"
[ -f "$vq_f" ] || { printf -- '-'; return 0; }
vq_v="$(awk '
NR==1 { if ($0 ~ /^---[[:space:]]*$/) { inf=1; next } else { exit } }
inf && /^---[[:space:]]*$/ { exit }
inf && /^brief_quality:[[:space:]]*/ {
sub(/^brief_quality:[[:space:]]*/, "")
gsub(/^["'"'"']|["'"'"']$/, "")
gsub(/[[:space:]]+$/, "")
print; exit
}' "$vq_f" 2>/dev/null | head -1 | tr -d '\r')"
[ -n "$vq_v" ] || vq_v="-"
printf '%s' "$vq_v"
}
# Open blocking decisions: `[BLOCKING DECISION, before S<n>]` in the brief.
# Counted at DECLARATION SITES ONLY - the bracket must open the line's content
# after list markers, headings and bold. Measured on the real brief: the string
# occurs on four lines, of which ONE declares and three refer to it in prose,
# so a bare `grep -c` reports 4 where the honest answer is 1. Selftest section
# 29 carries the known-negative control (a brief whose only occurrences are
# prose must count 0).
#
# It reports what the brief DECLARES and infers nothing about resolution: board
# reports and refuses the inference, exactly as FLY does for liveness.
# Prints "<count> <gates>" as two space-separated fields.
voyage_blocking() {
vb_f="$1/brief.md"
[ -f "$vb_f" ] || vb_f="$1/brief.md.draft"
[ -f "$vb_f" ] || { printf '? -'; return 0; }
vb_re='^[[:space:]]*(#+[[:space:]]*)?([-*+][[:space:]]*)?(\*\*)?\[BLOCKING DECISION'
vb_n="$(grep -cE "$vb_re" "$vb_f" 2>/dev/null | tr -d ' ')"
case "$vb_n" in ''|*[!0-9]*) vb_n=0 ;; esac
vb_g="$(grep -E "$vb_re" "$vb_f" 2>/dev/null \
| sed -n 's/.*\[BLOCKING DECISION,[[:space:]]*before[[:space:]]*\([A-Za-z0-9._-]*\)\].*/\1/p' \
| sort -u | paste -sd, - | tr -d ' ')"
[ -n "$vb_g" ] || vb_g="-"
printf '%s %s' "$vb_n" "$vb_g"
}
# The research denominator (the order's field 5). A research step with NULL
# output is ansikt 4 at programme level: research/README.md in the completed
# run opened with "this directory held 0 files when the session started", and
# nothing on any board could show that. Three outcomes, deliberately distinct:
# "-" no research directory at all - the step was never started.
# "0" the directory exists and holds nothing - started, produced nothing.
# "N" measured.
voyage_research() {
[ -d "$1/research" ] || { printf -- '-'; return 0; }
vr_n=0
for vr_f in "$1"/research/*.md; do
[ -f "$vr_f" ] && vr_n=$((vr_n+1))
done
printf '%d' "$vr_n"
}
# The repo-level table cell: count, and the age of the STALEST project - the
# one whose most recent activity is oldest, which is the analogue of ORDRE's
# oldest-first rule. A bare `0` means no projects; "0d" would mean a project
# touched today, and those are two different facts (the same reason FLY exists
# beside ORDRE, and the same reason an empty order queue prints a bare 0).
voyage_cell() {
vc_n=0; vc_max=""; vc_unmeas=0
while IFS= read -r vc_d; do
[ -n "$vc_d" ] || continue
vc_n=$((vc_n+1))
vc_a="$(voyage_age "$vc_d")"
case "$vc_a" in
'?') vc_unmeas=1 ;;
*) vc_a="${vc_a%d}"
if [ -z "$vc_max" ] || [ "$vc_a" -gt "$vc_max" ]; then vc_max="$vc_a"; fi ;;
esac
done <<VOYAGE_CELL_EOF
$(voyage_dirs "$1")
VOYAGE_CELL_EOF
[ "$vc_n" -eq 0 ] && { printf '0'; return 0; }
if [ "$vc_unmeas" -eq 1 ] || [ -z "$vc_max" ]; then printf '%d:?' "$vc_n"; return 0; fi
printf '%d:%dd' "$vc_n" "$vc_max"
}
printf '%s\n' "$REPOS" | while IFS= read -r d; do
[ -n "$d" ] || continue
name="$(basename "$d")"
@ -598,9 +763,13 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
# `orders` sits between blockedon and neste, never after it: `neste` is free
# prose lifted out of a STATE.md and may contain a literal '|', so it has to
# stay the last field on the line.
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s|%s|%s|%s|%s|%s\n' \
# `voy` sits between claimedage and neste, never after it: `neste` is free
# prose lifted out of a STATE.md and may contain a literal '|', so it has to
# stay the last field on the line.
voy="$(voyage_cell "$d")"
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
"$bucket" "$age" "$name" "$disp" "$cost" "$inbox" "$dirty" "$age" "$lastcol" "$d" \
"${blockedon:--}" "$orders" "$ordersage" "$claimed" "$claimedage" "$neste"
"${blockedon:--}" "$orders" "$ordersage" "$claimed" "$claimedage" "$voy" "$neste"
done > "${TMPDIR:-/tmp}/board.$$"
RECORDS="${TMPDIR:-/tmp}/board.$$"
@ -757,20 +926,20 @@ awk -F'|' '$4 ~ /^blocked/ && $11 != "-" && $11 != "" {print $11}' "$RECORDS" \
hdr() {
printf '\n%s\n' "$1"
printf '%-32s %-34s %-14s %4s %7s %7s %4s %6s %6s %s\n' \
"REPO" "STATUS" "KOST" "INN" "ORDRE" "FLY" "DRT" "ALDER" "SISTE" "NESTE"
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %s\n' \
"REPO" "STATUS" "KOST" "INN" "ORDRE" "FLY" "VOY" "DRT" "ALDER" "SISTE" "NESTE"
}
rows() {
awk -F'|' -v b="$1" '$1==b' "$RECORDS" | sort -t'|' -k2,2n | \
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage neste; do
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy neste; do
# `N:age` only when there is something to date. An empty queue prints a
# bare 0, never "0:0d" - absence must not borrow the shape of a
# measurement.
o_cell="$orders"; [ "$ordersage" = "-" ] || o_cell="$orders:$ordersage"
c_cell="$claimed"; [ "$claimedage" = "-" ] || c_cell="$claimed:$claimedage"
printf '%-32s %-34s %-14s %4s %7s %7s %4s %6s %6s %s\n' \
"$name" "$status" "$cost" "$inbox" "$o_cell" "$c_cell" "$dirty" "$age" "$last" "$(trunc "$neste" "$NESTE_WIDTH")"
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %s\n' \
"$name" "$status" "$cost" "$inbox" "$o_cell" "$c_cell" "$voy" "$dirty" "$age" "$last" "$(trunc "$neste" "$NESTE_WIDTH")"
done
}
@ -960,7 +1129,7 @@ brief() {
FILENAME==OWF { ow[$1] = $2 + 0; next }
{ name = $3; owed = (name in ow) ? ow[name] : 0; if (owed > 0) print owed, $0 }
' "$OWED" "$RECORDS" | sort -t'|' -k1,1nr | \
while IFS='|' read -r owed bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage neste; do
while IFS='|' read -r owed bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy neste; do
printf ' %-32s INN %-4s %s\n' "$name" "$owed" "$status"
# Wrapped, not cut - the whole line is the point, but a 500-character one
# is unreadable in a file nobody watched being written. Locale is set for
@ -1564,7 +1733,7 @@ plan() {
echo ""
pn=0
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage neste; do
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy neste; do
[ -n "$name" ] || continue
pn=$((pn + 1))
printf 'tab=%s\n' "$pn"
@ -1624,7 +1793,7 @@ inbox_plan() {
FILENAME==RCF {
name = $3
rc_bucket[name] = $1; rc_status[name] = $4; rc_cost[name] = $5
rc_dir[name] = $10; rc_neste[name] = $16
rc_dir[name] = $10; rc_neste[name] = $17
next
}
{
@ -1691,6 +1860,105 @@ if [ "$INBOX_PLAN" -eq 1 ]; then
exit 0
fi
# --- Voyage rendering (--voyage) -------------------------------------------
# A SIXTH rendering of the same scan, never a second scan, and read-only like
# every other one: it writes to no repo, no STATE.md and no mailbox.
#
# It answers the question no single STATE.md can - which briefs are in flight,
# in what phase, and who is waiting on whom - as key=value blocks, the format
# this file already uses for its machine-readable renderings. Prose would make
# the format an API no test in THIS repo could hold stable for a consumer in
# another.
#
# "Who waits on whom" has exactly two answerable forms here, and neither is
# guessed: an OPEN BLOCKING DECISION means the project waits on the operator
# (measured on the completed run - one decision gated /trekplan for a full day
# and nothing on any board showed it), and the repo's PENDING ORDER IDS are
# carried beside the phase, because that coupling is what lets a reader go from
# "this brief is standing still" to "this order is pending".
voyage() {
vy_repos=0; vy_with=0; vy_total=0
echo "VOYAGE - briefer i omloep"
# The legend is engine behaviour, exactly as FLY's liveness denial is: a
# column or a field whose form nothing pins is one that drifts.
echo "# FASE maaler ARTEFAKTER, ikke sesjoner. En plan utfoert i EN sesjon legger"
echo "# ingen fil igjen, saa 'plan' er det siste filsystemet kan bevise. Ingenting"
echo "# her sier at en sesjon lever - samme nekt som FLY baerer."
echo "# KVALITET er brief_quality fra frontmatter. '-' betyr at feltet MANGLER,"
echo "# aldri at briefen er complete. RESEARCH: '-' = ingen katalog, '0' = katalog"
echo "# uten output (et research-steg med null resultat, ansikt 4 paa programnivaa)."
echo "# ALDER = dager siden NYESTE artefakt: hvor lenge ingenting har skjedd. Det er"
echo "# motsatt av ORDRE/FLY med vilje - en ko sitt problem er det eldste som venter,"
echo "# et prosjekt sitt problem er at siste aktivitet er gammel."
echo ""
while IFS='|' read -r vy_name vy_dir; do
[ -n "$vy_name" ] || continue
vy_repos=$((vy_repos+1))
vy_here=0
# Order coupling: the repo's PENDING orders (claimed ones are in flight and
# are not what a reader is looking for when a brief has stopped moving).
vy_ord=0; vy_ids=""
for vy_of in "$COORD/$vy_name/orders"/*.md; do
[ -f "$vy_of" ] || continue
vy_ord=$((vy_ord+1))
vy_ids="$vy_ids$(basename "$vy_of" .md)
"
done
while IFS= read -r vy_p; do
[ -n "$vy_p" ] || continue
vy_here=1; vy_total=$((vy_total+1))
vy_rel="${vy_p#$vy_dir/}"
vy_bl="$(voyage_blocking "$vy_p")"
vy_bn="${vy_bl%% *}"; vy_bg="${vy_bl##* }"
case "$vy_bn" in
0|'?') vy_wait="-" ;;
*) vy_wait="operatoerbeslutning" ;;
esac
printf 'prosjekt=%s/%s\n' "$vy_name" "$vy_rel"
printf 'repo=%s\n' "$vy_name"
printf 'dir=%s\n' "$vy_p"
printf 'fase=%s\n' "$(voyage_phase "$vy_p")"
printf 'kvalitet=%s\n' "$(voyage_quality "$vy_p")"
printf 'blokkerende=%s\n' "$vy_bn"
printf 'blokkerende_gate=%s\n' "$vy_bg"
printf 'venter=%s\n' "$vy_wait"
printf 'research=%s\n' "$(voyage_research "$vy_p")"
printf 'alder=%s\n' "$(voyage_age "$vy_p")"
printf 'ordre=%s\n' "$vy_ord"
printf '%s' "$vy_ids" | while IFS= read -r vy_i; do
[ -n "$vy_i" ] && printf 'ordre_id=%s\n' "$vy_i"
done
echo ""
done <<VOYAGE_PROJ_EOF
$(voyage_dirs "$vy_dir")
VOYAGE_PROJ_EOF
[ "$vy_here" -eq 1 ] && vy_with=$((vy_with+1))
done <<VOYAGE_REPO_EOF
$(awk -F'|' '{print $3 "|" $10}' "$RECORDS")
VOYAGE_REPO_EOF
# The denominator, always - "nothing found" and "not measured" must never
# render as the same empty page. This is the whole ansikt-4 rule applied to
# this renderer's own output.
if [ "$vy_total" -eq 0 ]; then
printf 'ingen=0 Voyage-prosjekter funnet i %s skannede repo\n' "$vy_repos"
printf '# Det er et MAALT null, ikke en tom side: %s repo ble undersoekt paa de\n' "$vy_repos"
echo "# tre anerkjente plasseringene (.claude/projects/, docs/, features/)."
fi
printf '# nevner: %s repo skannet, %s med minst ett Voyage-prosjekt, %s prosjekter\n' \
"$vy_repos" "$vy_with" "$vy_total"
}
if [ "$VOYAGE" -eq 1 ]; then
voyage
exit 0
fi
count() { awk -F'|' -v b="$1" '$1==b' "$RECORDS" | wc -l | tr -d ' '; }
echo "BOARD - tverr-repo oppmerksomhetstavle ($(awk 'END{print NR}' "$RECORDS") repo)"
@ -1712,6 +1980,13 @@ echo "FLY = ordrer i arbeid (claimed). Beviser aldri at en okt lever eller er ak
echo " Beviser bare at noen TOK ordren; ingenting frigir den om okten dor."
echo " Summeres ALDRI med ORDRE - samme ko, annen tilstand."
echo " Baerer samme N:Md - alderen paa den ELDSTE claimen. Ingenting frigir den."
# VOY is the same class of reading as ORDRE and FLY - a durable filesystem fact
# about work sitting in this repo - and like them it is never summed with them.
# The per-project detail (phase, brief_quality, blocking decisions) lives in
# --voyage: a single cell cannot carry a phase when a repo holds seven briefs.
echo "VOY = Voyage-prosjekter (brief i omloep). N:Md = antall og alderen paa det"
echo " STALESTE prosjektet - dager siden NYESTE artefakt der, dvs. hvor lenge"
echo " ingenting har skjedd. Bart 0 = ingen prosjekter. Detaljer: --voyage."
echo "ALDER = dager siden STATE.md endret. SISTE = dager siden siste commit."
[ "$(count 1)" -gt 0 ] && { hdr "BLOKKERT (venter paa ekstern avhengighet)"; rows 1; }

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