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:
parent
0711b4209c
commit
85cd628c3e
5 changed files with 701 additions and 21 deletions
102
CLAUDE.md
102
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
267
scripts/board-selftest.sh
Executable file → Normal 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
|
||||
|
|
|
|||
305
scripts/board.sh
305
scripts/board.sh
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue