Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e5bc4a66e | |||
| 671e275a97 | |||
| a1ef1fb555 | |||
| 9a154950eb | |||
| b57a1ea286 | |||
| cde185979c | |||
| 2f8ceb3f97 | |||
| 5316688844 | |||
| a2019d44f7 | |||
| 39cb6538e3 | |||
| 2bec7fe2fb | |||
| 85cd628c3e |
19 changed files with 3061 additions and 331 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.31.0",
|
||||
"version": "0.34.0",
|
||||
"description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.",
|
||||
"author": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
|
|
|
|||
317
CHANGELOG.md
317
CHANGELOG.md
|
|
@ -5,6 +5,323 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.34.0] - 2026-09-12
|
||||
|
||||
### Changed
|
||||
|
||||
- **The advisor rule is struck from the rubric: `route.sh` and `board.sh
|
||||
--dispatch` no longer emit `--advisor` at all.** It fired on two needs -
|
||||
always on the Sonnet rows, and on the Opus rows at `costly|one-way` stakes -
|
||||
and it read well. What killed it was a measurement, not a change of taste:
|
||||
of 54 dispatches the PM issued 08.-12.09, **zero** carried the flag, because
|
||||
sessions are started by hand from the model and effort rather than from the
|
||||
whole emitted line. A rule nothing honours is not a policy, and an emitted
|
||||
value nobody acts on is decoration in a field whose only job is to be
|
||||
evidence. The advisor is now what it already was in practice: an operator
|
||||
decision per session, said in one sentence in `route.sh --help`. The
|
||||
direction is the safe one for the single thing the old rule protected - the
|
||||
quota fallback is still exactly one row cheaper, it just no longer implies a
|
||||
capability lift nobody was taking.
|
||||
- **The comments that rested on it were rewritten, not left standing.**
|
||||
`board.sh --dispatch` still refuses a `--model`/`--effort` pair, but the
|
||||
reason is no longer "the advisor is a property of the ROW": it is that the
|
||||
rubric has exactly one copy, and a dispatch taking the model directly would
|
||||
be a second, unscored way to reach the same decision - one recording no
|
||||
traits, no rationale and no `next-cost`, so nothing afterwards could say
|
||||
whether the routing or the scoring was wrong. A comment defending a
|
||||
mechanism that has been removed is how the next session restores it.
|
||||
`skills/route/SKILL.md` and `skills/dispatch/SKILL.md` carry the same
|
||||
correction.
|
||||
- Pinned as an ABSENCE over the whole trait space rather than on sampled rows -
|
||||
81 trait combinations, every line of output, with a known-positive control
|
||||
proving the sweep's grep can find a planted advisor - plus `--dispatch` at
|
||||
`reversibility=costly`, the exact input a reintroduced rule would fire on.
|
||||
Backward compatibility is pinned too: a route line carrying a legacy
|
||||
`advisor=` field still parses and still yields a command (measured the same
|
||||
day: 0 of 48 `<!-- route:` lines anywhere in `~/repos` carry one, so nothing
|
||||
in circulation depends on it - but a reader that broke on an unknown field
|
||||
would turn last month's STATE.md into "that repo has no route line").
|
||||
|
||||
### Added
|
||||
|
||||
- **`board.sh --row <repo>`: the table's row for one repo, as `field=value`
|
||||
lines.** A seventh rendering of the same scan, never a second scan, and
|
||||
read-only like every other one. The motivation is a measured misreading: on
|
||||
11.09 the PM read the FLY column off the table **by eye** and got it wrong.
|
||||
Every other rendering a program consumes (`--plan`, `--inbox-plan`,
|
||||
`--dispatch`) is already `key=value`; the default view was the one left out,
|
||||
and a 34-character repo key already shifts a row two characters right, which
|
||||
makes counting spaces worse than it looks. `inn`, `ordre` and `fly` are three
|
||||
separate fields because they are three separate facts, and confusing them is
|
||||
what this exists to close. `status` is the bare token, never the table's
|
||||
`blocked>target` display, with `blocked-on` beside it; `neste` is last and
|
||||
uncut, for the same reason it is last in the record. An unknown repo exits
|
||||
**2** and writes nothing to stdout - an empty block would read as a repo whose
|
||||
every column is blank, which is a real and different state.
|
||||
- `upushet=` is the one field that is **not** a rendering of the scan, and it is
|
||||
named rather than blended in: nothing in the scan measures it, so it is read
|
||||
once, for the named repo only, and never enters the table, the plan or the
|
||||
briefing. It reads the remote-TRACKING ref, not the remote, so `upushet=N`
|
||||
honestly means "the local ref says N"; a repo with no upstream reports `?`,
|
||||
never `0`, because "nothing is waiting to be pushed" and "nobody looked" are
|
||||
exactly the pair this engine refuses to render alike.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **README's selftest numbers were re-measured, not re-derived.** The badge said
|
||||
`selftest_checks-868` and the five `## Development` comments said
|
||||
220/360/73/99/40 = 792 - two different wrong sums on the same public surface,
|
||||
neither matching the other. The five suites were run under `/bin/bash` (3.2)
|
||||
on `b57a1ea` and reported `coord 257`, `board 393`, `route 73`, `orders 116`,
|
||||
`state-line-guard 54` = **893**, 0 failed in every summary. 0.33.0's entry
|
||||
below records 257+368+73+116+54 = 868 and was true when written; 0.33.1 added
|
||||
25 board checks (368 -> 393) without re-summing, and the comment block had been
|
||||
stale far longer. No code, no version bump: the number furthest from the meter
|
||||
rots first, and only the number moved.
|
||||
|
||||
## [0.33.1] - 2026-09-04
|
||||
|
||||
### Added
|
||||
|
||||
- **A git repo nested under a depth-1 repo now enters the board, on a STATE.md
|
||||
and nothing wider.** Discovery adds a depth-1 repo and stops, and the
|
||||
else-branch container scan - the only place children are ever looked at - is
|
||||
unreachable for an entry that is itself a repo. Measured before anything was
|
||||
written: **12 nested repos across the real tree, exactly 1 with a STATE.md**
|
||||
(`from-ai-to-chitta/content-sadhguru`, which had been running work and
|
||||
reporting to nobody). The other 11 are vendored or experimental checkouts and
|
||||
stay invisible on purpose - they do not even reach the `UTEN STATE.md` bucket.
|
||||
`add_nested_repos()` sits beside `add_dot_repos()` rather than widening the
|
||||
`*` loops, for the same reason ordre `20260818T124828Z` gave for dot repos:
|
||||
routing a depth-1 repo into the container branch would admit every nested
|
||||
checkout. One level only; depth 3 is pinned as NOT admitted. A dot-prefixed
|
||||
depth-1 repo gets the same nested scan, since nothing in the criterion
|
||||
distinguishes it.
|
||||
|
||||
A nested repo carries **two names**, and conflating them would have put a
|
||||
fabricated 0 in three columns. The board KEY is `<parent>/<child>`; the
|
||||
MAILBOX name is `basename(git toplevel)`, so `$COORD/<parent>/<child>/inbox`
|
||||
finds no directory and INN/ORDRE/FLY would read 0 for a repo that may have
|
||||
mail. The record loop carries `mbox` beside `name`, and `--voyage`'s order
|
||||
lookup takes `basename($vy_dir)`. Which directories are nested is RECORDED by
|
||||
discovery (`NESTED_LIST`), never re-derived from "is my parent a repo?".
|
||||
|
||||
- **The scan reports its own denominator.** `undersoekt: N katalog(er) depth 1,
|
||||
M polyrepo-container(e), K nestede repo (J med STATE.md tatt med).` The header
|
||||
count answered how many repos were found and nothing about how many were
|
||||
looked at, so a criterion excluding 11 of 12 nested repos was invisible on the
|
||||
surface built to show it. Real tree 2026-09-04: `43 / 5 / 12 (1 tatt med)`. A
|
||||
non-repo dot-directory counts in N and never in M - the line is a denominator,
|
||||
not a partition.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The denominator line's own wording broke an existing check: `polyrepo
|
||||
container itself is not listed as a repo` grepped the whole output for
|
||||
`polyrepo` and matched the new footer's `polyrepo-container(e)` - the same
|
||||
class as a grep reading a comment that EXPLAINS a pattern as an instance of
|
||||
it. Now anchored at column 1, which is what it always meant.
|
||||
|
||||
### Known gaps, stated rather than closed
|
||||
|
||||
- The mailbox-keyed JOINs (`$OWED`, `brief_orphans`, `--inbox-plan`) still key
|
||||
on the board name, so a nested repo owing a reply gets no debt tab and is
|
||||
listed under `UTENFOR REPO-SKANNEN`. Neither created nor worsened here: before
|
||||
this change the repo was absent from `RECORDS` entirely, so both readings were
|
||||
already exactly as wrong.
|
||||
- A 34-character key overflows the table's `%-32s` REPO column, shifting that
|
||||
row two characters right. Same class as `Fable 5.1/xhigh` in KOST; parsing is
|
||||
unaffected, and widening the column moves three `cut -c89-` selftest helpers.
|
||||
- A dot-prefixed NESTED repo is not looked for. The combination was neither
|
||||
measured nor ordered.
|
||||
|
||||
### Testing
|
||||
|
||||
- `board-selftest.sh` 368 -> 393 checks.
|
||||
|
||||
## [0.33.0] - 2026-09-04
|
||||
|
||||
### Added
|
||||
|
||||
- **The FYI sweep is scheduled. Invocation was the gap, not the mechanism.**
|
||||
`coord-sweep.sh` shipped in 0.10.0 and had never run once against the real
|
||||
mailbox. Measured 2026-09-03 with a denominator
|
||||
(`docs/2026-09-03-coordination-debt-measurement.md`): 55 mailbox directories,
|
||||
52 carrying an `inbox/`, 27 pending directed messages - of which **23 were
|
||||
pure notices**, re-injected at every session start in repos nobody had
|
||||
opened. The script was correct and unreachable.
|
||||
|
||||
The order behind this (WP5, `20260902T113745Z-1254925290`) asked for a
|
||||
mechanism and named two candidates; the measurement chose neither, and the
|
||||
first session returned the order saying so. Bulk-ack for pure notices *was
|
||||
already built* - it is this script - so building the second candidate would
|
||||
have been two copies of one policy. The broadcast class converges on its own
|
||||
(reading sets `seen`), and 34 of 263 unread pairs belong to two mailboxes no
|
||||
session can hold, so a TTL would have **closed** those rather than reduced
|
||||
them. The operator then chose the window and authorized the schedule.
|
||||
|
||||
`launchd/com.ktg.repo-mailbox-sweep.plist` runs `--write --days 14` daily at
|
||||
05:30. That is the entire behavioural change; no new mechanism was built.
|
||||
|
||||
- **The 14-day window is written out in the plist, not inherited from the
|
||||
script's default.** It is a policy constant chosen on a measured distribution
|
||||
(30 days would have closed 0 messages, 14 closed 7, 7 would have closed 13),
|
||||
of the same class as the STATE.md line limit. Leaving it implicit would let a
|
||||
later change to `DAYS=14` silently change what an unattended job closes every
|
||||
night across every other mailbox on this machine.
|
||||
|
||||
It runs at 05:30, clear of the 06:00 briefing agent, because the briefing
|
||||
scans the same mailbox this mutates and the two must not overlap. It does
|
||||
**not** change what the briefing reports as debt: since 0.22.0 that figure
|
||||
comes from `coord-count.sh`'s `owed` column, and this sweep closes only
|
||||
messages that owe nothing. What moves is the raw pending count — the table's
|
||||
`INN` column and the volume injected at every session start. Selftest section
|
||||
38 asserts the two agents never share an hour.
|
||||
|
||||
- **coord-selftest.sh section 38 pins the launchd templates (+15 checks, 242 ->
|
||||
257).** A wrong program path in a plist is the one defect here that nothing
|
||||
catches at runtime: the agent loads cleanly and then silently never runs -
|
||||
no output to be wrong, no exit status to read, a failure indistinguishable
|
||||
from a quiet machine. `launchctl list` proves an agent is *loaded*, never
|
||||
that it is *right*.
|
||||
|
||||
The section covers **every** plist in `launchd/`, not just the new one: the
|
||||
plist grammar gets one reader here rather than one per agent, which is the
|
||||
two-copies-of-one-policy defect this repo has named repeatedly.
|
||||
`board-selftest.sh` section 9 still owns `brief-nightly.sh`'s behaviour. Each
|
||||
plist must name a script that exists in this checkout, carry a `Label`
|
||||
matching its filename, keep its `__CHECKOUT__`/`__HOME__` placeholders (the
|
||||
repo is mirrored publicly, and a plist is the one file that would otherwise
|
||||
need an absolute home path), and never point into the version-pinned plugin
|
||||
cache.
|
||||
|
||||
The cache assertion runs on the **extracted program path, never the whole
|
||||
file** - caught by the check itself on its first run: the brief plist's header
|
||||
explains in prose why it does *not* point at the cache, and a file-wide grep
|
||||
read that explanation as the defect it warns about. Same shape as the board
|
||||
line, where prose saying `status=done` must never trigger the done-guard.
|
||||
Four controls are mandatory here and present: the extractor really does read
|
||||
a path, a plist naming a missing script is judged missing, a `Label`
|
||||
disagreeing with its filename is caught, and a program path inside the plugin
|
||||
cache is caught. Mutation-verified against the real file: a one-letter typo
|
||||
(`coord-sweeep.sh`) turns exactly that check red.
|
||||
|
||||
XML well-formedness is deliberately **not** checked. `plutil` is not
|
||||
coreutils, and malformed XML already fails loudly at `launchctl load` - the
|
||||
opposite of the silent failure this section exists for. Both files were
|
||||
linted by hand at 0.33.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The README's selftest-checks badge had read 529 since 0.25.0; the real
|
||||
total is 868.** A stale count in the one place a reader takes as the
|
||||
headline number, corrected while adding to it: 257 + 368 + 73 + 116 + 54.
|
||||
|
||||
## [0.32.1] - 2026-09-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A pending order's age is read from the FILENAME, never the mtime.**
|
||||
`coord-order-done --return` rewrites the order file's mtime, and both age
|
||||
surfaces read mtime, so returning an order reset the very reading that says
|
||||
how long it has waited. An order returned three times could never look old.
|
||||
Found by reading the board immediately after this repo returned an order of
|
||||
its own: a file whose name says 2026-09-02 rendered `ORDRE 1:0d` and
|
||||
`pending, 0d old` minutes later. The filename is written once, at delivery,
|
||||
and nothing rewrites it - which is exactly the fact "how long has this sat
|
||||
with no owner" is asking about.
|
||||
|
||||
Two questions, two sources, and the second one does not move: a CLAIMED
|
||||
order's age is "how long has it been in flight", which is the claim's own
|
||||
mtime and was already right. `board.sh` gains `oldest_pending_age()` beside
|
||||
`oldest_order_age()`; `coord-order-inbox.sh` gains `pending_age_of()` beside
|
||||
`age_of()`. Switching FLY to the filename would answer the delivery question
|
||||
in the column that asks the flight question.
|
||||
|
||||
An unparseable filename yields `?` for the whole reading, never a fabricated
|
||||
`0` - the same fail-safe the mtime path already carried, and for the same
|
||||
reason: an unmeasured order could be the oldest one.
|
||||
|
||||
Pinned by `orders-selftest.sh` section 11 (116 checks, up from 110) and
|
||||
`board-selftest.sh` section 30 (368, up from 360), each with its ground truth
|
||||
asserted before anything depends on it and with known-positive controls that
|
||||
a freshly delivered order still reads `0d` and that FLY did not move.
|
||||
Mutation-verified in both files: restoring the mtime read turns exactly the
|
||||
defect checks red and leaves every control green.
|
||||
|
||||
Section 28's fixtures were rewritten as part of this, not incidentally: they
|
||||
encoded their intended ages in `touch -t` with fixed 2026-01/2026-08
|
||||
filenames, which a filename-based reading makes wrong and time-dependent.
|
||||
They now compute their stems from `date -v`, and the section asserts two
|
||||
ground truths - the filename for ORDRE, the mtime for FLY - because the two
|
||||
columns no longer read the same source.
|
||||
|
||||
## [0.32.0] - 2026-09-01
|
||||
|
||||
Two commits, and the first of them was already written when this release
|
||||
started: `board.sh --voyage` landed on 2026-08-31 and then sat one step past
|
||||
`v0.31.0`, unreleased AND unpushed. Every entry point on this machine runs the
|
||||
version-pinned plugin cache, so the feature existed in git and did not exist
|
||||
for anyone using it. That gap is the reason both changes ship together - they
|
||||
are the same plugin and the same bump.
|
||||
|
||||
### Added
|
||||
|
||||
- **`board.sh --voyage` reports the Voyage briefs in flight** (committed
|
||||
2026-08-31, released here). A sixth rendering of the same read-only scan.
|
||||
`board` reads STATE lines, which say nothing about a brief, so a programme
|
||||
running Voyage across several repos had no shared surface at all: 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 counts
|
||||
only if it holds `brief.md` or `brief.md.draft` under one of the three
|
||||
planning locations the convention recognises - and it walks the FILESYSTEM,
|
||||
never the git index: `llm-ingestion-okf` gitignores `.claude/projects/`, so
|
||||
`git ls-files` would have reported zero briefs in the one repo actually
|
||||
running one. The phase ladder measures ARTIFACTS, not sessions, and says so
|
||||
in its legend; three fields refuse to let an absence borrow the shape of a
|
||||
measurement (`kvalitet=-` is never `complete`, `research=-` is not
|
||||
`research=0`, and the rendering always prints its own denominator). A
|
||||
matching `VOY` column sits beside `ORDRE` and `FLY`, never summed with them.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`route.sh --last-model` now accepts `Fable 5.1`, and the set stays
|
||||
CLOSED.** Fable 5.1 shipped 2026-09-01 and the three-value set refused it, so
|
||||
a session that actually ran it could not record what it ran: the record was
|
||||
either omitted or written as a name already in the set. A lied record reads
|
||||
back months later as a measurement rather than as the gap it is, which is the
|
||||
failure mode of the one field whose entire purpose is to be believed later.
|
||||
|
||||
Two boundary decisions were taken rather than deferred. `Fable 5` is KEPT -
|
||||
not for backward compatibility (measured: exactly 1 of 45 `route-last` lines
|
||||
on this machine carries it) but because `route.sh`'s own row table spells
|
||||
rows 5-6 `Fable 5/high` and `Fable 5/xhigh`, so dropping it would make the
|
||||
script refuse to record a name its own spec writes. And the set was WIDENED
|
||||
rather than replaced by form validation: a `<family> <digits>[.<digits>]`
|
||||
pattern would still catch a misspelled family, but would stop catching a
|
||||
version that does not exist, letting `Fable 5.2` and `Opus 7` read back as
|
||||
evidence that a model ran when it never shipped. The selftest pins that
|
||||
choice - `Fable 5.2` / `Fable 6` / `Fable 5.10` are rejected beside the new
|
||||
accept case - so a later switch to form validation fails there instead of
|
||||
quietly widening what the record can claim. The die message now names the
|
||||
repair, since the cost of a hand-maintained list is exactly what was paid
|
||||
here.
|
||||
|
||||
Not touched: the row table (still six rows, still emitting only 1-4, Fable
|
||||
still a hand-written override) and the note that a Fable session runs without
|
||||
an advisor because the CLI does not enforce it.
|
||||
|
||||
Known gap, stated rather than closed: `Fable 5.1/xhigh` is 15 characters and
|
||||
overflows `board.sh`'s `%-14s` KOST column, shifting the rest of that row one
|
||||
column right. Parsing is unaffected and is now pinned by the round-trip
|
||||
section. Widening the column was not ordered in this session and is reported
|
||||
rather than made.
|
||||
|
||||
`route-selftest.sh`: 69 -> 73 checks. The other four suites are unchanged
|
||||
(coord 242, board 360, orders 110, state-line-guard 54).
|
||||
|
||||
## [0.31.0] - 2026-08-29
|
||||
|
||||
Two commits landed after `v0.30.0` and, as with that release, neither reaches
|
||||
|
|
|
|||
489
CLAUDE.md
489
CLAUDE.md
|
|
@ -16,7 +16,7 @@ marketplace plugin. Three components, one boundary:
|
|||
`coord-done.sh` archives, `coord-count.sh` counts without delivering,
|
||||
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
|
||||
Everything is pinned by `coord-selftest.sh`
|
||||
(242 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||
(257 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||
|
||||
**`ktg-plugin-marketplace` is a RETIRED `--to` address (operator decision
|
||||
2026-08-15), rejected rather than redirected.** It is a polyrepo directory,
|
||||
|
|
@ -94,6 +94,62 @@ marketplace plugin. Three components, one boundary:
|
|||
the class, defaulting to `--write`, or dropping the log each independently
|
||||
turn this from a bounded cleanup into silent data loss.
|
||||
|
||||
**Order 20260902T113745Z-1254925290 (WP5, 2026-09-02, amended by the operator
|
||||
2026-09-03): the sweep was CORRECT AND UNREACHABLE, and the fix is a schedule,
|
||||
not a mechanism.** It shipped in 0.10.0 and had never run once against the
|
||||
real mailbox. Measured 2026-09-03 with a denominator
|
||||
(`docs/2026-09-03-coordination-debt-measurement.md`): 55 mailbox directories,
|
||||
52 with an `inbox/`, 27 pending directed messages, of which **23 were pure
|
||||
notices** being re-injected at every session start in repos nobody had opened.
|
||||
The order asked for a mechanism and named two candidates; the measurement
|
||||
chose neither, and the session returned it saying so. Bulk-ack for pure
|
||||
notices *was already built* - this script - so building the order's candidate
|
||||
B would have been two copies of one policy; and the broadcast class converges
|
||||
on its own (reading sets `seen`), with 34 of 263 unread pairs belonging to two
|
||||
mailboxes no session can hold, so a TTL would have **closed** those, not
|
||||
reduced them. `launchd/com.ktg.repo-mailbox-sweep.plist` is the entire
|
||||
0.33.0 change. **Invocation was the gap.**
|
||||
|
||||
**The 14-day window is written OUT in the plist, never left to the script's
|
||||
default.** It is a policy constant the operator chose on a measured
|
||||
distribution (30d -> 0 messages, 14d -> 7, 7d -> 13), of the same class as the
|
||||
STATE.md line limit - so a later change to `DAYS=14` in the script must not
|
||||
silently change what an unattended nightly job closes across every other
|
||||
mailbox on this machine. (A hard count belongs in the dated measurement doc,
|
||||
not here: it was 52 with an `inbox/` on 2026-09-03 and 53 a day later.)
|
||||
The agent runs at **05:30**, clear of the 06:00 briefing agent, because the
|
||||
briefing scans the same mailbox this mutates; selftest section 38 asserts the
|
||||
two hours differ. It does **not** change the briefing's DEBT figure - since
|
||||
0.22.0 that is read from `coord-count.sh`'s `owed` column, and this sweep
|
||||
closes only messages that owe nothing, so the debt listing is byte-identical
|
||||
before and after. What moves is the raw pending count (the table's `INN`
|
||||
column, `--brief`'s FYI-only naming, and the volume injected at every session
|
||||
start). The first cut of this paragraph claimed the sweep made the briefing
|
||||
report "the debt that remains" - an overclaim, caught in review, and recorded
|
||||
here because it is the kind of plausible sentence that survives a reread.
|
||||
|
||||
**`launchctl list` proves an agent is LOADED, never that it is RIGHT, and a
|
||||
wrong program path is the one defect in this repo that nothing catches at
|
||||
runtime** - the agent simply never runs, in silence, with no output to be
|
||||
wrong and no exit status to read. Two separate things close it, and neither
|
||||
substitutes for the other: `launchctl start <label>` plus a line appearing in
|
||||
the launchd log is the only *runtime* proof the path resolves, and selftest
|
||||
**section 38** asserts *statically*, for every plist in `launchd/`, that the
|
||||
path names a file that exists here, that `Label` matches the filename, that
|
||||
the `__CHECKOUT__`/`__HOME__` placeholders survive (the repo is mirrored
|
||||
publicly), and that no agent points into the version-pinned plugin cache.
|
||||
Section 38 covers **every** plist rather than only the new one, deliberately:
|
||||
the plist grammar gets one reader here instead of one per agent, and
|
||||
`board-selftest.sh` section 9 keeps owning `brief-nightly.sh`'s *behaviour*.
|
||||
The cache assertion runs on the **extracted path, never the whole file** - the
|
||||
brief plist's header explains in prose why it does not point at the cache, and
|
||||
a file-wide grep read that explanation as the defect it warns about, exactly
|
||||
as prose saying `status=done` must never trigger the board's done-guard.
|
||||
XML well-formedness is deliberately NOT checked: `plutil` is not coreutils,
|
||||
and malformed XML already fails loudly at `launchctl load`, which is the
|
||||
opposite of the failure this section exists for. Mutation-verified: a
|
||||
one-letter typo (`coord-sweeep.sh`) turns exactly that check red.
|
||||
|
||||
**`coord-count.sh` prints TWO integers per mailbox** (`<name>\t<pending>\t<debt>`),
|
||||
and the first must stay pending: `board.sh` counts the same inbox files
|
||||
itself, so a debt-only count would put two different numbers under one name.
|
||||
|
|
@ -146,7 +202,7 @@ marketplace plugin. Three components, one boundary:
|
|||
mailbox, `~/.claude/coord/<repo>/orders/`, with four one-verb scripts —
|
||||
`coord-order-send.sh` (write), `coord-order-inbox.sh` (read for injection),
|
||||
`coord-order-claim.sh` (claim), `coord-order-done.sh` (terminal state).
|
||||
Pinned by `orders-selftest.sh` (110 checks).
|
||||
Pinned by `orders-selftest.sh` (116 checks).
|
||||
|
||||
**It is a separate CHANNEL, not more mail, and the axis is authorization.**
|
||||
Inbox content is untrusted cross-repo data that may never instruct a session
|
||||
|
|
@ -411,7 +467,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` (427 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
|
||||
|
|
@ -583,13 +639,17 @@ marketplace plugin. Three components, one boundary:
|
|||
`brief-nightly.sh` already carries for the briefing.
|
||||
|
||||
**The cost comes from `route.sh`'s row table, and `--dispatch` deliberately
|
||||
refuses a `--model`/`--effort` pair.** `--advisor opus` is a property of the
|
||||
ROW; two rows share a model/effort pair while differing on it, and the CLI
|
||||
accepts a wrong advisor silently. A dispatch taking the model directly would
|
||||
have no honest source for that flag, and both available guesses produce the
|
||||
same failure — a session that looks peer-reviewed without being. A Fable
|
||||
dispatch is therefore not a `--dispatch` outcome at all, exactly as it is not
|
||||
a `route.sh` outcome; it is a hand-written override.
|
||||
refuses a `--model`/`--effort` pair.** Until 2026-09-12 that rested on
|
||||
`--advisor opus` being a property of the ROW; that rule is struck (see
|
||||
`route.sh` above) and the argument was REWRITTEN rather than left standing on
|
||||
a premise the code no longer has — a comment defending a removed mechanism is
|
||||
how the next session restores it. The reason now: the rubric is a policy with
|
||||
exactly one copy, and a dispatch accepting the model directly would be a
|
||||
second, unscored way to reach the same decision — one recording no traits, no
|
||||
rationale and no `next-cost`, so nothing afterwards could say whether the
|
||||
ROUTING was wrong or the SCORING was. A Fable dispatch is therefore not a
|
||||
`--dispatch` outcome at all, exactly as it is not a `route.sh` outcome; it is
|
||||
a hand-written override.
|
||||
|
||||
**`--target-pane yes|no` is REQUIRED, with no default, and that is the same
|
||||
rule `--last-effort` carries.** It is a measurement of the world — does the
|
||||
|
|
@ -846,6 +906,101 @@ marketplace plugin. Three components, one boundary:
|
|||
it was exactly the unmeasured side effect the order flagged, and nothing
|
||||
in `~/repos` currently needs it (`.claude` has no repo children today).
|
||||
|
||||
**Ordre 20260903T190201Z-238406410 (2026-09-03, operator decision the same
|
||||
day): a git repo nested under a depth-1 REPO was invisible to the board, and
|
||||
the admission that closes it is criterion (a) - a STATE.md - never "all
|
||||
nested repos".** Discovery adds a depth-1 repo and stops; the else-branch
|
||||
container scan is the only place children are ever looked at, and it is
|
||||
unreachable for an entry that is itself a repo. Measured on the real tree
|
||||
before anything was written: **12 nested repos across the tree, exactly 1
|
||||
with a STATE.md** (`from-ai-to-chitta/content-sadhguru`, which had been
|
||||
running work and reporting to nobody). The other 11 are vendored or
|
||||
experimental checkouts under `claude-code-100x/`; the operator's decision is
|
||||
that they stay invisible ON PURPOSE - they do not even reach the `UTEN
|
||||
STATE.md` bucket, because that bucket is for repos someone opens. Verified
|
||||
after the fix with `board.sh`, not from memory:
|
||||
`from-ai-to-chitta/content-sadhguru in-progress ... Sonnet 5/high`, and
|
||||
`--plan` gives it a real tab with a `paste=` line.
|
||||
|
||||
**`add_nested_repos()` beside `add_dot_repos()`, never a widening of the `*`
|
||||
loops - the same argument ordre 20260818T124828Z made for dot repos, and the
|
||||
order made it again before this repo wrote a line.** Routing a depth-1 repo
|
||||
into the container branch would admit every nested checkout, which is exactly
|
||||
what criterion (a) declines. One level only: a repo inside a nested repo is a
|
||||
checkout of a checkout, and the selftest pins that depth 3 is NOT admitted. A
|
||||
dot-prefixed depth-1 repo gets the same nested scan an ordinary one gets
|
||||
(`add_dot_repos` takes a `d1` flag for it) - the asymmetry would otherwise be
|
||||
arbitrary, and nothing in the criterion distinguishes them.
|
||||
|
||||
**A nested repo now has TWO names, and conflating them would have put a
|
||||
fabricated 0 in three columns.** The board KEY is `<parent>/<child>` as the
|
||||
order specifies - a bare basename says nothing about where the checkout lives
|
||||
and would collide with a same-named depth-1 repo. The MAILBOX name is not
|
||||
that key: a mailbox is addressed by `basename(git toplevel)`, which is
|
||||
`coord-send.sh`'s own rule and the only name a session in that repo can hold,
|
||||
so `$COORD/<parent>/<child>/inbox` finds no directory and INN/ORDRE/FLY print
|
||||
0 for a repo that may have mail. That is the failed measurement wearing the
|
||||
reassuring value, in three columns at once. The record loop therefore carries
|
||||
`mbox` beside `name`, and `--voyage`'s order lookup takes `basename($vy_dir)`
|
||||
for the same reason. Mutation-verified: reverting the eight `$COORD/$mbox`
|
||||
reads to `$COORD/$name` turns exactly the three mailbox checks red with every
|
||||
other check in the section green.
|
||||
|
||||
**Which nested dirs those are is RECORDED by discovery, not re-derived.** The
|
||||
obvious re-derivation ("is my parent directory a git repo?") answers a
|
||||
slightly different question and would prefix every depth-1 repo if a scan
|
||||
root ever happened to be a checkout; `NESTED_LIST` cannot drift from the
|
||||
criterion because discovery is what writes it.
|
||||
|
||||
**The denominator line is independent of all of that and went in regardless,
|
||||
because the order said so and because it is ansikt 4 pointed at the board's
|
||||
own reading.** `undersoekt: N katalog(er) depth 1, M polyrepo-container(e),
|
||||
K nestede repo (J med STATE.md tatt med).` The header's repo count answers
|
||||
"how many were found"; nothing answered "how many were looked at", so a
|
||||
criterion excluding 11 of 12 nested repos was invisible at the surface built
|
||||
to show it. Real tree, 2026-09-04: `43 katalog(er) depth 1, 5
|
||||
polyrepo-container(e), 12 nestede repo (1 med STATE.md tatt med)`. A
|
||||
non-repo dot-dir counts in N (it WAS examined) and never in M (it is
|
||||
deliberately not a container - ordre 20260818's decision), so the line is a
|
||||
denominator, not a partition. Selftest section 32 pins each number against a
|
||||
fixture, then changes the tree and asserts the numbers move, so a hardcoded
|
||||
constant cannot pass.
|
||||
|
||||
**The line's own wording broke an existing check, and that is the finding
|
||||
worth recording.** `check "polyrepo container itself is not listed as a
|
||||
repo"` grepped the WHOLE output for `polyrepo`, so the footer's
|
||||
`polyrepo-container(e)` matched and the check went red on a board that was
|
||||
behaving correctly - the same defect class as coord-selftest section 38's
|
||||
cache grep reading a comment that EXPLAINS a pattern as an instance of it,
|
||||
and as the done-guard's rule that prose saying `status=done` must not trigger
|
||||
it. The check is now anchored at column 1, which is what it always meant: a
|
||||
container has no ROW.
|
||||
|
||||
**Bounded gap, stated rather than closed (1): the mailbox-keyed JOINS still
|
||||
key on the board name.** `--brief`/`--plan`'s `$OWED` join, `brief_orphans`
|
||||
and `--inbox-plan` match `coord-count.sh`'s mailbox names against RECORDS
|
||||
field 3, so a nested repo owing a reply would not get a debt tab and would be
|
||||
listed under `UTENFOR REPO-SKANNEN`. This change neither creates nor worsens
|
||||
that: before it, the repo was not in RECORDS at all, so both readings were
|
||||
already exactly as wrong. Closing it means threading a mailbox key through
|
||||
six awk programs, which was not ordered and is not a discovery change.
|
||||
Reported to `.claude` for its own order.
|
||||
|
||||
**Bounded gap, stated rather than closed (2): a 34-character key overflows
|
||||
the table's `%-32s` REPO column**, shifting `from-ai-to-chitta/content-sadhguru`'s
|
||||
row two characters right. Same class as `Fable 5.1/xhigh` overflowing the
|
||||
`%-14s` KOST column, and handled the same way: parsing is unaffected (every
|
||||
rendering that a driver reads is `key=value`), widening the column moves every
|
||||
fixed offset including three `cut -c89-` selftest helpers, and it is a
|
||||
rendering change nobody ordered. Truncating instead would be worse - it would
|
||||
destroy the very identity the compound key exists to carry, and break
|
||||
copy-paste into `--dispatch --repo`.
|
||||
|
||||
**Bounded gap, stated rather than closed (3):** a DOT-prefixed *nested* repo
|
||||
is not looked for. The `*` glob in `add_nested_repos()` has no dotglob, the
|
||||
combination was neither measured nor ordered, and `add_dot_repos` exists for
|
||||
the depth-1 and container cases only because those were.
|
||||
|
||||
**Ordre 20260820T204409Z (2026-08-20): `route_cmd_for()`'s four trait
|
||||
extractions searched the WHOLE route line, and `.*trait=` is greedy, so a
|
||||
rationale that names a trait won the match over the field itself.**
|
||||
|
|
@ -897,7 +1052,7 @@ marketplace plugin. Three components, one boundary:
|
|||
rationale, and prints one block of `key=value` lines: the rubric row, the rule
|
||||
that fired, the `next-cost` value, a pasteable startup command, the one-row
|
||||
cheaper fallback, and the STATE.md comment lines. Pinned by
|
||||
`route-selftest.sh` (69 checks).
|
||||
`route-selftest.sh` (73 checks).
|
||||
|
||||
**It is here because it is the WRITER for the field `board.sh` already reads.**
|
||||
`next-cost` had a reader and no writer, so it was hand-typed every session and
|
||||
|
|
@ -915,23 +1070,38 @@ marketplace plugin. Three components, one boundary:
|
|||
and belongs to the skill; turning scores into a row is a lookup and takes zero
|
||||
model calls.
|
||||
|
||||
**`--advisor opus` is emitted per ROW, on a need, never unconditionally.**
|
||||
Rows 1-2 always carry it (Sonnet main model, so opus is a capability lift —
|
||||
and since every fallback is one row cheaper and the cheap rows are Sonnet,
|
||||
this is what makes the quota fallback safe to take); rows 3-4 only at
|
||||
`reversibility=costly|one-way` (Opus main model, so it buys peer review where
|
||||
a mistake is not cheap to undo); rows 5-6 never, because the CLI rejects every
|
||||
advisor for a Fable main model. The alternative — the global `advisorModel`
|
||||
setting written by `/advisor` — is what this replaces: it applies to every
|
||||
session in every repo, which is how it burned quota before. The two triggers
|
||||
are almost disjoint by construction, since `costly` forces row 3 and `one-way`
|
||||
forces row 4, so a Sonnet row always has `reversibility=cheap`.
|
||||
`verification=none` is deliberately NOT a third trigger: beyond the stakes
|
||||
rule it would only add mistakes that are cheap to reverse, docs sessions
|
||||
(`known/none/cheap/local`) among them. Section 14 pins the rule and gates the
|
||||
three CLI facts it rests on against the installed `claude` without spending a
|
||||
token — advisor validation runs before the empty-prompt check, so `-p ""`
|
||||
reaches the validator and stops there.
|
||||
**NO `--advisor` is emitted, by this calculator or by `board.sh --dispatch`
|
||||
(operator decision 2026-09-12, `~/.claude/docs/2026-09-12-helhetlig-vurdering-arbeidssystemet.md`
|
||||
§ 4, cut row 3).** The struck rule fired per ROW on a need — always on the
|
||||
Sonnet rows (a capability lift, which is what made the quota fallback safe to
|
||||
take), and on the Opus rows at `reversibility=costly|one-way` (peer review
|
||||
where a mistake is not cheap to undo). It read well and it was killed by a
|
||||
MEASUREMENT, not by taste: of 54 dispatches the PM issued 08.–12.09, **zero**
|
||||
carried the flag, because sessions are started by hand from the model and
|
||||
effort rather than from the whole emitted line. A rule nothing honours is not
|
||||
a policy, and an emitted value nobody acts on is decoration in a field whose
|
||||
only job is to be evidence. The advisor is now what it already was in
|
||||
practice: **an operator decision per session**, said in one sentence in
|
||||
`route.sh --help`.
|
||||
The absence is pinned over the WHOLE trait space — 81 combinations, every
|
||||
line of output, with a known-positive control proving the sweep's grep can
|
||||
find a planted advisor — rather than on four sampled rows, because the claim
|
||||
is that no path emits it. `--dispatch` at `reversibility=costly` is pinned
|
||||
separately: that is the exact input a reintroduced rule would fire on.
|
||||
The three CLI gates section 14 used to carry (does `claude` accept
|
||||
`--advisor`, does it accept opus advising sonnet, does it still reject opus
|
||||
advising fable) went with the rule — they existed to gate facts the rule
|
||||
rested on, and the suite no longer depends on the installed `claude` at all.
|
||||
**The literal string `--advisor opus` is absent from `route.sh` ENTIRELY,
|
||||
including the paragraph recording what was struck** (it says "an opus advisor
|
||||
flag" in words): a blunt grep cannot tell a description from a specification,
|
||||
the same reason coord-selftest's cache assertion runs on an extracted path
|
||||
rather than the whole file. Do not restore the quote.
|
||||
Backward compatibility is pinned rather than assumed: a route line carrying a
|
||||
legacy `advisor=` field still parses and still yields a command. Measured the
|
||||
same day — **0 of 48** `<!-- route:` lines anywhere in `~/repos` carry one, so
|
||||
nothing in circulation depends on it, but a reader that broke on an unknown
|
||||
field would turn last month's STATE.md into "that repo has no route line".
|
||||
|
||||
**Rows 5-6 are never a `route.sh` outcome.** Until 2026-08-06 they fired
|
||||
only from an explicit `--opus-xhigh-failed` flag, mirroring a global
|
||||
|
|
@ -957,6 +1127,53 @@ marketplace plugin. Three components, one boundary:
|
|||
startup command. Corollary pinned by section 13: `skills/route/SKILL.md` must
|
||||
never declare an `effort:` frontmatter field, because frontmatter overrides the
|
||||
session effort and the reading would then measure the skill, not the session.
|
||||
**Order 20260901T185028Z-3612729538 (2026-09-01): `--last-model` was a closed
|
||||
set of three, Fable 5.1 shipped that day, and the refusal turned a
|
||||
measurement into a lie.** A session that genuinely ran Fable 5.1 could not
|
||||
record what it ran, so its record was either omitted or written as a name
|
||||
already in the set - and a lied record reads back months later as a
|
||||
measurement rather than as the gap it is. The whole point of the four
|
||||
`--last-*` fields is to be readable evidence, so this is the field's own
|
||||
failure mode, not a cosmetic one. Both edit sites moved, never one: the usage
|
||||
block and the validating case, because fixing the case alone is the
|
||||
two-copies-of-one-policy defect this file names repeatedly.
|
||||
|
||||
**"Fable 5" is KEPT, and the reason is NOT backward compatibility.** The
|
||||
nevner was measured rather than assumed - exactly 1 of 45 `route-last` lines
|
||||
across the machine carries it - which is thin enough that a future session
|
||||
would happily "clean up" the one stale line and then drop the value. The
|
||||
load-bearing reason is that `route.sh`'s own row table spells rows 5-6
|
||||
`Fable 5/high` and `Fable 5/xhigh`. Dropping the value would make the script
|
||||
refuse to record a name its own spec writes.
|
||||
|
||||
**The set is WIDENED, never replaced by form validation, and the selftest is
|
||||
what makes that a fact rather than a paragraph.** A pattern like
|
||||
`<family> <digits>[.<digits>]` would still catch a misspelled family and a
|
||||
drifted case (`opus 5`), and would stop catching **a version that does not
|
||||
exist**: `Fable 5.2` and `Opus 7` would both pass and read back as evidence
|
||||
that a model ran when it never shipped - the positive-looking null this repo
|
||||
refuses everywhere else, in the one field whose entire purpose is to be
|
||||
believed later. Section 8b therefore rejects `Fable 5.2` / `Fable 6` /
|
||||
`Fable 5.10` beside the accept case, so a later switch to form validation
|
||||
fails here instead of silently widening what the record can claim. The cost
|
||||
of the closed set is real and was paid before the choice was made, so the die
|
||||
message now names the repair - a newly shipped model is ADDED to the list,
|
||||
never approximated to a value already in it.
|
||||
|
||||
Untouched, deliberately: the row table (still six rows; `route.sh` still
|
||||
emits only 1-4, and Fable remains a hand-written override), and the note that
|
||||
a Fable session runs without an advisor because the CLI does not enforce it.
|
||||
|
||||
**Bounded gap, stated rather than closed:** `Fable 5.1/xhigh` is 15
|
||||
characters and overflows `board.sh`'s `%-14s` KOST column, shifting the rest
|
||||
of that row one column right. Parsing is unaffected - `next-cost` extraction
|
||||
is free text, and selftest section 6 now pins that the point release survives
|
||||
the round trip - so this is alignment, not data. Widening the column is a
|
||||
`board.sh` rendering change nobody ordered in this session, which is also why
|
||||
the value is deliberately ABSENT from section 6's widest-value loop: adding
|
||||
it there would go red, and the red would be the unfixed gap wearing a broken
|
||||
test's clothes. Reported to `.claude` instead.
|
||||
|
||||
- **Board's ORDRE column** counts PENDING orders per repo with the identical
|
||||
idiom as INN, and the two are never summed: INN is "others are waiting on
|
||||
YOU" (outgoing obligation), ORDRE is "authorized work is waiting on this
|
||||
|
|
@ -1033,6 +1250,40 @@ marketplace plugin. Three components, one boundary:
|
|||
broken `touch -t` fails there rather than turning the section into a test of
|
||||
nothing.
|
||||
|
||||
**Order 20260903T185736Z-1290610855 (2026-09-03): the PENDING age read the
|
||||
mtime, and `coord-order-done --return` rewrites it - so putting an order back
|
||||
reset the very reading that says how long it has waited.** An order returned
|
||||
three times could never look old, on the one surface that exists precisely so
|
||||
a repo nobody opens still shows something. Found by reading the board right
|
||||
after this repo returned an order of its own, not by looking for it: a file
|
||||
whose name says 2026-09-02 rendered `ORDRE 1:0d` minutes later. It is the
|
||||
same class as every other reading hardened here - a failed or reset
|
||||
measurement rendering as a reassuring value - with the twist that the reset
|
||||
was performed by the engine itself.
|
||||
|
||||
**Two questions, two sources, and only one of them moved.** A PENDING order's
|
||||
age is "how long has this sat with no owner" = now - DELIVERY time, and only
|
||||
the filename carries that; it is written once and nothing rewrites it. A
|
||||
CLAIMED order's age is "how long has it been in flight", which is the claim's
|
||||
own mtime and was already right - so `oldest_pending_age()` sits BESIDE
|
||||
`oldest_order_age()` rather than replacing it, and `coord-order-inbox.sh`
|
||||
keeps `age_of` for the claim marker next to a new `pending_age_of`. Switching
|
||||
FLY to the filename would answer the delivery question in the column that
|
||||
asks the flight question. An unparseable filename yields `?` for the whole
|
||||
reading, never a fabricated `0`, because an unmeasured order could be the
|
||||
oldest one - the same rule the mtime path already carried.
|
||||
|
||||
**Section 28's fixtures had to be rewritten, and that is the finding worth
|
||||
recording.** They encoded their intended ages in `touch -t` while their
|
||||
filenames held fixed 2026-01/2026-08 dates, which a filename-based reading
|
||||
makes both wrong and time-dependent - the checks would have started failing
|
||||
on their own as the wall clock moved past them. They now compute their stems
|
||||
from `date -v`, and the section asserts TWO ground truths, the filename for
|
||||
ORDRE and the mtime for FLY, because a fixture check guarding only one source
|
||||
leaves the other assertion resting on nothing. Mutation-verified in both
|
||||
files: restoring the mtime read turns exactly the defect checks red with
|
||||
every control green.
|
||||
|
||||
**There is deliberately NO process inspection, and the selftest asserts it
|
||||
structurally** (no live `pgrep`/`pkill`/`lsof` in `board.sh`, with a
|
||||
known-positive control proving the grep can find a planted call). Four
|
||||
|
|
@ -1074,6 +1325,135 @@ 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.
|
||||
|
||||
- **`--row <repo>` is the SEVENTH rendering: the table's row for ONE repo, as
|
||||
`field=value` lines** (order 20260912T202210Z-7588027378, operator decision
|
||||
2026-09-12). Same scan, never a second one, read-only like every other
|
||||
rendering. **The order calls it the sixth; by this file's own numbering
|
||||
`--inbox-plan` is the fourth and `--dispatch` the fifth, so it is the
|
||||
seventh** — corrected here rather than carried wrong.
|
||||
|
||||
**It exists because the columns WERE misread.** On 2026-09-11 the PM read the
|
||||
FLY column off the table by eye and got it wrong. Every other rendering a
|
||||
program consumes (`--plan`, `--inbox-plan`, `--dispatch`) is already
|
||||
`key=value`; the default view was the one left out, and the 34-character
|
||||
nested-repo key already shifts a row two characters right (a stated, unclosed
|
||||
gap), which makes counting spaces worse than it looks. `inn`, `ordre` and
|
||||
`fly` are three separate fields because they are three separate facts, and
|
||||
confusing them is the whole reason this exists. `status` is the bare token,
|
||||
never the table's `blocked>target` display, with `blocked-on` beside it;
|
||||
`neste` is last and uncut, for the same reason it is last in the record. An
|
||||
unknown repo exits **2** and writes NOTHING to stdout — an empty block would
|
||||
read as a repo whose every column is blank, which is a real and different
|
||||
state (`?`).
|
||||
|
||||
**`upushet` is the one field that is NOT a rendering of the scan, and it is
|
||||
named rather than blended in.** Nothing in the scan measures it, so it is
|
||||
read once, for the named repo only, and never enters the table, the plan or
|
||||
the briefing. It reads the remote-TRACKING ref, not the remote: `upushet=N`
|
||||
honestly means "the local ref says N", and `git ls-remote` is still what a
|
||||
claim ABOUT THE WORLD costs. A repo with no upstream reports `?`, never `0` —
|
||||
"nothing is waiting to be pushed" and "nobody looked" are exactly the pair
|
||||
this engine refuses to render alike.
|
||||
|
||||
**The fixture's three counts are three DIFFERENT integers (3/2/1), and that
|
||||
is the finding worth recording.** Built first with 2/1/1, it was
|
||||
mutation-tested by making `fly` read the ORDRE field — the exact 11.09
|
||||
misreading — and the check stayed GREEN, because the two fields held the same
|
||||
digit. A fixture that cannot tell two columns apart is the defect wearing a
|
||||
passing test, inside the section written to prevent it.
|
||||
|
||||
- **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 —
|
||||
|
|
@ -1121,10 +1501,10 @@ obligations in another repo.
|
|||
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
|
||||
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/route-selftest.sh` must exit 0 (69/69),
|
||||
`bash scripts/orders-selftest.sh` must exit 0 (110/110) and
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (257/257),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (427/427),
|
||||
`bash scripts/route-selftest.sh` must exit 0 (73/73),
|
||||
`bash scripts/orders-selftest.sh` must exit 0 (116/116) and
|
||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (54/54).
|
||||
- English for all code, docs, and commit messages (public repo). Norwegian
|
||||
trigger aliases in the skill description are deliberate.
|
||||
|
|
@ -1140,6 +1520,36 @@ obligations in another repo.
|
|||
and re-measured after: 11 of 11 now match. Only the Install block was
|
||||
converted; converting the other 39 indented blocks was not ordered and
|
||||
would bury a two-line fix in a whole-file diff.
|
||||
- **README's selftest numbers are pinned by `tests/selftest.test.mjs`, and the
|
||||
check lives there rather than in one of the five bash suites for a measured
|
||||
reason.** The badge and the five `## Development` comments rotted twice in a
|
||||
row - 529 carried from 0.25.0, then a badge saying 868 beside comments summing
|
||||
to 792, two different wrong sums of the same fact on the same screen - because
|
||||
nothing compared them to anything. The order behind the fix
|
||||
(`20260905T053602Z-6743615726`) suggested hosting it in whichever suite
|
||||
already pins README/catalog invariants; measured before choosing, **no such
|
||||
suite exists** (`grep -ln README scripts/*selftest*.sh` returns
|
||||
`board-selftest.sh` alone, on two incidental hits - a prose comment and a
|
||||
`research/README.md` fixture). This wrapper is the only place where all five
|
||||
numbers exist at once in a run that already happens: `runSuite()` captures
|
||||
each suite's own summary line, so the truth source is the line the suite
|
||||
prints, never a re-count. A check inside one suite could see its own total but
|
||||
would have to RE-RUN the other four (**212s sequentially**, measured
|
||||
2026-09-05 under `/bin/bash` 3.2) to see theirs, and grepping `check` calls out
|
||||
of the scripts is both the second copy of the counting and a wrong one, since
|
||||
those calls sit inside loops. The badge is compared against the MEASURED sum,
|
||||
not against the five README comments - a badge agreeing with five stale
|
||||
comments is the 868-beside-792 shape one layer down - and a suite that stops
|
||||
printing a summary FAILS the check rather than being skipped, because an
|
||||
absent measurement must not read as a matching one. It adds no bash check, so
|
||||
the five counts and the 893 badge are unchanged by its arrival.
|
||||
Mutation-verified in both directions: `73 -> 74` on the route comment gives
|
||||
"README says route-selftest has 74 checks; it reported 73", `893 -> 894` on
|
||||
the badge gives "README's badge says 894 selftest checks; the five suites
|
||||
reported 893", and the restored file is 12/12 green. **Bounded gap, stated
|
||||
rather than closed:** `CLAUDE.md`'s own copies of the five counts (in this
|
||||
section, above) are NOT checked - a second surface carrying the same numbers,
|
||||
measured and left alone because widening the check to it was not ordered.
|
||||
- Conventional Commits: `type(scope): description`.
|
||||
|
||||
## Commands
|
||||
|
|
@ -1147,7 +1557,7 @@ obligations in another repo.
|
|||
- Test: `bash scripts/coord-selftest.sh`, `bash scripts/board-selftest.sh`,
|
||||
`bash scripts/route-selftest.sh`, `bash scripts/orders-selftest.sh` and
|
||||
`bash scripts/state-line-guard-selftest.sh` (or `npm test`, the Node wrapper
|
||||
around all five plus the hook tests)
|
||||
around all five plus the hook tests and the README-number check)
|
||||
- Order queue smoke test: `CLAUDE_COORD_DIR=$(mktemp -d) bash
|
||||
scripts/coord-order-send.sh --to smoke --from tester --subject s --message m`
|
||||
then `CLAUDE_COORD_DIR=<same> bash scripts/coord-order-inbox.sh --repo smoke`
|
||||
|
|
@ -1156,7 +1566,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
|
||||
|
|
@ -1172,6 +1586,13 @@ obligations in another repo.
|
|||
- Sweep smoke test: `bash scripts/coord-sweep.sh` (dry-run is the default, so
|
||||
this writes nothing; never add `--write` to a smoke test against the real
|
||||
mailbox)
|
||||
- launchd template check: `plutil -lint launchd/*.plist` (XML only). The paths,
|
||||
labels, placeholders and schedule are pinned by coord-selftest section 38 —
|
||||
run that, not `launchctl list`, to find a broken plist. Installing an agent
|
||||
is `sed` + `launchctl load` (see the header of either plist); proving it
|
||||
works is `launchctl start <label>` plus a new line in
|
||||
`~/Library/Logs/repo-mailbox-<brief|sweep>.log`, which is a DIFFERENT file
|
||||
from the sweep's own closure log at `$CLAUDE_COORD_DIR/_sweep.log`
|
||||
|
||||
## Release
|
||||
|
||||
|
|
|
|||
43
README.md
43
README.md
|
|
@ -10,11 +10,11 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
|
|||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ The plugin ships empty: your mailbox is created lazily on first send, on your ma
|
|||
|
||||
Since v0.8.0 it closes with one aggregate line about mail pending in *other* mailboxes, so an empty inbox no longer reads as "all clear" while messages sit unanswered elsewhere. Two integers, never a roster: naming the other mailboxes would put their situation inside your repo's injection, and the line explicitly disclaims the obligation it sits beneath — those counts are not yours to handle, and counting them delivered nothing.
|
||||
|
||||
**Choosing between repos (the `board` skill).** "What should I work on?", "who is waiting on me?", "what unblocks the most?" — `board.sh` scans every repo it can find and reads three sources per repo: the STATE.md next-step block and its optional board line, `git status`, and the pending count in that repo's mailbox. The skill runs it, ranks by leverage (what unblocks the most, cheapest first) and answers with one repo and the rule that fired, never the table. Read-only: it writes to no repo, no STATE.md, and no mailbox. The board is deliberately *not* wired into session start — it runs when asked.
|
||||
**Choosing between repos (the `board` skill).** "What should I work on?", "who is waiting on me?", "what unblocks the most?" — `board.sh` scans every repo it can find and reads three sources per repo: the STATE.md next-step block and its optional board line, `git status`, and the pending count in that repo's mailbox. The skill runs it, ranks by leverage (what unblocks the most, cheapest first) and answers with one repo and the rule that fired, never the table. Read-only: it writes to no repo, no STATE.md, and no mailbox. The board is deliberately *not* wired into session start — it runs when asked. Discovery takes git repos at depth 1, the children of a polyrepo container, and — since 0.33.1 — a repo nested under a depth-1 repo **only when it carries a `STATE.md`**: measured on the real tree, 12 such checkouts existed and exactly 1 had one, so admitting all of them would have buried the board in vendored clones. Every run prints its own denominator (`undersoekt: N katalog(er) depth 1, M polyrepo-container(e), K nestede repo (J med STATE.md tatt med)`), because a repo count alone says how many were found and nothing about how many were looked at.
|
||||
|
||||
The mailbox is one of its three inputs, which is why the board lives here. Note the axis: a repo's pending count means *others are waiting on it*, an obligation it owes outward. Who a repo waits *on* comes only from its own board line, because the message format has no reply-to field.
|
||||
|
||||
|
|
@ -114,6 +114,8 @@ 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 --row <repo> # one repo's row as field=value lines (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
|
||||
|
|
@ -129,11 +131,28 @@ The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to …
|
|||
|
||||
**`coord-sweep.sh` is the only script that closes a message without a human in the loop**, and it is bounded to one mechanically decidable class: a directed message whose sender declared `reply-expected: no`, older than a grace window (default 14 days). A message that owes a reply is never touched, at any age, with any flag — answering it would mean deciding something on the receiving repo's behalf. Dry-run is the default, inverted from every other script here, because this is the one that destroys pending state. Every closure appends a line naming the sender and subject: a directed message has no seen-tracking, so the sweep cannot tell "seen and ignored" from "never delivered", and a notice to a repo left unopened for the whole window is closed *unread*. The log is what keeps that from being silent.
|
||||
|
||||
**`board.sh --brief` renders the nightly briefing**, a second rendering of the scan the board already does rather than a second scan: the repos with an unhandled inbox, each one's next step *in full* (the 38-character cut belongs to the table column, not to the record), and the exact command to start a session there — derived by calling `route.sh` with that repo's own four traits, since `next-cost` alone cannot produce the advisor flag. A repo with no route line is told so rather than handed a guessed command. It also cross-checks itself against `coord-count.sh`, because the repo scan and the mailbox are different populations: a mailbox can carry a name no scan will ever produce, such as a declared non-git surface (`CLAUDE_COORD_REPO`) or a checkout outside the roots, and a briefing that only walked the scan would answer "who is waiting on you" with a number it quietly knew was short.
|
||||
**Since 0.33.0 that sweep is scheduled, and the schedule is the whole feature — invocation was the gap, not the mechanism.** `coord-sweep.sh` shipped in 0.10.0 and had then never run once: measured 2026-09-03 across 52 mailboxes, 27 pending directed messages, of which 23 were pure notices being re-injected at every session start in repos nobody had opened. The script was correct and unreachable, so nothing new was built — a second mechanism would have been two copies of a policy that already existed. `launchd/com.ktg.repo-mailbox-sweep.plist` runs `--write --days 14` daily at 05:30, half an hour *before* the briefing agent — not because it changes what the briefing reports, but because the briefing scans the same mailbox this mutates, and the two must not overlap. The briefing's *debt* figure is in fact unaffected: since 0.22.0 it is computed from `coord-count.sh`'s `owed` column, and this sweep closes only messages that owe nothing. What the sweep moves is the raw pending count — the table's `INN` column, and the volume every repo gets injected at session start. The window is written out in the plist rather than left to the script's default: it is a policy constant decided on a measured distribution (30 days would have closed 0 messages, 14 closed 7, 7 would have closed 13), and changing a default must never silently change what an unattended job closes every night.
|
||||
|
||||
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.
|
||||
Both agents are **templates**, carrying `__CHECKOUT__`/`__HOME__` placeholders rather than absolute paths, because this repo is mirrored publicly. Substitute them at install time:
|
||||
|
||||
**`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`.
|
||||
```bash
|
||||
sed -e "s|__CHECKOUT__|$PWD|g" -e "s|__HOME__|$HOME|g" \
|
||||
launchd/com.ktg.repo-mailbox-sweep.plist \
|
||||
> ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
```
|
||||
|
||||
**`launchctl list` proves an agent is *loaded*, never that it is *right*.** A plist naming a script that does not exist loads cleanly and then silently never runs — there is no output to be wrong and no exit status to read, so the failure looks exactly like a quiet machine. Two separate things close that: `launchctl start <label>` followed by a line appearing in `~/Library/Logs/repo-mailbox-sweep.log` is the only runtime proof the program path resolves, and `coord-selftest.sh` section 38 asserts statically, for *every* plist in `launchd/`, that the path it names is a file that exists in this repo, that the `Label` matches the filename, that the placeholders survive, and that no agent points into the version-pinned plugin cache. Note that the launchd log is not the closure log: `$CLAUDE_COORD_DIR/_sweep.log` is where the record of each closed notice lives.
|
||||
|
||||
**`board.sh --brief` renders the nightly briefing**, a second rendering of the scan the board already does rather than a second scan: the repos with an unhandled inbox, each one's next step *in full* (the 38-character cut belongs to the table column, not to the record), and the exact command to start a session there — derived by calling `route.sh` with that repo's own four traits, since `next-cost` names the row rather than spelling the command and there must be exactly one spelling in circulation. A repo with no route line is told so rather than handed a guessed command. It also cross-checks itself against `coord-count.sh`, because the repo scan and the mailbox are different populations: a mailbox can carry a name no scan will ever produce, such as a declared non-git surface (`CLAUDE_COORD_REPO`) or a checkout outside the roots, and a briefing that only walked the scan would answer "who is waiting on you" with a number it quietly knew was short.
|
||||
|
||||
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 at 06:00; like the sweep agent above it points at a checkout, never at the version-pinned plugin cache, and it is pinned by the same section 38 checks.
|
||||
|
||||
**`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 --row <repo>` renders one repo's row as `field=value` lines**, a seventh rendering of the same scan and the only one that answers about a single repo. It exists because the table's columns *were* misread: the fixed-width view is for a human, and reading a column off it by eye is how `FLY` was read as `ORDRE` on 2026-09-11 — while every other rendering a program consumes is already `key=value`. `inn`, `ordre` and `fly` are three separate fields because they are three separate facts; `status` is the bare token rather than the table's `blocked>target` display, with `blocked-on` beside it; `neste` is last and uncut. An unknown repo exits **2** and writes nothing, because an empty block would read as a repo whose every column is blank — a real and different state. One field is deliberately *not* a rendering of the scan and is named as such: `upushet` is measured once, for that repo only, from the remote-**tracking** ref rather than the remote, so a repo with no upstream reports `?` and never `0`.
|
||||
|
||||
**`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 **five ordered groups** decide it, each a lookup over a field the scan already read rather than a weighted score: (1) **chain-root credit**, most repos released first; (2) **debt**, most-owed-first, whatever the status; (3) `planned`; (4) `in-progress`; (5) `?`/`MALFORMED` — undeclared, last and labelled. Within a group, ties break on a cheap Sonnet `next-cost` row, then oldest plan first. A 0.19.0 weighted score (`40 ×` repos released, `15 ×` unhandled inbox messages) briefly stood in this spot and *could* express "this repo owes one message and releases two others" as a single number — but re-tuning those two coefficients would have silently reordered a parser living in another repo, with no test here able to hold a ranking stable for a consumer it can't see; the operator replaced it with the group order in 0.20.0 for that reason. Groups 3 and 4 are `planned` above `in-progress`, inverted from every earlier version by the same decision: turning a decision into motion is the slow step, live work is already moving. **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 group that placed the repo, 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.
|
||||
|
||||
|
|
@ -179,12 +198,12 @@ 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/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
|
||||
npm test # all five selftests plus the hook tests, via node --test
|
||||
bash scripts/coord-selftest.sh # 257 checks against a throwaway mailbox
|
||||
bash scripts/board-selftest.sh # 427 checks against a throwaway repo tree
|
||||
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
|
||||
bash scripts/orders-selftest.sh # 116 checks, incl. the 20-way barriered claim race
|
||||
bash scripts/state-line-guard-selftest.sh # 54 checks, incl. the Edit replace_all projection and the ratchet
|
||||
npm test # all five selftests, the hook tests, and the README-number check
|
||||
|
||||
TDD is the house rule: every behavior change lands with a failing selftest check first.
|
||||
|
||||
|
|
|
|||
152
docs/2026-09-03-coordination-debt-measurement.md
Normal file
152
docs/2026-09-03-coordination-debt-measurement.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Coordination debt: what does not converge, and why building a second
|
||||
# mechanism would have been wrong
|
||||
|
||||
Measured 2026-09-03 against the live mailbox, for order
|
||||
`20260902T113745Z-1254925290-from-.claude` (WP5). Every number below was
|
||||
produced by a command, and every negative result carries the control that
|
||||
proves the query could have found something.
|
||||
|
||||
The order offered two candidate mechanisms and said to choose by measurement,
|
||||
not taste: a broadcast TTL, or a bulk-ack for pure notices. The measurement
|
||||
chose neither. One of them is already built and has never been run; the other
|
||||
addresses the class that converges on its own.
|
||||
|
||||
## Denominators
|
||||
|
||||
| population | count |
|
||||
|---|---|
|
||||
| mailbox directories under the coord root | 55 |
|
||||
| of those, holding an `inbox/` | 52 |
|
||||
| pending directed messages across all inboxes | 27 |
|
||||
| broadcasts in `_broadcast/inbox/` | 17 |
|
||||
| (mailbox x broadcast) delivery pairs | 884 |
|
||||
|
||||
The three directories with no `inbox/` are named rather than silently dropped:
|
||||
`jobbsok`, `mediemon`, `medieovervaaking`. 55 - 3 = 52 is the reconciliation,
|
||||
stated because an unreconciled pair of denominators in one report is the same
|
||||
positive-looking null this engine refuses everywhere else.
|
||||
|
||||
## The two classes behave in opposite directions
|
||||
|
||||
**Directed messages do not converge.** They are re-injected at every session
|
||||
start until a session closes them by hand. Of the 27 pending, 4 owe a reply and
|
||||
23 are pure notices (`reply-expected: no`). By age:
|
||||
|
||||
| class | <7d | 7-13d | 14-29d | >=30d |
|
||||
|---|---|---|---|---|
|
||||
| owes a reply | 0 | 3 | 1 | 0 |
|
||||
| pure notice | 10 | 6 | 7 | 0 |
|
||||
|
||||
**Broadcasts converge on their own.** Reading one records it as seen, so a
|
||||
mailbox clears its whole backlog on its next session. 263 of the 884 pairs are
|
||||
unread (29.8%), and the distribution shows the self-clearing: 10 mailboxes are
|
||||
fully current, 26 sit at exactly 6 unread (the newest announcements), and the
|
||||
tail is short.
|
||||
|
||||
## The floor under the broadcast number, which strengthens the case
|
||||
|
||||
Two mailboxes hold all 17 broadcasts unread, and neither can ever read them:
|
||||
|
||||
- `ktg-plugin-marketplace` is a **retired `--to` address**. It is a polyrepo
|
||||
directory, not a git repo, so `basename(git toplevel)` can never resolve to
|
||||
it and no session can hold that identity. This is already documented as
|
||||
engine behaviour; the 17 unread are its permanent consequence.
|
||||
- `llm-ingestion-guard` has no checkout anywhere under `/Users/ktg`
|
||||
(`find -maxdepth 4`, with `llm-ingestion-okf` as the known-positive control
|
||||
proving the query finds a real one) and no `.origin`. It also holds the
|
||||
single oldest pending notice, 24 days.
|
||||
|
||||
So **34 of 263 unread pairs (12.9%) are a permanent floor no TTL would reduce
|
||||
to zero** - it would close them, but it would be closing announcements for
|
||||
mailboxes that were never going to read anything. `.origin` absence alone is
|
||||
NOT a proxy for unholdable: `repos` also lacks one, yet sits at 2 unread of 17,
|
||||
which is only possible if something reads it.
|
||||
|
||||
## Why no second mechanism was built
|
||||
|
||||
`coord-sweep.sh` already **is** the bulk-ack for pure notices: machine-wide,
|
||||
one mechanically decidable class (`reply-expected: no`, older than a grace
|
||||
window), dry-run by default, closing through `coord-done.sh`, logging sender
|
||||
and subject for every closure. Building the order's second candidate would have
|
||||
been a second copy of a shipped policy - the defect class this repo names
|
||||
repeatedly.
|
||||
|
||||
Its own dry-run reports what it would close today, and the figures agree
|
||||
exactly with the independent classification above:
|
||||
|
||||
--days 30 -> 0 messages
|
||||
--days 14 -> 7 messages
|
||||
--days 7 -> 13 messages
|
||||
|
||||
**The gap is invocation, not mechanism.** No `_sweep.log` exists at the default
|
||||
path, and no sweep log exists anywhere under `~/.claude` (the one `*sweep*` hit
|
||||
is an unrelated plugin file) - stated that way because `--log` can override the
|
||||
default, so an absent default-path log alone would not prove it never ran.
|
||||
|
||||
## What remains, and why it is not this repo's call
|
||||
|
||||
Making the notice class converge without opening each repo means running
|
||||
`coord-sweep.sh --write` unattended. That closes mail in 51 other repos'
|
||||
inboxes, unread, and the script's own design says so in as many words: a notice
|
||||
to a repo left unopened for the whole window is closed unread, and the log is
|
||||
the only thing standing between that and a silent disappearance.
|
||||
|
||||
Deciding that on another repo's behalf is the one anti-pattern with no
|
||||
exception clause, and the grace window is a policy constant of the same class
|
||||
as the STATE.md line limit, which was an operator decision both times it moved.
|
||||
Dry-run is the default precisely because this is the script that destroys
|
||||
pending state; flipping that to a schedule is the operator's act, not a
|
||||
plugin's.
|
||||
|
||||
The messages that owe a reply are untouched by any of this, at any age, with
|
||||
any flag. That is not a gap to close later - it is the rule that keeps a
|
||||
procedural duty from becoming a substantive one.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: the before-state, captured 2026-09-04 08:0x UTC
|
||||
|
||||
The operator authorized the sweep on 2026-09-03 (14-day window, scheduled via
|
||||
launchd). This is `coord-count.sh` immediately before the authorized
|
||||
`--write --days 14` run, recorded here because a before-state stops existing
|
||||
the moment the write happens, and the order asks for debt before/after with a
|
||||
denominator.
|
||||
|
||||
mailbox pending owed origin-age
|
||||
app-creator 1 1 -
|
||||
claude-playlist-corpus 1 0 -
|
||||
graceful-handoff 3 0 -
|
||||
human-friendly-style 1 0 -
|
||||
ki-produktivitetsmodell 1 0 -
|
||||
llm-ingestion-guard 1 0 25
|
||||
llm-security 2 0 -
|
||||
mcp-servere 2 0 -
|
||||
okr 1 0 -
|
||||
org-ops 5 2 -
|
||||
portfolio-optimiser-commons 3 0 -
|
||||
repo-standard 1 1 -
|
||||
wiki-advise 2 0 -
|
||||
.claude 1 0 -
|
||||
.profile 1 0 -
|
||||
------------------------------------------------------
|
||||
15 mailboxes with pending mail 26 4
|
||||
|
||||
`$CLAUDE_COORD_DIR/_sweep.log` did not exist: the sweep had still never run.
|
||||
|
||||
**The control this file exists to make runnable:** after the write, the `owed`
|
||||
column must be BYTE-IDENTICAL (total 4), because the sweep spares every message
|
||||
that owes a reply at any age. A changed `owed` figure means the sweep closed
|
||||
something it must never touch, and is a defect, not a result.
|
||||
|
||||
**The dry-run said 11, not the 7 this document measured a day earlier, and the
|
||||
gap is entirely the moving cutoff.** Verified two ways rather than assumed.
|
||||
Yesterday's 14-day cutoff was ~20260820T184902; today's is 20260821T055442.
|
||||
Four notices timestamped 2026-08-20 between those two instants
|
||||
(`human-friendly-style` T210113Z, `ki-produktivitetsmodell` T205611Z, `org-ops`
|
||||
T210955Z and T211358Z) crossed the boundary in one calendar day: 7 + 4 = 11.
|
||||
Independently, `--days 15` today yields 6, and those 6 plus
|
||||
`mcp-servere/20260820T104644Z` - which sits between the 15-day cutoff and
|
||||
yesterday's 14-day one - reconstruct yesterday's 7 exactly. The engine is
|
||||
consistent; the 7 was a measurement of a moment, never a constant, and reading
|
||||
it as one would have been face 3 of the verification law pointed at our own
|
||||
report.
|
||||
99
launchd/com.ktg.repo-mailbox-sweep.plist
Normal file
99
launchd/com.ktg.repo-mailbox-sweep.plist
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!--
|
||||
Daily FYI sweep. Closes directed messages whose sender declared
|
||||
reply-expected: no and whose filename timestamp is older than 14 days, across
|
||||
every mailbox on this machine, through coord-done.sh.
|
||||
|
||||
INVOCATION WAS THE GAP, NOT THE MECHANISM. coord-sweep.sh shipped in 0.10.0
|
||||
and had never run once against the real mailbox - measured 2026-09-03: 27
|
||||
pending directed messages, 23 of them pure notices nobody was ever going to
|
||||
act on, re-injected at every session start in repos nobody had opened. The
|
||||
script was correct and unreachable. This file is the whole fix; no new
|
||||
mechanism was built, and building a second one would have been two copies of
|
||||
a policy that already existed.
|
||||
|
||||
THE 14-DAY WINDOW IS THE OPERATOR'S CONSTANT, NOT A DEFAULT WEARING A
|
||||
SCHEDULE (decided 2026-09-03, on a measured distribution: 30d -> 0 messages,
|
||||
14d -> 7, 7d -> 13). It is written out explicitly here rather than left to
|
||||
coord-sweep.sh's own default, so that changing the script's default can never
|
||||
silently change what this agent closes every night.
|
||||
|
||||
WHAT IT CAN NEVER DO. A message that owes a reply is untouched at any age -
|
||||
the script's own rule, not this file's. This agent only supplies the
|
||||
invocation; every bound on what gets closed lives in coord-sweep.sh, and the
|
||||
closure log ($CLAUDE_COORD_DIR/_sweep.log, NOT the launchd log below) is the
|
||||
only record that a notice closed unread ever existed.
|
||||
|
||||
THE TWO AGENTS MUST NEVER SHARE AN HOUR. com.ktg.repo-mailbox-brief renders
|
||||
at 06:00 from a scan of the same mailbox this mutates, so a briefing rendered
|
||||
mid-sweep reads a mailbox changing underneath it. This runs at 05:30, clear of
|
||||
it; coord-selftest.sh section 38 asserts the two hours differ.
|
||||
|
||||
What it does NOT change is the briefing's DEBT figure. Since 0.22.0 that is
|
||||
computed from coord-count.sh's `owed` column, and this sweep closes only
|
||||
messages that owe nothing - so the debt listing is identical before and after.
|
||||
What moves is the raw pending count (the table's INN column, the FYI-only
|
||||
naming in --brief, and the volume every repo gets injected at session start).
|
||||
Claiming the briefing reports "the debt that remains" because of this agent
|
||||
would be an overclaim; the ordering exists for the read/write overlap alone.
|
||||
|
||||
ZERO MODEL CALLS, same as the briefing and for the same reason: the operator
|
||||
authenticates by subscription, so a headless `claude -p` job would draw from
|
||||
the same quota pool as interactive work. This runs one shell script.
|
||||
|
||||
PATH: every binary this touches (bash, date, grep, sed, basename, tr, cut)
|
||||
lives in /usr/bin or /bin, so launchd's minimal default PATH is sufficient
|
||||
and no EnvironmentVariables block is needed.
|
||||
|
||||
The program path points at the SOURCE REPO, deliberately. The alternative is
|
||||
version-pinned (~/.claude/plugins/cache/.../repo-mailbox/<version>/...), so
|
||||
an agent pointing there would break silently on the next version bump - and a
|
||||
second copy of these scripts on disk is the exact defect class that produced
|
||||
the 0.12.1 stale-fallback bug.
|
||||
|
||||
This file is a TEMPLATE. It carries no absolute home path on purpose: the
|
||||
repo is mirrored publicly, and a plist is the one file here that would need
|
||||
one. Substitute both placeholders at install time.
|
||||
|
||||
Install: sed -e "s|__CHECKOUT__|$PWD|g" -e "s|__HOME__|$HOME|g" \
|
||||
launchd/com.ktg.repo-mailbox-sweep.plist \
|
||||
> ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
Verify: launchctl list | grep com.ktg.repo-mailbox-sweep # loaded only
|
||||
launchctl start com.ktg.repo-mailbox-sweep # proves the path
|
||||
tail ~/Library/Logs/repo-mailbox-sweep.log # the actual proof
|
||||
Remove: launchctl unload ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
|
||||
`launchctl list` proves the agent is LOADED, never that it does anything
|
||||
right: a wrong program path produces a loaded agent that silently never runs.
|
||||
Only `launchctl start` plus a line in the log below proves the path resolves.
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.ktg.repo-mailbox-sweep</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>__CHECKOUT__/scripts/coord-sweep.sh</string>
|
||||
<string>--write</string>
|
||||
<string>--days</string>
|
||||
<string>14</string>
|
||||
</array>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>__HOME__/Library/Logs/repo-mailbox-sweep.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>__HOME__/Library/Logs/repo-mailbox-sweep.log</string>
|
||||
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>5</integer>
|
||||
<key>Minute</key>
|
||||
<integer>30</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.31.0",
|
||||
"version": "0.34.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
838
scripts/board-selftest.sh
Executable file → Normal file
838
scripts/board-selftest.sh
Executable file → Normal file
|
|
@ -244,8 +244,8 @@ for n in 1 2 3; do echo "msg" > "$CLAUDE_COORD_DIR/repo-a/inbox/2026-msg$n-from-
|
|||
echo "old" > "$CLAUDE_COORD_DIR/repo-a/archive/2026-old-from-x.md"
|
||||
|
||||
# repo-owes: unhandled inbox AND a route line, so the briefing can derive the
|
||||
# EXACT startup command - advisor flag included - from the repo's own four
|
||||
# traits instead of guessing from next-cost alone. Its NESTE runs deliberately
|
||||
# EXACT startup command from the repo's own four traits instead of guessing
|
||||
# from next-cost alone. Its NESTE runs deliberately
|
||||
# past the table's 38-character column: carrying that line whole is the reason
|
||||
# the briefing exists at all.
|
||||
mkrepo "$ROOT/repo-owes"
|
||||
|
|
@ -355,7 +355,13 @@ printf '%s' "$OUT" | grep -q 'repo-a'; check "discovers top-level git repo" $?
|
|||
printf '%s' "$OUT" | grep -q 'plug-x'; check "discovers nested polyrepo git repo (depth 2)" $?
|
||||
printf '%s' "$OUT" | grep -q 'repo-c'; check "lists git repo without STATE.md" $?
|
||||
printf '%s' "$OUT" | grep -q 'plain-dir'; [ $? -ne 0 ]; check "ignores non-git directory tree" $?
|
||||
printf '%s' "$OUT" | grep -q 'polyrepo'; [ $? -ne 0 ]; check "polyrepo container itself is not listed as a repo" $?
|
||||
# Anchored at column 1, and the anchor is load-bearing: the claim is "the
|
||||
# container has no ROW", and a row starts with the repo name. Unanchored, this
|
||||
# check now matches the denominator line's own word "polyrepo-container(e)" -
|
||||
# the same defect class as a file-wide grep reading a comment that EXPLAINS a
|
||||
# pattern as an instance of it (coord-selftest section 38's cache assertion,
|
||||
# and the done-guard's rule that prose saying status=done must not trigger it).
|
||||
printf '%s' "$OUT" | grep -q '^polyrepo'; [ $? -ne 0 ]; check "polyrepo container itself is not listed as a repo" $?
|
||||
# Guard the fixture itself: if git ever stops writing a .git FILE for
|
||||
# worktrees, the next two checks would pass for the wrong reason.
|
||||
[ -f "$ROOT/wt-feature/.git" ]; check "fixture: worktree .git is a FILE, not a dir" $?
|
||||
|
|
@ -575,10 +581,15 @@ printf '%s' "$BRIEF" | grep -q 'check-versions'
|
|||
check "brief prints the full NESTE line, not the 38-char table excerpt" $?
|
||||
|
||||
# Derived by CALLING route.sh with the repo's own four traits - deliberately
|
||||
# not spelled out in board.sh - so the rubric keeps exactly one copy. Row 1
|
||||
# carries the advisor, which is what makes the quota fallback safe to take.
|
||||
printf '%s' "$BRIEF" | grep -q 'claude --model sonnet --effort high --advisor opus'
|
||||
# not spelled out in board.sh - so the rubric keeps exactly one copy.
|
||||
printf '%s' "$BRIEF" | grep -q 'claude --model sonnet --effort high'
|
||||
check "brief derives the exact startup command from the repo's route line" $?
|
||||
# Anchored to end-of-line, because the loose grep above would pass just as
|
||||
# happily on a command that grew a flag back. The rubric emits no advisor
|
||||
# since 2026-09-12 and the briefing is a rendering of the rubric, not a
|
||||
# second policy.
|
||||
printf '%s' "$BRIEF" | grep -q 'claude --model sonnet --effort high$'
|
||||
check "brief's startup command ends at the effort - no advisor is appended" $?
|
||||
|
||||
# The command must start its OWN line. `fold` copies its input's missing
|
||||
# trailing newline, which ran the command onto the tail of the NESTE prose and
|
||||
|
|
@ -894,8 +905,8 @@ n_b="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-b$' | cut -d: -f1)"
|
|||
check "uavklart repos rank below every repo that declared a status" $?
|
||||
|
||||
# The command comes from route.sh, same single copy of the rubric the briefing
|
||||
# uses - advisor flag included, since that is a property of the ROW.
|
||||
printf '%s' "$PLAN" | grep -q '^command=claude --model sonnet --effort high --advisor opus$'
|
||||
# uses.
|
||||
printf '%s' "$PLAN" | grep -q '^command=claude --model sonnet --effort high$'
|
||||
check "plan derives the exact startup command from the repo's route line" $?
|
||||
|
||||
# Both no-command causes must degrade to a marker. A bare `command=` would be
|
||||
|
|
@ -926,7 +937,7 @@ check "plan prints the full NESTE line, not the 38-char table excerpt" $?
|
|||
# because they answer different questions: a driver cd's the pane itself and
|
||||
# would choke on a compound line, while the operator needs ONE thing to copy.
|
||||
# Assembling it by hand from two fields is where a tab lands in the wrong repo.
|
||||
printf '%s' "$PLAN" | grep -q "^paste=cd $ROOT/repo-owes && claude --model sonnet --effort high --advisor opus\$"
|
||||
printf '%s' "$PLAN" | grep -q "^paste=cd $ROOT/repo-owes && claude --model sonnet --effort high\$"
|
||||
check "paste= is the whole line: cd into the repo, then the startup command" $?
|
||||
|
||||
printf '%s\n' "$PLAN" | grep -A7 '^repo=repo-typo$' | grep -q '^paste='; [ $? -ne 0 ]
|
||||
|
|
@ -1524,7 +1535,7 @@ check "ip-repo-no-state reports 1 owed (no reply-expected: no declared)" $?
|
|||
# class=repo still derives the exact startup command from the repo's own
|
||||
# route line - same single copy of the rubric every other rendering uses.
|
||||
printf '%s\n' "$IPLAN" | grep -A8 '^repo=ip-repo-done-fyi$' \
|
||||
| grep -q '^command=claude --model sonnet --effort high --advisor opus$'
|
||||
| grep -q '^command=claude --model sonnet --effort high$'
|
||||
check "class=repo block derives its command from the repo's own route line" $?
|
||||
|
||||
# class=no-state and class=orphan-mailbox can never fabricate a command - ask
|
||||
|
|
@ -1760,8 +1771,8 @@ d6="$("$BOARD" --roots "$ROOT" --dispatch --repo no-such-repo --prompt-file "$DS
|
|||
[ "$rc" -eq 2 ]; check "dispatch: refuses a repo the scan does not know" $?
|
||||
|
||||
# Invalid traits must refuse, never degrade to a command without them: a
|
||||
# command missing --advisor reads exactly like a row that legitimately has no
|
||||
# advisor.
|
||||
# command built on three scored traits and one silently dropped one reads
|
||||
# exactly like a fully scored decision.
|
||||
d7="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no \
|
||||
--path bogus --verification strong --reversibility cheap --scope local --rationale t 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "dispatch: refuses trait values route.sh rejects, rather than emitting a partial command" $?
|
||||
|
|
@ -1783,18 +1794,27 @@ printf '%s\n' "$d8" | grep -q '^paste=cd '; check "dispatch: plan block carries
|
|||
printf '%s\n' "$d8" | grep -q "command=.*\"\$(cat $DSP)\""; check "dispatch: the command carries the prompt in argv, never bare model+effort" $?
|
||||
printf '%s\n' "$d8" | grep -q "^paste=cd $ROOT/repo-a && claude .*\"\$(cat $DSP)\""; check "dispatch: paste= is the complete one-string form, prompt included" $?
|
||||
|
||||
# The cost comes from route.sh's row table and nowhere else, so --advisor is a
|
||||
# property of the ROW. These two differ ONLY in reversibility: both land on
|
||||
# row 3 (Opus 5/high), and only the costly one carries the advisor. A dispatch
|
||||
# that took --model/--effort directly would have no source for that flag at
|
||||
# all, and forwarding it blindly is how a session ends up looking peer-reviewed
|
||||
# without being.
|
||||
# The cost comes from route.sh's row table and nowhere else. These two differ
|
||||
# ONLY in reversibility, and both land on row 3 (Opus 5/high) - so since the
|
||||
# advisor rule was struck (2026-09-12) they must produce the IDENTICAL command.
|
||||
# costly is the case that used to add --advisor opus, which makes it the exact
|
||||
# input a reintroduced rule would fire on: this is the order's named red test,
|
||||
# not a sampled one.
|
||||
d9="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no \
|
||||
--path known --verification strong --reversibility cheap --scope multi-file --rationale t 2>/dev/null)"
|
||||
printf '%s\n' "$d9" | grep -q '^command=claude --model opus --effort high "'; check "dispatch: model+effort come from the route row (row 3, no advisor at cheap)" $?
|
||||
printf '%s\n' "$d9" | grep -q '^command=claude --model opus --effort high "'; check "dispatch: model+effort come from the route row (row 3)" $?
|
||||
d10="$("$BOARD" --roots "$ROOT" --dispatch --repo repo-a --prompt-file "$DSP" --target-pane no \
|
||||
--path known --verification strong --reversibility costly --scope local --rationale t 2>/dev/null)"
|
||||
printf '%s\n' "$d10" | grep -q '^command=claude --model opus --effort high --advisor opus "'; check "dispatch: the advisor flag follows the ROW (costly -> row 3 with advisor)" $?
|
||||
printf '%s\n' "$d10" | grep -q '^command=claude --model opus --effort high "'; check "dispatch: reversibility=costly emits NO advisor (rule struck 2026-09-12)" $?
|
||||
if printf '%s\n' "$d10" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "dispatch: no advisor anywhere in the costly dispatch block" "$rc"
|
||||
if printf '%s\n' "$d10 claude --advisor opus" | grep -q -- '--advisor'; then rc=0; else rc=1; fi
|
||||
check "control: that grep does find a planted advisor" "$rc"
|
||||
# The paste line is assembled separately from command=, so it is pinned
|
||||
# separately: a flag reintroduced in only one of the two is the shape that
|
||||
# sends the operator and the driver down different paths.
|
||||
if printf '%s\n' "$d10" | grep '^paste=' | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "dispatch: the paste line carries no advisor either" "$rc"
|
||||
printf '%s\n' "$d8" | grep -q '^next-cost=Sonnet 5/high$'; check "dispatch: the row's next-cost is reported alongside the command" $?
|
||||
|
||||
# --no-go stops the Go MESSAGE, not the work: the startup command - prompt in
|
||||
|
|
@ -1920,9 +1940,14 @@ for i in 1 2 3; do
|
|||
printf -- '---\nfrom: x\nto: ord-both\nsubject: m%s\n---\nbody\n' "$i" \
|
||||
> "$CLAUDE_COORD_DIR/ord-both/inbox/2026010${i}T000000Z-${i}-from-x.md"
|
||||
done
|
||||
# The PENDING filenames carry TODAY's stamp: since ORDRE 20260903T185736Z the
|
||||
# ORDRE age is read from the name, so a fixed 2026-01 name would render its real
|
||||
# delivery age here and this row asserts 2:0d. Claimed fixtures keep their fixed
|
||||
# names - FLY still reads the mtime.
|
||||
ORD_TODAY="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
for i in 1 2; do
|
||||
printf -- '---\nfrom: d\nto: ord-both\norder-id: o%s\nsubject: order %s\n---\nbody\n' "$i" "$i" \
|
||||
> "$CLAUDE_COORD_DIR/ord-both/orders/2026010${i}T000000Z-${i}-from-d.md"
|
||||
> "$CLAUDE_COORD_DIR/ord-both/orders/${ORD_TODAY}-${i}-from-d.md"
|
||||
done
|
||||
# A CLAIMED order is deliberately NOT counted: the column answers "what can a
|
||||
# session pick up here", and one already in flight cannot be. Counting it would
|
||||
|
|
@ -2122,7 +2147,7 @@ mkrepo "$RG_ROOT/repo-rationale-overrides"
|
|||
} > "$RG_ROOT/repo-rationale-overrides/STATE.md"
|
||||
|
||||
RG_PLAN="$("$BOARD" --roots "$RG_ROOT" --plan 2>/dev/null)"
|
||||
RG_CMD='^command=claude --model sonnet --effort high --advisor opus$'
|
||||
RG_CMD='^command=claude --model sonnet --effort high$'
|
||||
|
||||
printf '%s\n' "$RG_PLAN" | grep -A6 '^repo=repo-greedy-rationale$' | grep -q "$RG_CMD"
|
||||
check "route line whose rationale names all four traits still yields a command" $?
|
||||
|
|
@ -2192,8 +2217,9 @@ mkfly fly-inflight planned; mkorder fly-inflight claimed 20260101T000000Z-1-fro
|
|||
mkfly fly-empty planned
|
||||
# Both states at once, so the two counts are provably read from different dirs.
|
||||
mkfly fly-mixed planned
|
||||
mkorder fly-mixed pending 20260102T000000Z-2-from-d
|
||||
mkorder fly-mixed pending 20260103T000000Z-3-from-d
|
||||
FLY_TODAY="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkorder fly-mixed pending "${FLY_TODAY}-2-from-d"
|
||||
mkorder fly-mixed pending "${FLY_TODAY}-3-from-d"
|
||||
mkorder fly-mixed claimed 20260104T000000Z-4-from-d
|
||||
|
||||
FLY_OUT="$("$BOARD" --roots "$FLY_ROOT" 2>/dev/null)"
|
||||
|
|
@ -2221,7 +2247,7 @@ check "the legend denies that FLY proves a session is alive" $?
|
|||
# deferred repos were NOT free (2 held a pending order, 1 owed a reply, 1 had
|
||||
# an uncommitted tree). All four conditions are required.
|
||||
mkfly fly-free done
|
||||
mkfly fly-busy-order done; mkorder fly-busy-order pending 20260105T000000Z-5-from-d
|
||||
mkfly fly-busy-order done; mkorder fly-busy-order pending "${FLY_TODAY}-5-from-d"
|
||||
mkfly fly-busy-fly done; mkorder fly-busy-fly claimed 20260106T000000Z-6-from-d
|
||||
mkfly fly-busy-mail done
|
||||
mkdir -p "$CLAUDE_COORD_DIR/fly-busy-mail/inbox"
|
||||
|
|
@ -2576,9 +2602,17 @@ check "exit3: a nonzero that is not 3 is caught too, and names its own status" $
|
|||
# problem, and the newest reading is precisely what would hide it. This is not
|
||||
# a new mechanism in the file - ALDER (STATE.md mtime) and SISTE (last commit)
|
||||
# already read a clock the same way.
|
||||
#
|
||||
# Since ORDRE 20260903T185736Z the PENDING age is read from the FILENAME, so
|
||||
# these fixtures carry their intended age in the NAME rather than only in a
|
||||
# `touch -t`. The stems are computed once - the timestamp embeds HHMMSS, so
|
||||
# recomputing one a second later names a different file. FLY still reads the
|
||||
# mtime, which is why the claimed fixture keeps its touch.
|
||||
AGROOT="$(mktemp -d)"
|
||||
AGCOORD="$(mktemp -d)"
|
||||
AG_OLD="$(date -v-10d +%Y%m%d%H%M)"
|
||||
AG_OLD_TS="$(date -u -v-10d +%Y%m%dT%H%M%SZ)"
|
||||
AG_NEW_TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
|
||||
mk_ag_repo() {
|
||||
mkrepo "$AGROOT/$1"
|
||||
|
|
@ -2599,15 +2633,15 @@ mk_ag_order() { # $1=repo $2=subdir ("orders" or "orders/claimed") $3=file $4=t
|
|||
|
||||
# ag-old: one fresh order AND one ten days old. The whole point of the section.
|
||||
mk_ag_repo ag-old
|
||||
mk_ag_order ag-old orders 20260829T000000Z-fresh -
|
||||
mk_ag_order ag-old orders 20260818T000000Z-stale "$AG_OLD"
|
||||
mk_ag_order ag-old orders "${AG_NEW_TS}-fresh" -
|
||||
mk_ag_order ag-old orders "${AG_OLD_TS}-stale" "$AG_OLD"
|
||||
|
||||
# ag-fresh: KNOWN-POSITIVE CONTROL. Two orders, both fresh. If the assertion
|
||||
# below could not tell these two repos apart it would pass on anything - this
|
||||
# is the tree "where the age is wrong" that proves the check can fail.
|
||||
mk_ag_repo ag-fresh
|
||||
mk_ag_order ag-fresh orders 20260829T000001Z-a -
|
||||
mk_ag_order ag-fresh orders 20260829T000002Z-b -
|
||||
mk_ag_order ag-fresh orders "${AG_NEW_TS}-a" -
|
||||
mk_ag_order ag-fresh orders "${AG_NEW_TS}-b" -
|
||||
|
||||
# ag-none: no orders at all. "0 days old" and "no orders" are two different
|
||||
# facts and must not render the same - the same reason FLY exists next to ORDRE.
|
||||
|
|
@ -2617,7 +2651,7 @@ mk_ag_repo ag-none
|
|||
# that most needs this: nothing un-claims an order when the session dies, so a
|
||||
# claim with no age is the 117-hour claim with its only counter-evidence removed.
|
||||
mk_ag_repo ag-fly
|
||||
mk_ag_order ag-fly orders/claimed 20260818T000001Z-inflight "$AG_OLD"
|
||||
mk_ag_order ag-fly orders/claimed "${AG_OLD_TS}-inflight" "$AG_OLD"
|
||||
|
||||
AGOUT="$(CLAUDE_COORD_DIR="$AGCOORD" "$BOARD" --roots "$AGROOT" 2>/dev/null)"
|
||||
ag_row() { printf '%s' "$AGOUT" | grep "^$1 "; }
|
||||
|
|
@ -2625,9 +2659,15 @@ ag_row() { printf '%s' "$AGOUT" | grep "^$1 "; }
|
|||
# Ground truth for the fixture itself: the stale file really is ~10 days old.
|
||||
# Asserted before anything depends on it, so a broken `touch -t` fails here
|
||||
# rather than silently turning the checks below into a test of nothing.
|
||||
ag_gt="$(( ( $(date +%s) - $(stat -f %m "$AGCOORD/ag-old/orders/20260818T000000Z-stale.md") ) / 86400 ))"
|
||||
# Two ground truths, because the two columns now read two different sources.
|
||||
# ORDRE depends on the FILENAME, FLY on the mtime, and a fixture check that
|
||||
# guarded only one of them would leave the other assertion resting on nothing.
|
||||
ag_gt_name="$(( ( $(date -u +%s) - $(date -u -j -f %Y%m%d%H%M%S "$(printf '%s' "$AG_OLD_TS" | tr -dc '0-9')" +%s) ) / 86400 ))"
|
||||
[ "$ag_gt_name" -eq 10 ]
|
||||
check "orderage: ground truth - the stale fixture's FILENAME says 10 days" $?
|
||||
ag_gt="$(( ( $(date +%s) - $(stat -f %m "$AGCOORD/ag-fly/orders/claimed/${AG_OLD_TS}-inflight.md") ) / 86400 ))"
|
||||
[ "$ag_gt" -eq 10 ]
|
||||
check "orderage: ground truth - the fixture's stale order really is 10 days old" $?
|
||||
check "orderage: ground truth - the in-flight fixture's MTIME really is 10 days old" $?
|
||||
|
||||
ag_cells() { ag_row "$1" | cut -c89- ; }
|
||||
[ "$(ag_cells ag-old | awk '{print $1}')" = "2:10d" ]
|
||||
|
|
@ -2673,6 +2713,740 @@ 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
|
||||
# --- 30. ORDRE's pending age comes from the FILENAME, never the mtime -------
|
||||
# ORDRE 20260903T185736Z-1290610855 (.claude, 2026-09-03). Found by reading the
|
||||
# board right after this repo returned an order of its own: `--return` rewrites
|
||||
# the order file's mtime, and ORDRE read mtime, so a 1-day-old order rendered
|
||||
# `1:0d`. An order returned three times could never look old on the one surface
|
||||
# that exists so a repo nobody opens still shows something.
|
||||
#
|
||||
# PM decision - two questions, two sources. A PENDING order's age is "how long
|
||||
# has this sat with no owner" = now - DELIVERY time, which only the filename
|
||||
# carries and nothing rewrites. A CLAIMED order's age is "how long has it been
|
||||
# in flight", which is the claim's own mtime and is already right.
|
||||
PAROOT="$(mktemp -d)"
|
||||
PACOORD="$(mktemp -d)"
|
||||
pa_ts() { date -u -v-"$1"d +%Y%m%dT%H%M%SZ; }
|
||||
|
||||
mk_pa_repo() {
|
||||
mkrepo "$PAROOT/$1"
|
||||
{
|
||||
echo "# STATE - $1"
|
||||
echo ""
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "Et steg."
|
||||
} > "$PAROOT/$1/STATE.md"
|
||||
}
|
||||
mk_pa_order() { # $1=repo $2=subdir $3=full filename stem (carries the timestamp)
|
||||
mkdir -p "$PACOORD/$1/$2"
|
||||
printf -- '---\nfrom: x\nto: %s\n---\nDo a thing.\n' "$1" > "$PACOORD/$1/$2/$3.md"
|
||||
}
|
||||
|
||||
# The stems are computed ONCE and reused: pa_ts embeds HHMMSS, so recomputing
|
||||
# it a second later names a different file than the one that was created.
|
||||
pa_ret_stem="$(pa_ts 3)-1111111111-from-x"
|
||||
pa_fresh_stem="$(pa_ts 0)-2222222222-from-x"
|
||||
pa_fly_stem="$(pa_ts 9)-3333333333-from-x"
|
||||
|
||||
# pa-returned: filename says 3 days ago, mtime is NOW - exactly what a return
|
||||
# leaves behind. This is the defect, reproduced in a fixture.
|
||||
mk_pa_repo pa-returned
|
||||
mk_pa_order pa-returned orders "$pa_ret_stem"
|
||||
|
||||
# pa-fresh: KNOWN-POSITIVE CONTROL. Delivered today, so it must still read 0d -
|
||||
# without this, an assertion of "3d" would pass against a function that always
|
||||
# says 3.
|
||||
mk_pa_repo pa-fresh
|
||||
mk_pa_order pa-fresh orders "$pa_fresh_stem"
|
||||
|
||||
# pa-fly: an OLD filename claimed just now. FLY must stay on the mtime and read
|
||||
# 0d, or the fix has quietly changed the other question's answer too.
|
||||
mk_pa_repo pa-fly
|
||||
mk_pa_order pa-fly orders/claimed "$pa_fly_stem"
|
||||
|
||||
PAOUT="$(CLAUDE_COORD_DIR="$PACOORD" "$BOARD" --roots "$PAROOT" 2>/dev/null)"
|
||||
pa_cells() { printf '%s' "$PAOUT" | grep "^$1 " | cut -c89- ; }
|
||||
|
||||
# Ground truth for the fixture first: the mtime really is fresh, so a green
|
||||
# 30b cannot come from the file secretly being 3 days old on disk.
|
||||
pa_gt="$(( ( $(date +%s) - $(stat -f %m "$PACOORD/pa-returned/orders/$pa_ret_stem.md") ) / 86400 ))"
|
||||
[ "$pa_gt" -eq 0 ]
|
||||
check "pendingage: ground truth - the returned fixture's mtime is TODAY" $?
|
||||
|
||||
[ "$(pa_cells pa-returned | awk '{print $1}')" = "1:3d" ]
|
||||
check "pendingage: ORDRE reads the DELIVERY age from the filename, not the mtime" $?
|
||||
[ "$(pa_cells pa-returned | awk '{print $1}')" = "1:0d" ]; [ $? -ne 0 ]
|
||||
check "pendingage: a rewritten mtime can no longer reset a pending order's age" $?
|
||||
|
||||
[ "$(pa_cells pa-fresh | awk '{print $1}')" = "1:0d" ]
|
||||
check "pendingage: control - an order delivered today still reads 0d" $?
|
||||
|
||||
[ "$(pa_cells pa-fly | awk '{print $2}')" = "1:0d" ]
|
||||
check "pendingage: FLY still reads the claim's mtime - the other question is unchanged" $?
|
||||
[ "$(pa_cells pa-fly | awk '{print $2}')" = "1:9d" ]; [ $? -ne 0 ]
|
||||
check "pendingage: FLY did NOT switch to the filename with ORDRE" $?
|
||||
|
||||
# A name the grammar does not produce has no readable delivery time. "?" is the
|
||||
# whole reading, never a skipped file: an unmeasured order could be the oldest,
|
||||
# which is the same rule the mtime path already carried.
|
||||
mk_pa_repo pa-bad
|
||||
mk_pa_order pa-bad orders "not-a-timestamp"
|
||||
PAOUT2="$(CLAUDE_COORD_DIR="$PACOORD" "$BOARD" --roots "$PAROOT" 2>/dev/null)"
|
||||
[ "$(printf '%s' "$PAOUT2" | grep "^pa-bad " | cut -c89- | awk '{print $1}')" = "1:?" ]
|
||||
check "pendingage: an unparseable filename reads ?, never a fabricated 0" $?
|
||||
|
||||
/bin/rm -rf "$PAROOT" "$PACOORD" 2>/dev/null
|
||||
|
||||
# --- 31. Nested git repos with a STATE.md (admission a) ---------------------
|
||||
# Operator decision 2026-09-03, order 20260903T190201Z-238406410-from-.claude:
|
||||
# a git repo nested at depth 2 UNDER a directory that is itself a git repo
|
||||
# (claude-code-100x/*, from-ai-to-chitta/*) was invisible to the board - the
|
||||
# discovery loop adds a depth-1 repo and never looks inside it, and the
|
||||
# else-branch container scan is reached only when the depth-1 entry is NOT a
|
||||
# repo. Admission is criterion (a) and nothing wider: such a repo enters the
|
||||
# board ONLY when it carries a STATE.md. A nested repo without one stays
|
||||
# invisible ON PURPOSE - it does not even reach the UTEN STATE.md bucket,
|
||||
# because that bucket is for repos the operator opens, and these are vendored
|
||||
# or experimental checkouts nobody has declared a next step for. Measured on
|
||||
# the real tree the day the order was written: 12 nested repos, exactly 1 with
|
||||
# a STATE.md (from-ai-to-chitta/content-sadhguru).
|
||||
#
|
||||
# A dedicated add_nested_repos() beside add_dot_repos(), never a widening of
|
||||
# the `*` loops: the same argument ordre 20260818T124828Z made for dot repos.
|
||||
# Turning the depth-1 repo branch into a container scan would admit every
|
||||
# nested checkout, which is precisely what criterion (a) declines.
|
||||
NEST_ROOT="$(mktemp -d)"
|
||||
NEST_COORD="$(mktemp -d)"
|
||||
|
||||
mk_nest_state() {
|
||||
{
|
||||
echo "# STATE - $2"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=$3; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "Next step for $2."
|
||||
} > "$1/STATE.md"
|
||||
}
|
||||
|
||||
# A depth-1 repo holding THREE nested repos, exactly one with a STATE.md.
|
||||
mkrepo "$NEST_ROOT/parent-repo"
|
||||
mk_nest_state "$NEST_ROOT/parent-repo" parent-repo in-progress
|
||||
mkrepo "$NEST_ROOT/parent-repo/nest-with-state"
|
||||
mk_nest_state "$NEST_ROOT/parent-repo/nest-with-state" nest-with-state planned
|
||||
mkrepo "$NEST_ROOT/parent-repo/nest-no-state-1"
|
||||
mkrepo "$NEST_ROOT/parent-repo/nest-no-state-2"
|
||||
# Known-positive control: an ordinary depth-1 repo must still be found, so a
|
||||
# green section cannot come from a fix that broke the case that already worked.
|
||||
mkrepo "$NEST_ROOT/plain-repo"
|
||||
mk_nest_state "$NEST_ROOT/plain-repo" plain-repo planned
|
||||
# Known-negative control: a depth-1 repo whose nested repos carry NO STATE.md
|
||||
# must contribute exactly zero extra rows.
|
||||
mkrepo "$NEST_ROOT/barren-parent"
|
||||
mk_nest_state "$NEST_ROOT/barren-parent" barren-parent done
|
||||
mkrepo "$NEST_ROOT/barren-parent/nest-barren"
|
||||
|
||||
NEST_OUT="$(CLAUDE_COORD_DIR="$NEST_COORD" "$BOARD" --roots "$NEST_ROOT" 2>/dev/null)"
|
||||
|
||||
# Ground truth for the fixture itself, before anything depends on it: the
|
||||
# nested checkout really is a git repo and really does carry a STATE.md.
|
||||
[ -e "$NEST_ROOT/parent-repo/nest-with-state/.git" ] && \
|
||||
[ -f "$NEST_ROOT/parent-repo/nest-with-state/STATE.md" ]
|
||||
check "nested: ground truth - the fixture nested repo has .git AND STATE.md" $?
|
||||
[ -e "$NEST_ROOT/parent-repo/nest-no-state-1/.git" ] && \
|
||||
[ ! -f "$NEST_ROOT/parent-repo/nest-no-state-1/STATE.md" ]
|
||||
check "nested: ground truth - the excluded fixture has .git and NO STATE.md" $?
|
||||
|
||||
printf '%s' "$NEST_OUT" | grep -q 'parent-repo/nest-with-state'
|
||||
check "nested: a nested repo with STATE.md is on the board, keyed <parent>/<child>" $?
|
||||
printf '%s' "$NEST_OUT" | grep -q 'nest-no-state-1'; [ $? -ne 0 ]
|
||||
check "nested: a nested repo WITHOUT STATE.md stays invisible (criterion a)" $?
|
||||
printf '%s' "$NEST_OUT" | grep -q 'nest-no-state-2'; [ $? -ne 0 ]
|
||||
check "nested: the second STATE-less nested repo is invisible too" $?
|
||||
printf '%s' "$NEST_OUT" | grep -q 'nest-barren'; [ $? -ne 0 ]
|
||||
check "nested: known-negative - a parent with no STATE-carrying child adds 0 rows" $?
|
||||
printf '%s' "$NEST_OUT" | grep -q '^plain-repo '
|
||||
check "nested: known-positive - the ordinary depth-1 repo is still found" $?
|
||||
printf '%s' "$NEST_OUT" | grep -q '(4 repo'
|
||||
check "nested: exactly one row is added (4 = parent + plain + barren + nested)" $?
|
||||
|
||||
# The nested row is a repo like any other: its status comes from its OWN
|
||||
# STATE.md, not the parent's.
|
||||
printf '%s' "$NEST_OUT" | grep '^parent-repo/nest-with-state ' | grep -q 'planned'
|
||||
check "nested: the nested row reads its own STATE.md board line" $?
|
||||
printf '%s' "$NEST_OUT" | grep '^parent-repo ' | grep -q 'in-progress'
|
||||
check "nested: the parent keeps its own status (known-positive)" $?
|
||||
|
||||
# The display key is <parent>/<child>; the MAILBOX name is not. A mailbox is
|
||||
# addressed by basename(git toplevel) - coord-send.sh's own rule - so a repo
|
||||
# named parent-repo/nest-with-state on the board still counts its mail under
|
||||
# <coord>/nest-with-state. Reading the compound name against $COORD would find
|
||||
# no directory and print INN 0, which is a failed measurement wearing the
|
||||
# reassuring value - the one thing this file refuses everywhere.
|
||||
mkdir -p "$NEST_COORD/nest-with-state/inbox" "$NEST_COORD/nest-with-state/orders/claimed"
|
||||
: > "$NEST_COORD/nest-with-state/inbox/20260101T000000Z-1-from-x.md"
|
||||
: > "$NEST_COORD/nest-with-state/inbox/20260101T000000Z-2-from-x.md"
|
||||
: > "$NEST_COORD/nest-with-state/orders/$(date -u +%Y%m%dT%H%M%SZ)-3-from-x.md"
|
||||
: > "$NEST_COORD/nest-with-state/orders/claimed/20260101T000000Z-4-from-x.md"
|
||||
NEST_OUT2="$(CLAUDE_COORD_DIR="$NEST_COORD" "$BOARD" --roots "$NEST_ROOT" 2>/dev/null)"
|
||||
nest_cells="$(printf '%s' "$NEST_OUT2" | grep '^parent-repo/nest-with-state ' | cut -c89-)"
|
||||
# Exact values, never "not 0": before the fix the row did not exist at all, so
|
||||
# `awk '{print $1}'` on an empty string is "" - which is not "0" and would have
|
||||
# passed these two checks while measuring nothing. That is state-line-guard
|
||||
# section 11's vacuously-green defect, and it was observed here on the red run.
|
||||
[ -n "$nest_cells" ]
|
||||
check "nested: ground truth - the nested row exists before its cells are read" $?
|
||||
# A today-stamped filename, because a PENDING order's age is read from the
|
||||
# FILENAME, not the mtime (0.32.1) - a fixed 2026-01 stem would date this cell
|
||||
# to whenever the suite happens to run.
|
||||
[ "$(printf '%s' "$nest_cells" | awk '{print $1}')" = "1:0d" ]
|
||||
check "nested: ORDRE is counted under the mailbox BASENAME, not the compound key" $?
|
||||
[ "$(printf '%s' "$nest_cells" | awk '{print $2}')" = "1:0d" ]
|
||||
check "nested: FLY likewise reads <coord>/<basename>" $?
|
||||
printf '%s' "$NEST_OUT2" | grep '^parent-repo/nest-with-state ' | grep -q ' 2 '
|
||||
check "nested: INN reads the basename mailbox, never a fabricated 0" $?
|
||||
|
||||
# Depth 3 is NOT admitted: the order says depth 2 under a depth-1 repo, and a
|
||||
# repo nested inside a nested repo is a checkout of a checkout. Widening was
|
||||
# not ordered, and an unmeasured widening is the defect ordre 20260818 flagged.
|
||||
mkrepo "$NEST_ROOT/parent-repo/nest-with-state/deeper"
|
||||
mk_nest_state "$NEST_ROOT/parent-repo/nest-with-state/deeper" deeper planned
|
||||
NEST_OUT3="$(CLAUDE_COORD_DIR="$NEST_COORD" "$BOARD" --roots "$NEST_ROOT" 2>/dev/null)"
|
||||
printf '%s' "$NEST_OUT3" | grep -q 'deeper'; [ $? -ne 0 ]
|
||||
check "nested: depth 3 is not admitted (one level under a depth-1 repo only)" $?
|
||||
/bin/rm -rf "$NEST_ROOT/parent-repo/nest-with-state/deeper"
|
||||
|
||||
# A polyrepo container's children are UNCHANGED - their parent is not a repo,
|
||||
# so they keep their bare basename and never grow a <parent>/ prefix.
|
||||
mkdir -p "$NEST_ROOT/container-dir"
|
||||
mkrepo "$NEST_ROOT/container-dir/child-repo"
|
||||
mk_nest_state "$NEST_ROOT/container-dir/child-repo" child-repo planned
|
||||
NEST_OUT4="$(CLAUDE_COORD_DIR="$NEST_COORD" "$BOARD" --roots "$NEST_ROOT" 2>/dev/null)"
|
||||
printf '%s' "$NEST_OUT4" | grep -q '^child-repo '
|
||||
check "nested: a polyrepo container's child keeps its bare name (known-positive)" $?
|
||||
printf '%s' "$NEST_OUT4" | grep -q 'container-dir/child-repo'; [ $? -ne 0 ]
|
||||
check "nested: container children are NOT renamed to <parent>/<child>" $?
|
||||
|
||||
# Same rule under a dot-prefixed depth-1 repo: it is a depth-1 repo, so its
|
||||
# STATE-carrying nested repo is admitted on exactly the same criterion. The
|
||||
# asymmetry would otherwise be arbitrary - ordinary parents scanned, dot ones
|
||||
# not - and nothing in the order distinguishes them.
|
||||
mkrepo "$NEST_ROOT/.dotparent"
|
||||
mk_nest_state "$NEST_ROOT/.dotparent" .dotparent planned
|
||||
mkrepo "$NEST_ROOT/.dotparent/dotnest"
|
||||
mk_nest_state "$NEST_ROOT/.dotparent/dotnest" dotnest planned
|
||||
NEST_OUT5="$(CLAUDE_COORD_DIR="$NEST_COORD" "$BOARD" --roots "$NEST_ROOT" 2>/dev/null)"
|
||||
printf '%s' "$NEST_OUT5" | grep -q '\.dotparent/dotnest'
|
||||
check "nested: a dot-prefixed depth-1 repo's nested repo is admitted too" $?
|
||||
|
||||
/bin/rm -rf "$NEST_ROOT" "$NEST_COORD" 2>/dev/null
|
||||
|
||||
# --- 32. The scan reports its own denominator -------------------------------
|
||||
# Independent of section 31 and required regardless (the order says so in as
|
||||
# many words). Verifiseringsloven ansikt 4 applied to the board's own reading:
|
||||
# a repo count alone answers "how many were found" and says nothing about how
|
||||
# many directories were LOOKED AT, so a criterion that silently excludes 11 of
|
||||
# 12 nested repos is invisible at the surface that is supposed to show it.
|
||||
# The line names all four quantities, including the excluded ones.
|
||||
DEN_ROOT="$(mktemp -d)"
|
||||
DEN_COORD="$(mktemp -d)"
|
||||
mk_den_state() {
|
||||
{
|
||||
echo "# STATE - $2"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "Next step for $2."
|
||||
} > "$1/STATE.md"
|
||||
}
|
||||
# Depth-1 directories: 4 (two repos, one polyrepo container, one plain dir).
|
||||
mkrepo "$DEN_ROOT/d1-repo-a"
|
||||
mk_den_state "$DEN_ROOT/d1-repo-a" d1-repo-a
|
||||
mkrepo "$DEN_ROOT/d1-repo-b"
|
||||
mk_den_state "$DEN_ROOT/d1-repo-b" d1-repo-b
|
||||
mkdir -p "$DEN_ROOT/d1-container"
|
||||
mkrepo "$DEN_ROOT/d1-container/contained"
|
||||
mk_den_state "$DEN_ROOT/d1-container/contained" contained
|
||||
mkdir -p "$DEN_ROOT/d1-emptydir"
|
||||
# Nested repos: 3 under d1-repo-a, of which 2 carry a STATE.md.
|
||||
mkrepo "$DEN_ROOT/d1-repo-a/n1"; mk_den_state "$DEN_ROOT/d1-repo-a/n1" n1
|
||||
mkrepo "$DEN_ROOT/d1-repo-a/n2"; mk_den_state "$DEN_ROOT/d1-repo-a/n2" n2
|
||||
mkrepo "$DEN_ROOT/d1-repo-a/n3"
|
||||
|
||||
DEN_OUT="$(CLAUDE_COORD_DIR="$DEN_COORD" "$BOARD" --roots "$DEN_ROOT" 2>/dev/null)"
|
||||
DEN_LINE="$(printf '%s\n' "$DEN_OUT" | grep '^undersoekt:')"
|
||||
|
||||
[ -n "$DEN_LINE" ]
|
||||
check "denominator: the board prints an 'undersoekt:' line" $?
|
||||
printf '%s' "$DEN_LINE" | grep -q '4 katalog(er) depth 1'
|
||||
check "denominator: depth-1 directory count matches the fixture (4)" $?
|
||||
printf '%s' "$DEN_LINE" | grep -q '2 polyrepo-container(e)'
|
||||
check "denominator: container count matches the fixture (2: one with a repo, one empty)" $?
|
||||
printf '%s' "$DEN_LINE" | grep -q '3 nestede repo'
|
||||
check "denominator: nested-repo count matches the fixture (3)" $?
|
||||
printf '%s' "$DEN_LINE" | grep -q '2 med STATE.md tatt med'
|
||||
check "denominator: admitted-nested count matches the fixture (2 of 3)" $?
|
||||
|
||||
# The line must be a real reading, not a constant: change the tree and it moves.
|
||||
mkrepo "$DEN_ROOT/d1-repo-a/n4"; mk_den_state "$DEN_ROOT/d1-repo-a/n4" n4
|
||||
DEN_OUT2="$(CLAUDE_COORD_DIR="$DEN_COORD" "$BOARD" --roots "$DEN_ROOT" 2>/dev/null)"
|
||||
printf '%s\n' "$DEN_OUT2" | grep '^undersoekt:' | grep -q '4 nestede repo (3 med STATE.md tatt med)'
|
||||
check "denominator: the counts follow the tree, they are not hardcoded" $?
|
||||
|
||||
# A root that does not exist contributes nothing and must not silently inflate
|
||||
# the denominator - F10's rule, applied to the new counters.
|
||||
DEN_OUT3="$(CLAUDE_COORD_DIR="$DEN_COORD" "$BOARD" --roots "$DEN_ROOT,/nonexistent-root-$$" 2>/dev/null)"
|
||||
printf '%s\n' "$DEN_OUT3" | grep '^undersoekt:' | grep -q '4 katalog(er) depth 1'
|
||||
check "denominator: a missing scan root adds nothing to the counts" $?
|
||||
|
||||
/bin/rm -rf "$DEN_ROOT" "$DEN_COORD" 2>/dev/null
|
||||
|
||||
|
||||
# --- 33. --row <repo>: one machine-readable line per column -----------------
|
||||
# Order 20260912T202210Z-7588027378 (.claude, operator decision 2026-09-12).
|
||||
# The motivation is a measured misreading, not a convenience: on 11.09 the PM
|
||||
# read the FLY column off the table BY EYE and got it wrong. Every other
|
||||
# rendering that a program consumes emits `key=value` for exactly that reason -
|
||||
# the table's fixed-width columns are for a human, and a 34-character repo key
|
||||
# already shifts a row two characters right (a stated, unclosed gap). A repo
|
||||
# holding a value nobody can grep is a repo whose state is read by counting
|
||||
# spaces.
|
||||
#
|
||||
# It is a RENDERING of the scan every other view already ran, never a second
|
||||
# scan - the same rule --brief has carried since it shipped. Two numbers under
|
||||
# one name, computed twice, is the defect this file names most often.
|
||||
#
|
||||
# ONE DEPARTURE, stated rather than smuggled: `upushet=` is not a column the
|
||||
# scan computes, and it is measured here, once, for the named repo only. It is
|
||||
# in the order's field list, it is exactly the kind of fact that gets misread
|
||||
# ("nothing unpushed" vs "not measured"), and measuring it for one repo in a
|
||||
# one-repo rendering is not a scan. It never enters the table, the plan or the
|
||||
# briefing.
|
||||
ROW_ROOT="$(mktemp -d)"
|
||||
ROW_COORD="$(mktemp -d)"
|
||||
|
||||
mkrepo "$ROW_ROOT/row-repo"
|
||||
{
|
||||
echo "# STATE - row-repo"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=blocked; blocked-on=other-repo; next-cost=Opus 5/high -->"
|
||||
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=x -->"
|
||||
echo "The whole next step, well past the 38 characters the table column cuts at."
|
||||
} > "$ROW_ROOT/row-repo/STATE.md"
|
||||
|
||||
# Mailbox fixture: 3 pending messages, 2 pending orders, 1 claimed order. The
|
||||
# three counts are deliberately three DIFFERENT integers, and that is the whole
|
||||
# design of this fixture rather than an arbitrary choice. Built first with
|
||||
# 2/1/1, it was mutation-tested by making `fly` read the ORDRE field - the
|
||||
# exact 2026-09-11 misreading this rendering exists to close - and the check
|
||||
# stayed GREEN, because the two fields held the same digit. A fixture that
|
||||
# cannot tell two columns apart is the defect wearing a passing test, in the
|
||||
# section written to prevent it. With 3/2/1 that mutation turns it red.
|
||||
mkdir -p "$ROW_COORD/row-repo/inbox" "$ROW_COORD/row-repo/orders/claimed"
|
||||
echo "m" > "$ROW_COORD/row-repo/inbox/20260901T101010Z-1-from-x.md"
|
||||
echo "m" > "$ROW_COORD/row-repo/inbox/20260901T101011Z-2-from-x.md"
|
||||
echo "m" > "$ROW_COORD/row-repo/inbox/20260901T101014Z-5-from-x.md"
|
||||
echo "o" > "$ROW_COORD/row-repo/orders/20260901T101012Z-3-from-x.md"
|
||||
echo "o" > "$ROW_COORD/row-repo/orders/20260901T101015Z-6-from-x.md"
|
||||
echo "o" > "$ROW_COORD/row-repo/orders/claimed/20260901T101013Z-4-from-x.md"
|
||||
|
||||
ROW_OUT="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row row-repo 2>/dev/null)"; rc=$?
|
||||
[ "$rc" -eq 0 ]; check "row: a known repo exits 0" $?
|
||||
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^repo=row-repo$'; check "row: repo= is the board key" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q "^dir=$ROW_ROOT/row-repo\$"; check "row: dir= is the checkout path" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^status=blocked$'; check "row: status= is the bare token, not the blocked>target display" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^blocked-on=other-repo$';check "row: blocked-on= is its own field" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^next-cost=Opus 5/high$';check "row: next-cost= survives the space and the slash" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^inn=3$'; check "row: inn= counts pending messages" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^ordre=2$'; check "row: ordre= counts pending orders" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^fly=1$'; check "row: fly= counts claimed orders" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^siste='; check "row: siste= reports the last-commit reading" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^drt='; check "row: drt= reports the working-tree reading" $?
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^upushet='; check "row: upushet= is present" $?
|
||||
|
||||
# The three counts are the whole point: FLY was misread as ORDRE, so a rendering
|
||||
# that emitted the same digit under two names would reproduce the defect it
|
||||
# exists to close.
|
||||
[ "$(printf '%s\n' "$ROW_OUT" | grep -c '^\(inn\|ordre\|fly\)=')" -eq 3 ]
|
||||
check "row: inn, ordre and fly are three separate fields" $?
|
||||
|
||||
# NESTE uncut, same argument --brief has: the 38-char cut is the TABLE column's
|
||||
# property, and cutting here would make the cut string the only copy.
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^neste=.*38 characters the table column cuts at'
|
||||
check "row: neste= carries the whole line, not the table excerpt" $?
|
||||
|
||||
# `neste` is free prose and may contain anything the STATE.md author wrote, so
|
||||
# it is LAST - the same rule the RECORDS line itself obeys. A consumer reading
|
||||
# field N from the end depends on it.
|
||||
[ "$(printf '%s\n' "$ROW_OUT" | tail -1 | cut -d= -f1)" = "neste" ]
|
||||
check "row: neste= is the last line" $?
|
||||
|
||||
# One line per field, every line a field: a value that wrapped would be read as
|
||||
# a field name by anything grepping ^name=.
|
||||
ROW_BAD="$(printf '%s\n' "$ROW_OUT" | grep -v '^[a-z][a-z-]*=' || true)"
|
||||
[ -z "$ROW_BAD" ]; check "row: every line is a field=value line:${ROW_BAD:- clean}" $?
|
||||
|
||||
# ASCII, because the row is consumed by awk/sed/grep under bash 3.2 - the same
|
||||
# constraint every other emitted block in this file carries. Asserted on a
|
||||
# fixture whose own prose is ASCII, so this measures the renderer, not the
|
||||
# STATE.md it happened to read.
|
||||
if printf '%s' "$ROW_OUT" | LC_ALL=C grep -q '[^ -~]'; then rc=1; else rc=0; fi
|
||||
check "row: the output is ASCII" "$rc"
|
||||
if printf '%s' "$ROW_OUT" | LC_ALL=C grep -q '[^ -~]x'; then rc=1; else rc=0; fi
|
||||
check "control: that ASCII grep runs at all (no match on a clean string)" "$rc"
|
||||
|
||||
# A repo whose NESTE is NOT ASCII still emits one line per field. The renderer
|
||||
# cannot make the operator's Norwegian prose ASCII and must never try - the
|
||||
# field NAMES are the machine surface, the value is whatever was written.
|
||||
mkrepo "$ROW_ROOT/row-utf8"
|
||||
{
|
||||
echo "# STATE - row-utf8"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
printf 'M\xc3\x85LT i dag: ingenting gjenst\xc3\xa5r.\n'
|
||||
} > "$ROW_ROOT/row-utf8/STATE.md"
|
||||
ROW_U="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row row-utf8 2>/dev/null)"
|
||||
ROW_UBAD="$(printf '%s\n' "$ROW_U" | grep -v '^[a-z][a-z-]*=' || true)"
|
||||
[ -z "$ROW_UBAD" ]; check "row: non-ASCII NESTE still yields one field per line" $?
|
||||
printf '%s\n' "$ROW_U" | grep -q '^neste=.*ingenting'
|
||||
check "row: non-ASCII NESTE is carried through, never mangled or dropped" $?
|
||||
|
||||
# An unknown repo must REFUSE. Emitting an empty block would be the same defect
|
||||
# as a bare `command=`: a consumer reading ^status= would get nothing back and
|
||||
# read it as a repo with no status, which is a real and different state ("?").
|
||||
ROW_ERR="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row no-such-repo 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ]; check "row: an unknown repo exits 2" $?
|
||||
printf '%s\n' "$ROW_ERR" | grep -q 'no-such-repo'
|
||||
check "row: the refusal names the repo it could not find" $?
|
||||
ROW_ERR_OUT="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row no-such-repo 2>/dev/null)"
|
||||
[ -z "$ROW_ERR_OUT" ]; check "row: a refusal writes nothing at all to stdout" $?
|
||||
|
||||
# A missing value is a usage error, not a silently empty row.
|
||||
CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row >/dev/null 2>&1
|
||||
[ $? -eq 2 ]; check "row: --row with no value exits 2" $?
|
||||
|
||||
# Read-only, asserted rather than assumed: this is the one property every
|
||||
# rendering in this file shares and the one a new rendering is most likely to
|
||||
# break.
|
||||
ROW_BEFORE="$(find "$ROW_ROOT" "$ROW_COORD" -type f | sort | while read -r f; do printf '%s %s\n' "$f" "$(stat -f %m "$f")"; done)"
|
||||
CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row row-repo >/dev/null 2>&1
|
||||
ROW_AFTER="$(find "$ROW_ROOT" "$ROW_COORD" -type f | sort | while read -r f; do printf '%s %s\n' "$f" "$(stat -f %m "$f")"; done)"
|
||||
[ "$ROW_BEFORE" = "$ROW_AFTER" ]; check "row: writes nothing - no file added, removed or touched" $?
|
||||
|
||||
# upushet: the three states must stay distinguishable. This fixture has no
|
||||
# upstream at all, so the honest answer is "not measured", never 0 - the same
|
||||
# rule DRT's ? and coord-count's exit 3 carry. A 0 here would say "nothing is
|
||||
# waiting to be pushed" about a repo that has never had a remote.
|
||||
printf '%s\n' "$ROW_OUT" | grep -q '^upushet=?$'
|
||||
check "row: a repo with no upstream reports upushet=?, never 0" $?
|
||||
|
||||
# Known-positive control, and the case the field exists for: a real upstream
|
||||
# with a real unpushed commit must report the count. Without this the ? above
|
||||
# would pass on a field that can only ever say ?.
|
||||
ROW_UP="$ROW_ROOT/row-upstream"
|
||||
mkrepo "$ROW_UP"
|
||||
{
|
||||
echo "# STATE - row-upstream"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "Next step."
|
||||
} > "$ROW_UP/STATE.md"
|
||||
git -C "$ROW_UP" add -A >/dev/null 2>&1
|
||||
git -C "$ROW_UP" commit -qm "state" >/dev/null 2>&1
|
||||
ROW_BARE="$(mktemp -d)/origin.git"
|
||||
git init -q --bare "$ROW_BARE" >/dev/null 2>&1
|
||||
git -C "$ROW_UP" remote add origin "$ROW_BARE" >/dev/null 2>&1
|
||||
git -C "$ROW_UP" push -q -u origin HEAD >/dev/null 2>&1
|
||||
ROW_U0="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row row-upstream 2>/dev/null)"
|
||||
printf '%s\n' "$ROW_U0" | grep -q '^upushet=0$'
|
||||
check "row: control - an upstream with everything pushed reports upushet=0" $?
|
||||
echo "more" > "$ROW_UP/extra.txt"
|
||||
git -C "$ROW_UP" add -A >/dev/null 2>&1
|
||||
git -C "$ROW_UP" commit -qm "unpushed work" >/dev/null 2>&1
|
||||
ROW_U1="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" --row row-upstream 2>/dev/null)"
|
||||
printf '%s\n' "$ROW_U1" | grep -q '^upushet=1$'
|
||||
check "row: one unpushed commit reports upushet=1" $?
|
||||
|
||||
# The table is UNCHANGED by all of this. --row is a rendering, so a field that
|
||||
# only exists there must not leak into the view a human reads.
|
||||
ROW_TABLE="$(CLAUDE_COORD_DIR="$ROW_COORD" "$BOARD" --roots "$ROW_ROOT" 2>/dev/null)"
|
||||
if printf '%s' "$ROW_TABLE" | grep -qi 'upushet'; then rc=1; else rc=0; fi
|
||||
check "row: upushet does not leak into the table" "$rc"
|
||||
|
||||
# Structural, in the shape this file already uses for "no write path exists":
|
||||
# --row must not run its own repo discovery. A second scan is the defect the
|
||||
# rendering rule exists to prevent, and a behavioural test cannot see it.
|
||||
grep -q -- '--row' "$BOARD"; check "row: board.sh actually carries the --row flag" $?
|
||||
|
||||
/bin/rm -rf "$ROW_ROOT" "$ROW_COORD" 2>/dev/null
|
||||
|
||||
|
||||
echo ""
|
||||
echo "board-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
639
scripts/board.sh
639
scripts/board.sh
|
|
@ -80,8 +80,9 @@
|
|||
# the exact command to start a session there. The 38-char cut is the table
|
||||
# column's property, not the record's, so the briefing prints NESTE uncut. Each
|
||||
# command is derived by CALLING route.sh with that repo's own four traits -
|
||||
# next-cost alone cannot yield it, since the advisor flag is a property of the
|
||||
# ROW. A repo with no route line is told so rather than handed a guess.
|
||||
# next-cost alone cannot yield it, since it names the row rather than spelling
|
||||
# the command, and there must be exactly one spelling in circulation. A repo
|
||||
# with no route line is told so rather than handed a guess.
|
||||
#
|
||||
# --brief is still read-only: it writes nothing. The file write lives in
|
||||
# brief-nightly.sh, which renders to a temp file and renames it into place, and
|
||||
|
|
@ -200,14 +201,24 @@
|
|||
# --path P --verification V --reversibility R --scope S
|
||||
# --rationale "why"
|
||||
#
|
||||
# Model, effort and --advisor come from route.sh's row table, never from a
|
||||
# --model/--effort pair: the advisor flag is a property of the ROW, and a
|
||||
# dispatch that took the model directly would have no honest source for it.
|
||||
# Model and effort come from route.sh's row table, never from a --model/--effort
|
||||
# pair: the rubric has exactly one copy, and a dispatch that took the model
|
||||
# directly would be a second, unscored way to reach the same decision.
|
||||
# --target-pane is a MEASUREMENT (morning --probe-panes) and has no default -
|
||||
# see the dispatch() comment for what happens when it is guessed.
|
||||
#
|
||||
# --row <repo> is a SEVENTH rendering: the table's row for ONE repo, as
|
||||
# `field=value` lines a program can grep instead of columns a human counts
|
||||
# spaces in. It exists because the columns WERE misread - FLY read as ORDRE on
|
||||
# 2026-09-11 - and because every other rendering a program consumes is already
|
||||
# key=value. It takes no position: every field is a value the scan already
|
||||
# holds, under the name its column carries, with `neste` last and uncut.
|
||||
# One exception, stated rather than blended in: `upushet` is not a column, and
|
||||
# is measured once for the named repo only. See row_one() for what it can and
|
||||
# cannot claim.
|
||||
#
|
||||
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain]
|
||||
# [--brief|--plan|--inbox-plan|--dispatch ...]
|
||||
# [--brief|--plan|--inbox-plan|--dispatch|--voyage|--row <repo>]
|
||||
# [--focus "<prose>"]
|
||||
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
|
||||
# BOARD_ROOTS overrides the default scan roots.
|
||||
|
|
@ -223,6 +234,8 @@ PLAN=0
|
|||
INBOX_PLAN=0
|
||||
FOCUS=""
|
||||
DISPATCH=0
|
||||
VOYAGE=0
|
||||
ROW=0; ROW_REPO=""
|
||||
D_REPO=""; D_PROMPT=""; D_PANE=""; D_ORDER=""
|
||||
D_PATH=""; D_VER=""; D_REV=""; D_SCOPE=""; D_RAT=""
|
||||
|
||||
|
|
@ -237,10 +250,16 @@ while [ $# -gt 0 ]; do
|
|||
# bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever.
|
||||
--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 ;;
|
||||
# Renderings of one scan, so exactly one may be selected: last wins.
|
||||
--brief) BRIEF=1; PLAN=0; INBOX_PLAN=0; VOYAGE=0; ROW=0; shift ;;
|
||||
--plan) PLAN=1; BRIEF=0; INBOX_PLAN=0; VOYAGE=0; ROW=0; shift ;;
|
||||
--inbox-plan) INBOX_PLAN=1; BRIEF=0; PLAN=0; VOYAGE=0; ROW=0; shift ;;
|
||||
# One repo, one field=value line per column. Same `shift 2` guard as every
|
||||
# other value flag, for the same bash 3.2 reason - and a MISSING value is a
|
||||
# usage error rather than an empty row, since an empty row reads exactly
|
||||
# like a repo whose every column is blank.
|
||||
--row) [ $# -ge 2 ] || { echo "board: --row requires a repo name" >&2; exit 2; }
|
||||
ROW=1; ROW_REPO="$2"; BRIEF=0; PLAN=0; INBOX_PLAN=0; VOYAGE=0; DISPATCH=0; shift 2 ;;
|
||||
# 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 +267,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; ROW=0; shift ;;
|
||||
--voyage) VOYAGE=1; BRIEF=0; PLAN=0; INBOX_PLAN=0; DISPATCH=0; ROW=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; }
|
||||
|
|
@ -282,13 +302,67 @@ trunc() { printf '%s' "$1" | LC_ALL=en_US.UTF-8 cut -c1-"$2"; }
|
|||
# --- Discovery: git repos at depth 1, plus depth 2 under polyrepo dirs ------
|
||||
# A directory that is itself a git repo is one repo; a directory that is not
|
||||
# but contains git repos is a polyrepo container (the plugin marketplace) and
|
||||
# contributes its children, never itself.
|
||||
# contributes its children, never itself. A repo nested under a depth-1 REPO is
|
||||
# a third case, admitted on criterion (a) only - see add_nested_repos() below.
|
||||
#
|
||||
# "Is a repo" tests .git with -e, not -d: a worktree or submodule has .git as a
|
||||
# FILE. A plain `git worktree add <root>/feature-x` lands a depth-1 sibling that
|
||||
# can CARRY its own STATE.md - a -d test drops it silently. Kept identical in
|
||||
# the rollup builder (catalog) on purpose: two readers, one name.
|
||||
REPOS=""
|
||||
# Dirs admitted by add_nested_repos(), recorded rather than re-derived. The
|
||||
# record loop needs to know which entries are nested so it can build the
|
||||
# <parent>/<child> key, and the obvious re-derivation ("is my parent dir a git
|
||||
# repo?") answers a slightly different question: it would also fire for every
|
||||
# depth-1 repo if a SCAN ROOT ever happened to be a git checkout. Discovery
|
||||
# already knows the answer exactly; storing it cannot drift from the criterion.
|
||||
NESTED_LIST=""
|
||||
|
||||
# --- Denominator counters (order 20260903T190201Z-238406410, .claude) -------
|
||||
# A repo COUNT alone answers "how many were found" and is silent about how many
|
||||
# directories were looked at, so an admission criterion that excludes 11 of 12
|
||||
# nested repos is invisible at the very surface meant to show it - ansikt 4
|
||||
# turned on the board's own reading. These four are printed as one line under
|
||||
# the table (see UNDERSOEKT below) and are independent of the nested-repo
|
||||
# admission itself: the line goes in whether or not anything is admitted.
|
||||
N_DEPTH1=0 # directories examined at depth 1, dot-entries included
|
||||
N_CONTAINER=0 # of those, entered as polyrepo containers (not themselves repos)
|
||||
N_NESTED=0 # git repos found one level under a depth-1 repo
|
||||
N_NESTED_IN=0 # of those, admitted because they carry a STATE.md
|
||||
|
||||
# Admission criterion (a), operator decision 2026-09-03: a git repo nested
|
||||
# under a directory that is ITSELF a git repo enters the board only when it
|
||||
# carries a STATE.md. Measured on the real tree that day: 12 nested repos,
|
||||
# exactly 1 with a STATE.md (from-ai-to-chitta/content-sadhguru). The other 11
|
||||
# are vendored or experimental checkouts with no declared next step, and they
|
||||
# stay invisible ON PURPOSE - they do not even reach the UTEN STATE.md bucket,
|
||||
# which is for repos the operator opens.
|
||||
#
|
||||
# A dedicated function beside add_dot_repos(), never a widening of the `*`
|
||||
# loops below, for the identical reason ordre 20260818T124828Z gave for dot
|
||||
# repos: routing a depth-1 REPO into the else-branch container scan would admit
|
||||
# every nested checkout, which is exactly what criterion (a) declines. One
|
||||
# level only - a repo nested inside a nested repo is a checkout of a checkout,
|
||||
# and widening past what was ordered is the unmeasured scope creep that order
|
||||
# flagged.
|
||||
#
|
||||
# Bounded gap, stated rather than closed: a DOT-prefixed nested repo is not
|
||||
# looked for (the `*` glob here has no dotglob, same as everywhere else in this
|
||||
# file). The combination was not measured and not ordered; add_dot_repos exists
|
||||
# for the depth-1 and container cases because those were.
|
||||
add_nested_repos() {
|
||||
for nested in "$1"/*; do
|
||||
[ -d "$nested" ] || continue
|
||||
[ -e "$nested/.git" ] || continue
|
||||
N_NESTED=$((N_NESTED + 1))
|
||||
[ -f "$nested/STATE.md" ] || continue
|
||||
N_NESTED_IN=$((N_NESTED_IN + 1))
|
||||
REPOS="$REPOS
|
||||
$nested"
|
||||
NESTED_LIST="$NESTED_LIST
|
||||
$nested"
|
||||
done
|
||||
}
|
||||
|
||||
# Picks up dot-prefixed git repos directly under $1 - the Forgejo/GitHub
|
||||
# `.profile` org-profile convention, measured invisible to the loops below
|
||||
|
|
@ -302,16 +376,29 @@ REPOS=""
|
|||
# a bare `*` needs dotglob to see them), so no shopt toggle is needed here.
|
||||
# Only a dot-entry that IS itself a repo is ever added; one that is not is
|
||||
# silently skipped, never recursed into as a container.
|
||||
#
|
||||
# $2 = "d1" when this is the depth-1 scan of a root. A dot-prefixed depth-1
|
||||
# repo IS a depth-1 repo, so it gets the same nested scan an ordinary one gets
|
||||
# - the asymmetry would otherwise be arbitrary, and nothing in the admission
|
||||
# criterion distinguishes them. Under a polyrepo container the flag is absent:
|
||||
# those entries are already at depth 2, and their children would be depth 3.
|
||||
add_dot_repos() {
|
||||
for dotentry in "$1"/.*; do
|
||||
dotbase="$(basename "$dotentry")"
|
||||
[ "$dotbase" = "." ] && continue
|
||||
[ "$dotbase" = ".." ] && continue
|
||||
[ -d "$dotentry" ] || continue
|
||||
# Counted as a depth-1 directory that was LOOKED AT even when it is not a
|
||||
# repo: the denominator reports what was examined, not what was admitted.
|
||||
# It is deliberately never counted as a polyrepo container - a dot-dir that
|
||||
# is not a repo is skipped outright, which is ordre 20260818's decision.
|
||||
[ "${2:-}" = "d1" ] && N_DEPTH1=$((N_DEPTH1 + 1))
|
||||
[ -e "$dotentry/.git" ] || continue
|
||||
REPOS="$REPOS
|
||||
$dotentry"
|
||||
[ "${2:-}" = "d1" ] && add_nested_repos "$dotentry"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Split on comma via IFS + `set --` rather than an unquoted $(...) expansion:
|
||||
|
|
@ -334,13 +421,20 @@ for root in "$@"; do
|
|||
continue
|
||||
fi
|
||||
ROOTS_OK=$((ROOTS_OK + 1))
|
||||
add_dot_repos "$root"
|
||||
add_dot_repos "$root" d1
|
||||
for entry in "$root"/*; do
|
||||
[ -d "$entry" ] || continue
|
||||
N_DEPTH1=$((N_DEPTH1 + 1))
|
||||
if [ -e "$entry/.git" ]; then
|
||||
REPOS="$REPOS
|
||||
$entry"
|
||||
# A depth-1 repo used to end the walk here: the else-branch below is the
|
||||
# only place children are ever looked at, and it is unreachable for an
|
||||
# entry that is itself a repo. That is what made every nested repo
|
||||
# invisible.
|
||||
add_nested_repos "$entry"
|
||||
else
|
||||
N_CONTAINER=$((N_CONTAINER + 1))
|
||||
for child in "$entry"/*; do
|
||||
[ -e "$child/.git" ] || continue
|
||||
REPOS="$REPOS
|
||||
|
|
@ -403,9 +497,232 @@ oldest_order_age() {
|
|||
printf '%dd' $(( (NOW - ooa_oldest) / 86400 ))
|
||||
}
|
||||
|
||||
|
||||
# The PENDING age is read from the FILENAME's timestamp, never the mtime.
|
||||
# ORDRE 20260903T185736Z-1290610855: `coord-order-done --return` rewrites the
|
||||
# order file's mtime, and this column read mtime, so an order returned three
|
||||
# times could never look old - on the one surface that exists precisely so a
|
||||
# repo nobody opens still shows something. Measured on the live queue the day
|
||||
# the order was written: a file whose name said 2026-09-02 rendered `1:0d`.
|
||||
#
|
||||
# The filename is written once, at delivery, and nothing rewrites it, which is
|
||||
# exactly the fact "how long has this sat with no owner" is asking about.
|
||||
#
|
||||
# FLY keeps oldest_order_age above, and that is not an oversight: a claimed
|
||||
# order's age is "how long has it been in flight", which is when the CLAIM
|
||||
# happened - the mtime - and switching it to the filename would answer the
|
||||
# delivery question in a column that asks the flight question.
|
||||
#
|
||||
# Same three outcomes as oldest_order_age, for the same reasons: "-" empty,
|
||||
# "?" unmeasured (a name the grammar does not produce has no readable delivery
|
||||
# time, and an unmeasured file could be the oldest one), "Nd" measured.
|
||||
oldest_pending_age() {
|
||||
opa_oldest=""; opa_seen=0; opa_unmeasured=0
|
||||
for opa_f in "$1"/*.md; do
|
||||
[ -f "$opa_f" ] || continue
|
||||
opa_seen=1
|
||||
opa_ts="$(basename "$opa_f")"; opa_ts="${opa_ts%%-*}"
|
||||
case "$opa_ts" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z)
|
||||
opa_n="$(printf '%s' "$opa_ts" | tr -dc '0-9')"
|
||||
opa_e="$(date -u -j -f %Y%m%d%H%M%S "$opa_n" +%s 2>/dev/null)"
|
||||
case "$opa_e" in
|
||||
''|*[!0-9]*) opa_unmeasured=1 ;;
|
||||
*) if [ -z "$opa_oldest" ] || [ "$opa_e" -lt "$opa_oldest" ]; then
|
||||
opa_oldest="$opa_e"
|
||||
fi ;;
|
||||
esac ;;
|
||||
*) opa_unmeasured=1 ;;
|
||||
esac
|
||||
done
|
||||
if [ "$opa_seen" -eq 0 ]; then printf -- '-'; return 0; fi
|
||||
if [ "$opa_unmeasured" -eq 1 ] || [ -z "$opa_oldest" ]; then printf '?'; return 0; fi
|
||||
printf '%dd' $(( (NOW - opa_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")"
|
||||
# Two names, deliberately: the KEY the board shows and the MAILBOX the coord
|
||||
# columns read. For every repo but a nested one they are the same string.
|
||||
#
|
||||
# A nested repo is keyed <parent>/<child> (operator decision 2026-09-03): the
|
||||
# bare basename would render a row that says nothing about where the checkout
|
||||
# lives, and two repos of the same name - one depth-1, one nested - would
|
||||
# collide into indistinguishable rows.
|
||||
#
|
||||
# The mailbox name is NOT that key. A mailbox is addressed by
|
||||
# basename(git toplevel), which is coord-send.sh's own rule and the only name
|
||||
# a session in that repo can hold; reading $COORD/<parent>/<child>/inbox would
|
||||
# find no directory and print INN 0 - a failed measurement wearing the
|
||||
# reassuring value, which is the one thing this file refuses everywhere.
|
||||
mbox="$(basename "$d")"
|
||||
name="$mbox"
|
||||
if printf '%s\n' "$NESTED_LIST" | grep -qxF "$d"; then
|
||||
name="$(basename "$(dirname "$d")")/$mbox"
|
||||
fi
|
||||
state="$d/STATE.md"
|
||||
|
||||
# F6: `git status | wc -l` yields 0 lines whether the tree is CLEAN or git
|
||||
|
|
@ -432,8 +749,8 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
fi
|
||||
|
||||
inbox=0
|
||||
if [ -d "$COORD/$name/inbox" ]; then
|
||||
inbox="$(ls "$COORD/$name/inbox"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
||||
if [ -d "$COORD/$mbox/inbox" ]; then
|
||||
inbox="$(ls "$COORD/$mbox/inbox"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
||||
[ -n "$inbox" ] || inbox=0
|
||||
fi
|
||||
|
||||
|
|
@ -449,8 +766,8 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
# as mail to such a name is invisible in INN. coord-count.sh is the cross-
|
||||
# check for the mail half; there is deliberately no join built for orders.
|
||||
orders=0
|
||||
if [ -d "$COORD/$name/orders" ]; then
|
||||
orders="$(ls "$COORD/$name/orders"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
||||
if [ -d "$COORD/$mbox/orders" ]; then
|
||||
orders="$(ls "$COORD/$mbox/orders"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
||||
[ -n "$orders" ] || orders=0
|
||||
fi
|
||||
|
||||
|
|
@ -473,8 +790,8 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
# read as "a session is running here" would be the process axis smuggled in
|
||||
# as a file count.
|
||||
claimed=0
|
||||
if [ -d "$COORD/$name/orders/claimed" ]; then
|
||||
claimed="$(ls "$COORD/$name/orders/claimed"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
||||
if [ -d "$COORD/$mbox/orders/claimed" ]; then
|
||||
claimed="$(ls "$COORD/$mbox/orders/claimed"/*.md 2>/dev/null | wc -l | tr -d ' ')"
|
||||
[ -n "$claimed" ] || claimed=0
|
||||
fi
|
||||
|
||||
|
|
@ -488,9 +805,9 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
# measured 2026-08-23), so a claim without an age is that non-claim with its
|
||||
# only counter-evidence removed.
|
||||
ordersage="-"
|
||||
[ "$orders" -eq 0 ] || ordersage="$(oldest_order_age "$COORD/$name/orders")"
|
||||
[ "$orders" -eq 0 ] || ordersage="$(oldest_pending_age "$COORD/$mbox/orders")"
|
||||
claimedage="-"
|
||||
[ "$claimed" -eq 0 ] || claimedage="$(oldest_order_age "$COORD/$name/orders/claimed")"
|
||||
[ "$claimed" -eq 0 ] || claimedage="$(oldest_order_age "$COORD/$mbox/orders/claimed")"
|
||||
|
||||
# Read for EVERY repo, not just the STATE-less ones: a repo whose plan file
|
||||
# is fresh can still have been silent for a year, and that is precisely the
|
||||
|
|
@ -598,9 +915,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,28 +1078,28 @@ 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
|
||||
}
|
||||
|
||||
# --- Briefing rendering (--brief) ------------------------------------------
|
||||
# The startup command for one repo, derived from that repo's OWN route line by
|
||||
# calling route.sh. Deriving it from next-cost instead would not work even in
|
||||
# principle: the advisor flag is a property of the ROW, and two rows can share
|
||||
# a model/effort pair while differing on it. A repo with no route line gets its
|
||||
# calling route.sh. Deriving it from next-cost instead would mean a second
|
||||
# writer for the same command string, spelled from a rubric NAME rather than
|
||||
# from the row table itself. A repo with no route line gets its
|
||||
# next-cost printed and is told where the command comes from - a guessed
|
||||
# command would read as authoritative while being a guess, which is worse than
|
||||
# no command at all.
|
||||
|
|
@ -960,7 +1281,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
|
||||
|
|
@ -1000,13 +1321,18 @@ fi
|
|||
# brief-nightly.sh already owns the briefing's write.
|
||||
#
|
||||
# THE COST COMES FROM route.sh's ROW TABLE, never from a --model/--effort pair
|
||||
# handed in. That is not tidiness: `--advisor opus` is a property of the ROW,
|
||||
# two rows share a model/effort pair while differing on it, and the CLI accepts
|
||||
# a wrong advisor silently (it drops it for a Fable main model without failing).
|
||||
# A dispatch that took the model directly would have no source for that flag,
|
||||
# and both available guesses are wrong in the same direction - a session that
|
||||
# looks peer-reviewed without being. Scoring the four traits is the caller's
|
||||
# judgement; turning them into a command is this lookup.
|
||||
# handed in. That is not tidiness: the rubric is a policy with exactly one copy,
|
||||
# and a dispatch accepting the model directly would be a second way to reach the
|
||||
# same decision - one that records no traits, no rationale and no next-cost, so
|
||||
# nothing afterwards can say whether the ROUTING was wrong or the SCORING was.
|
||||
# Scoring the four traits is the caller's judgement; turning them into a command
|
||||
# is this lookup.
|
||||
#
|
||||
# Until 2026-09-12 this paragraph rested on `--advisor opus` instead: the flag
|
||||
# was a property of the ROW, so a model/effort pair could not yield it. That
|
||||
# rule is struck (see route.sh), and the argument is rewritten rather than
|
||||
# quietly left standing on a premise the code no longer has - a comment
|
||||
# defending a mechanism that was removed is how the next session "restores" it.
|
||||
dispatch() {
|
||||
# --target-pane is REQUIRED and has no default, the same rule route.sh's
|
||||
# --last-effort carries: it is a MEASUREMENT of the world (does the target
|
||||
|
|
@ -1084,8 +1410,8 @@ dispatch() {
|
|||
[ -d "$d_dir" ] || { echo "board: the directory for $D_REPO does not exist: $d_dir" >&2; exit 2; }
|
||||
|
||||
# Rejected traits REFUSE. Degrading to a command without them is the worst
|
||||
# available outcome: a command missing --advisor is indistinguishable from a
|
||||
# row that legitimately has none.
|
||||
# available outcome: a command built on three scored traits and one silently
|
||||
# dropped one is indistinguishable from a fully scored decision.
|
||||
d_route="$(bash "$ROUTE" --path "$D_PATH" --verification "$D_VER" \
|
||||
--reversibility "$D_REV" --scope "$D_SCOPE" --rationale "$D_RAT" 2>&1)" || {
|
||||
echo "board: route.sh rejected the traits, so there is no command to give:" >&2
|
||||
|
|
@ -1564,7 +1890,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 +1950,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 +2017,217 @@ 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=""
|
||||
# basename($vy_dir), never $vy_name: a nested repo's board key is
|
||||
# <parent>/<child> while its mailbox is addressed by the bare basename
|
||||
# (see the record loop's own note). Reading the compound key here would
|
||||
# find no directory and report ordre=0 for a repo that has orders.
|
||||
for vy_of in "$COORD/$(basename "$vy_dir")/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
|
||||
|
||||
# --- Row rendering (--row <repo>) -------------------------------------------
|
||||
# A SEVENTH rendering of the same scan, never a second scan, and read-only like
|
||||
# every other one. (Order 20260912T202210Z-7588027378 calls it the sixth; by
|
||||
# this file's own numbering --inbox-plan is the fourth and --dispatch the fifth,
|
||||
# so it is the seventh. The count is corrected here rather than carried wrong,
|
||||
# for the same reason every other number in this file is measured.)
|
||||
#
|
||||
# WHAT IT ANSWERS: the table's row for ONE repo, as `field=value` lines a
|
||||
# program can grep. The table is fixed-width and is for a human; reading a
|
||||
# column off it by eye is how FLY was misread as ORDRE on 2026-09-11, and a
|
||||
# 34-character repo key already shifts a row two characters right - a stated,
|
||||
# unclosed gap that makes counting spaces strictly worse than it looks. Every
|
||||
# other rendering a program consumes (--plan, --inbox-plan, --dispatch) is
|
||||
# key=value for exactly this reason; the default view was the one left out.
|
||||
#
|
||||
# It takes NO position and adds NO judgement: every field is a value $RECORDS
|
||||
# already holds, printed under the name its column carries. The three mailbox
|
||||
# counts are three separate fields because they are three separate facts - INN
|
||||
# is "others wait on you", ORDRE is "work waits on this repo", FLY is "someone
|
||||
# took an order" - and this rendering exists because they were confused.
|
||||
#
|
||||
# ONE FIELD IS NOT A RENDERING, and it is named rather than blended in:
|
||||
# `upushet` is not a column the scan computes. It is measured here, once, for
|
||||
# the named repo only, because the order asks for it and because "nothing is
|
||||
# waiting to be pushed" and "nobody looked" are exactly the pair this repo
|
||||
# refuses to render alike. One reading for one repo in a one-repo view is not a
|
||||
# scan; it never enters the table, the plan or the briefing.
|
||||
#
|
||||
# It reads the remote-TRACKING ref, not the remote. That is a real bound, not a
|
||||
# hedge: a stale ref under-reports after someone else pushed, and over-reports
|
||||
# after a push this checkout has not fetched. The honest reading of `upushet=N`
|
||||
# is "the local ref says N", and `git ls-remote` is still what a claim ABOUT
|
||||
# THE WORLD costs.
|
||||
row_unpushed() {
|
||||
ru_up="$(git -C "$1" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)" || return 1
|
||||
[ -n "$ru_up" ] || return 1
|
||||
ru_n="$(git -C "$1" rev-list --count "$ru_up..HEAD" 2>/dev/null)" || return 1
|
||||
case "$ru_n" in ''|*[!0-9]*) return 1 ;; esac
|
||||
printf '%s' "$ru_n"
|
||||
}
|
||||
|
||||
row_one() {
|
||||
r_rec="$(awk -F'|' -v n="$ROW_REPO" '$3==n {print; exit}' "$RECORDS")"
|
||||
# REFUSE rather than emit an empty block. A consumer reading ^status= off an
|
||||
# empty output gets nothing back and reads it as a repo with no board line -
|
||||
# which is a real, different state this very rendering spells as `?`.
|
||||
if [ -z "$r_rec" ]; then
|
||||
echo "board: no repo named '$ROW_REPO' in the scanned roots" >&2
|
||||
return 2
|
||||
fi
|
||||
r_disp="$(printf '%s' "$r_rec" | cut -d'|' -f4)"
|
||||
r_cost="$(printf '%s' "$r_rec" | cut -d'|' -f5)"
|
||||
r_inn="$(printf '%s' "$r_rec" | cut -d'|' -f6)"
|
||||
r_drt="$(printf '%s' "$r_rec" | cut -d'|' -f7)"
|
||||
r_alder="$(printf '%s' "$r_rec" | cut -d'|' -f8)"
|
||||
r_siste="$(printf '%s' "$r_rec" | cut -d'|' -f9)"
|
||||
r_dir="$(printf '%s' "$r_rec" | cut -d'|' -f10)"
|
||||
r_blocked="$(printf '%s' "$r_rec" | cut -d'|' -f11)"
|
||||
r_ordre="$(printf '%s' "$r_rec" | cut -d'|' -f12)"
|
||||
r_ordreage="$(printf '%s' "$r_rec" | cut -d'|' -f13)"
|
||||
r_fly="$(printf '%s' "$r_rec" | cut -d'|' -f14)"
|
||||
r_flyage="$(printf '%s' "$r_rec" | cut -d'|' -f15)"
|
||||
r_voy="$(printf '%s' "$r_rec" | cut -d'|' -f16)"
|
||||
# `neste` is free prose and may contain a literal '|', which is why it is the
|
||||
# last field of the record - so it is taken as "everything from 17 on", never
|
||||
# as field 17.
|
||||
r_neste="$(printf '%s' "$r_rec" | cut -d'|' -f17-)"
|
||||
|
||||
# The TOKEN, not the display string: the table renders a blocked repo as
|
||||
# `blocked>target` because one column has to carry both, and this rendering
|
||||
# has two fields and no such constraint. A consumer matching status=blocked
|
||||
# must not have to know about the arrow.
|
||||
case "$r_disp" in
|
||||
blocked\>*) r_status=blocked ;;
|
||||
*) r_status="$r_disp" ;;
|
||||
esac
|
||||
|
||||
r_unpushed="$(row_unpushed "$r_dir")" || r_unpushed="?"
|
||||
|
||||
echo "repo=$ROW_REPO"
|
||||
echo "dir=$r_dir"
|
||||
echo "status=$r_status"
|
||||
echo "blocked-on=$r_blocked"
|
||||
echo "next-cost=$r_cost"
|
||||
echo "inn=$r_inn"
|
||||
echo "ordre=$r_ordre"
|
||||
echo "ordre-alder=$r_ordreage"
|
||||
echo "fly=$r_fly"
|
||||
echo "fly-alder=$r_flyage"
|
||||
echo "voy=$r_voy"
|
||||
echo "drt=$r_drt"
|
||||
echo "alder=$r_alder"
|
||||
echo "siste=$r_siste"
|
||||
echo "upushet=$r_unpushed"
|
||||
# LAST, and for the same reason it is last in the record: it is free prose
|
||||
# lifted out of a STATE.md, so anything placed after it would be unreachable
|
||||
# for a consumer reading from the end. Printed WHOLE - the 38-character cut
|
||||
# belongs to the table column, and cutting here would make the cut string the
|
||||
# only copy, which is the defect --brief was built to stop.
|
||||
echo "neste=$r_neste"
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "$ROW" -eq 1 ]; then
|
||||
row_one
|
||||
exit $?
|
||||
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 +2249,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; }
|
||||
|
|
@ -1732,6 +2276,13 @@ n_nofield="$(awk -F'|' '$4=="?"' "$RECORDS" | wc -l | tr -d ' ')"
|
|||
n_unmeas="$(awk -F'|' '$7=="?"' "$RECORDS" | wc -l | tr -d ' ')"
|
||||
|
||||
printf '\nSUM: %s uhaandterte innboks-meldinger, %s ukommiterte filer.\n' "$tot_in" "$tot_dirty"
|
||||
# The scan's own denominator (order 20260903T190201Z-238406410, .claude). The
|
||||
# repo count in the header answers "how many were found"; this answers "how
|
||||
# many were looked at", which is the only way an admission criterion that
|
||||
# excludes most nested repos is visible at all. Verifiseringsloven ansikt 4:
|
||||
# a measurement without its nevner is read as a fact about the world.
|
||||
printf 'undersoekt: %s katalog(er) depth 1, %s polyrepo-container(e), %s nestede repo (%s med STATE.md tatt med).\n' \
|
||||
"$N_DEPTH1" "$N_CONTAINER" "$N_NESTED" "$N_NESTED_IN"
|
||||
[ "${n_mal:-0}" -gt 0 ] && printf 'ADVARSEL: %s repo har MALFORMED status-token (utenfor det lukkede settet).\n' "$n_mal"
|
||||
[ "${n_nofield:-0}" -gt 0 ] && printf 'MERK: %s repo mangler board-linje - status/kost er ukjent (?), NESTE-utdrag brukes.\n' "$n_nofield"
|
||||
[ "${n_unmeas:-0}" -gt 0 ] && printf 'MERK: %s repo har umaalbart arbeidstre - git svarte ikke, DRT er ? og IKKE med i summen over.\n' "$n_unmeas"
|
||||
|
|
|
|||
|
|
@ -73,6 +73,31 @@ age_of() {
|
|||
if [ -n "$ao_m" ]; then echo $(( (NOW - ao_m) / 86400 )); else echo "?"; fi
|
||||
}
|
||||
|
||||
# Delivery age in whole days, read from the FILENAME's timestamp and never from
|
||||
# the mtime. ORDRE 20260903T185736Z-1290610855: `--return` rewrites the order
|
||||
# file's mtime, so an order returned three times reported as brand new - the
|
||||
# reading that exists to say "this has sat here a long time" was reset by the
|
||||
# act of putting it back. The filename is written once, at delivery, and nothing
|
||||
# rewrites it, which is exactly the fact a PENDING age is asking about.
|
||||
#
|
||||
# This is NOT the claimed case. A claim's age is "how long has it been in
|
||||
# flight", which is the claim marker's mtime - a different question with a
|
||||
# different right answer, so age_of stays and stays used there.
|
||||
#
|
||||
# A name the grammar does not produce has no readable delivery time and yields
|
||||
# "?" - the same fail-safe age_of already used for an unreadable mtime, never a
|
||||
# fabricated 0, which would make an unmeasured order look new.
|
||||
pending_age_of() {
|
||||
pao_ts="$(basename "$1")"; pao_ts="${pao_ts%%-*}"
|
||||
case "$pao_ts" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z) ;;
|
||||
*) echo "?"; return 0 ;;
|
||||
esac
|
||||
pao_n="$(printf '%s' "$pao_ts" | tr -dc '0-9')"
|
||||
pao_e="$(date -u -j -f %Y%m%d%H%M%S "$pao_n" +%s 2>/dev/null)"
|
||||
if [ -n "$pao_e" ]; then echo $(( (NOW - pao_e) / 86400 )); else echo "?"; fi
|
||||
}
|
||||
|
||||
field_of() {
|
||||
# Bounded to the frontmatter block: a body line must never be able to forge a
|
||||
# header field the reader is told to trust.
|
||||
|
|
@ -92,7 +117,7 @@ for f in "$ORDERS"/*.md; do
|
|||
# next session the same dead premise with no warning that it is dead.
|
||||
ret="$(grep -m1 '^<!-- order-returned:' "$f" 2>/dev/null | sed -e 's/^<!-- order-returned:[[:space:]]*//' -e 's/[[:space:]]*-->$//')"
|
||||
OUT="${OUT}
|
||||
--- order: ${id} (from ${from}, pending, $(age_of "$f")d old) ---
|
||||
--- order: ${id} (from ${from}, pending, $(pending_age_of "$f")d old) ---
|
||||
subject: ${subj}"
|
||||
[ -n "$ret" ] && OUT="${OUT}
|
||||
returned earlier: ${ret}"
|
||||
|
|
|
|||
|
|
@ -1250,6 +1250,144 @@ check "F14: control - a correct call over a real root still exits 0" $?
|
|||
|
||||
/bin/rm -rf "$F5DIR" 2>/dev/null
|
||||
|
||||
# 38. The launchd templates. A wrong program path in a plist is the one defect
|
||||
# in this repo that NOTHING catches at runtime: the agent simply never runs, in
|
||||
# silence, and `launchctl list` confirms only that it is LOADED, never that it
|
||||
# does anything right. There is no output to be wrong, no exit status to read -
|
||||
# the failure looks exactly like a quiet machine. So the path is asserted here,
|
||||
# statically, against the file it actually names.
|
||||
#
|
||||
# This section covers EVERY plist in launchd/, not only the sweep agent that
|
||||
# 0.33.0 adds, and that is deliberate: the plist grammar has one reader here
|
||||
# rather than one per agent. Two half-checks in two suites would drift, which is
|
||||
# the two-copies-of-one-policy defect this repo names repeatedly. board-selftest
|
||||
# still owns brief-nightly.sh's BEHAVIOUR (section 9); this owns the templates.
|
||||
#
|
||||
# Deliberately NOT checked here: XML well-formedness. `plutil` is not coreutils,
|
||||
# and malformed XML is the one plist defect that already fails LOUDLY - launchctl
|
||||
# load rejects it on the spot. This section is for the defect that does not: a
|
||||
# path that is merely wrong. Both files were linted by hand at 0.33.0.
|
||||
LAUNCHD="$DIR/../launchd"
|
||||
REPOROOT="$(cd "$DIR/.." && pwd)"
|
||||
|
||||
# One reader, shared by the real files below AND by the control at the end. A
|
||||
# control that runs different code from the case it certifies proves nothing
|
||||
# about it. The program path is the <string> carrying the checkout placeholder;
|
||||
# the install instructions in the header comment name __CHECKOUT__ too, which is
|
||||
# why <string> has to match first.
|
||||
plist_program_path() {
|
||||
grep '<string>' "$1" 2>/dev/null | grep '__CHECKOUT__' | head -1 \
|
||||
| sed -e 's/.*<string>//' -e 's|</string>.*||'
|
||||
}
|
||||
plist_label() {
|
||||
grep -A1 '<key>Label</key>' "$1" 2>/dev/null | grep '<string>' | head -1 \
|
||||
| sed -e 's/.*<string>//' -e 's|</string>.*||'
|
||||
}
|
||||
plist_hour() {
|
||||
grep -A1 '<key>Hour</key>' "$1" 2>/dev/null | grep '<integer>' | head -1 \
|
||||
| sed -e 's/.*<integer>//' -e 's|</integer>.*||'
|
||||
}
|
||||
|
||||
plist_n=0
|
||||
for p in "$LAUNCHD"/*.plist; do
|
||||
[ -e "$p" ] || continue
|
||||
plist_n=$((plist_n + 1))
|
||||
pb="$(basename "$p")"
|
||||
|
||||
# launchctl addresses an agent by Label, the operator by filename. When they
|
||||
# disagree, load/start/unload silently act on a different agent than the one
|
||||
# being edited.
|
||||
lbl="$(plist_label "$p")"
|
||||
[ -n "$lbl" ] && [ "$lbl" = "${pb%.plist}" ]
|
||||
check "launchd $pb: Label matches the filename" $?
|
||||
|
||||
# The check this section exists for.
|
||||
prog="$(plist_program_path "$p")"
|
||||
[ -n "$prog" ] && [ -s "$REPOROOT/${prog#__CHECKOUT__/}" ]
|
||||
check "launchd $pb: ProgramArguments names a script that exists here" $?
|
||||
|
||||
# The repo is mirrored publicly and a plist is the one file that would
|
||||
# otherwise carry an absolute home path. It stays a TEMPLATE.
|
||||
grep -q '__HOME__' "$p"
|
||||
check "launchd $pb: log paths stay a __HOME__ placeholder (public mirror)" $?
|
||||
|
||||
# The cache path is version-pinned, so an agent pointing there breaks silently
|
||||
# on the next bump - and a second copy of these scripts on disk is the exact
|
||||
# defect class that produced the 0.12.1 stale-fallback bug. Asserted on the
|
||||
# EXTRACTED PATH, never on the whole file: the brief plist's header explains in
|
||||
# prose why it does not point at the cache, and a file-wide grep read that
|
||||
# explanation as the defect it warns about. Same shape as the board line, where
|
||||
# prose saying status=done must never trigger the done-guard.
|
||||
case "$prog" in *plugins/cache*) false ;; *) true ;; esac
|
||||
check "launchd $pb: the program path is not the version-pinned plugin cache" $?
|
||||
done
|
||||
|
||||
[ "$plist_n" -ge 2 ]
|
||||
check "launchd: both agent templates are present (brief + sweep)" $?
|
||||
|
||||
# The grace window is the OPERATOR's policy constant (14 days, decided
|
||||
# 2026-09-03), not the script's default wearing a schedule. An agent quietly
|
||||
# running a different window would close a different population every night with
|
||||
# nothing reporting the change.
|
||||
SWEEPPL="$LAUNCHD/com.ktg.repo-mailbox-sweep.plist"
|
||||
grep -q '<string>--write</string>' "$SWEEPPL" 2>/dev/null \
|
||||
&& grep -q '<string>--days</string>' "$SWEEPPL" 2>/dev/null \
|
||||
&& grep -q '<string>14</string>' "$SWEEPPL" 2>/dev/null
|
||||
check "launchd sweep: the agent runs --write --days 14, the authorized window" $?
|
||||
|
||||
# The briefing READS the mailbox the sweep MUTATES, so the two must not fire in
|
||||
# the same minute: a briefing rendered mid-sweep counts messages that are being
|
||||
# closed underneath it.
|
||||
hb="$(plist_hour "$LAUNCHD/com.ktg.repo-mailbox-brief.plist")"
|
||||
hs="$(plist_hour "$SWEEPPL")"
|
||||
[ -n "$hb" ] && [ -n "$hs" ] && [ "$hb" != "$hs" ]
|
||||
check "launchd: the two agents run at different hours (the brief reads what the sweep mutates)" $?
|
||||
|
||||
# Mandatory controls. A path check with no negative case is a check that cannot
|
||||
# go red, which this repo has shipped once already (section 11's vacuous first
|
||||
# cut) and will not ship again.
|
||||
BADPL="$CLAUDE_COORD_DIR/bad.plist"
|
||||
{
|
||||
echo '<plist version="1.0"><dict>'
|
||||
echo '<key>Label</key>'
|
||||
echo '<string>com.ktg.repo-mailbox-bad</string>'
|
||||
echo '<key>ProgramArguments</key>'
|
||||
echo '<array>'
|
||||
echo '<string>/bin/bash</string>'
|
||||
echo '<string>__CHECKOUT__/scripts/no-such-script.sh</string>'
|
||||
echo '</array>'
|
||||
echo '</dict></plist>'
|
||||
} > "$BADPL"
|
||||
|
||||
[ "$(plist_program_path "$BADPL")" = "__CHECKOUT__/scripts/no-such-script.sh" ]
|
||||
check "launchd control: the extraction really does read a program path" $?
|
||||
|
||||
badprog="$(plist_program_path "$BADPL")"
|
||||
[ -s "$REPOROOT/${badprog#__CHECKOUT__/}" ]; [ $? -ne 0 ]
|
||||
check "launchd control: a plist naming a missing script is judged missing" $?
|
||||
|
||||
[ "$(plist_label "$BADPL")" = "bad" ]; [ $? -ne 0 ]
|
||||
check "launchd control: a Label disagreeing with the filename is caught" $?
|
||||
|
||||
# The cache check needs its own control, because narrowing it from the whole file
|
||||
# to the extracted path is exactly the kind of narrowing that can quietly stop
|
||||
# catching anything.
|
||||
CACHEPL="$CLAUDE_COORD_DIR/cache.plist"
|
||||
{
|
||||
echo '<plist version="1.0"><dict>'
|
||||
echo '<key>ProgramArguments</key>'
|
||||
echo '<array>'
|
||||
echo '<string>/bin/bash</string>'
|
||||
echo '<string>__CHECKOUT__/.claude/plugins/cache/repo-mailbox/0.33.0/scripts/coord-sweep.sh</string>'
|
||||
echo '</array>'
|
||||
echo '</dict></plist>'
|
||||
} > "$CACHEPL"
|
||||
cprog="$(plist_program_path "$CACHEPL")"
|
||||
case "$cprog" in *plugins/cache*) true ;; *) false ;; esac
|
||||
check "launchd control: a program path INSIDE the plugin cache is caught" $?
|
||||
|
||||
/bin/rm -f "$BADPL" "$CACHEPL" 2>/dev/null
|
||||
|
||||
echo "----"
|
||||
echo "PASS=$PASS FAIL=$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -408,6 +408,69 @@ CLAUDE_COORD_DIR="$O10DIR" "$SEND" --to .profile --from tester --subject s --mes
|
|||
check "10d: control - a dot-prefixed target name still receives its order" $?
|
||||
/bin/rm -rf "$O10DIR" 2>/dev/null
|
||||
|
||||
# --- 11. Pending age comes from the FILENAME, never the mtime ---------------
|
||||
# ORDRE 20260903T185736Z-1290610855: `--return` rewrites the order file's
|
||||
# mtime, and both age surfaces read mtime, so an order returned three times can
|
||||
# never read as old. Measured on the live queue before the order was written: a
|
||||
# file whose name says 2026-09-02 reported "0d old" minutes after a return.
|
||||
#
|
||||
# PM decision, and it is two questions with two answers: a PENDING order's age
|
||||
# is "how long has this sat with no owner" = now - the DELIVERY time, which only
|
||||
# the filename carries; a CLAIMED order's age is "how long has it been in
|
||||
# flight" = the claim marker's mtime, which is already right and stays.
|
||||
O11DIR="$WORK/o11"; mkdir -p "$O11DIR"
|
||||
o11_old_ts="$(date -u -v-2d +%Y%m%dT%H%M%SZ 2>/dev/null)"
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$SEND" --to o11repo --from dispatcher \
|
||||
--subject "aged order" --message "body" >/dev/null 2>&1
|
||||
o11_q="$O11DIR/o11repo/orders"
|
||||
o11_orig="$(ls -1 "$o11_q"/*.md 2>/dev/null | head -1)"
|
||||
o11_id="$(basename "$o11_orig" .md)"
|
||||
o11_aged_id="${o11_old_ts}-${o11_id#*-}"
|
||||
mv "$o11_orig" "$o11_q/$o11_aged_id.md" 2>/dev/null
|
||||
|
||||
# Ground truth FIRST, so a broken `date -v` fails here instead of turning the
|
||||
# whole section into a test of nothing (the F13 section-11 lesson).
|
||||
[ -n "$o11_old_ts" ] && [ -f "$o11_q/$o11_aged_id.md" ]
|
||||
check "11a: ground truth - the fixture order's filename timestamp is 2 days old" $?
|
||||
|
||||
# Drive the REAL defect: claim it, then return it. The return is what rewrites
|
||||
# the mtime, so this is the path that produced the live 0d reading.
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$CLAIM" --repo o11repo "$o11_aged_id" >/dev/null 2>&1
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$ODONE" --repo o11repo "$o11_aged_id" --return --reason "test" >/dev/null 2>&1
|
||||
o11_mtime="$(stat -f %m "$o11_q/$o11_aged_id.md" 2>/dev/null)"
|
||||
o11_now="$(date +%s)"
|
||||
[ -n "$o11_mtime" ] && [ $(( o11_now - o11_mtime )) -lt 300 ]
|
||||
check "11b: ground truth - the return really did rewrite the file's mtime to now" $?
|
||||
|
||||
o11_out="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11repo 2>/dev/null)"
|
||||
printf '%s' "$o11_out" | grep -q 'pending, 2d old'
|
||||
check "11c: a returned order reports its DELIVERY age (2d), not 0d" $?
|
||||
|
||||
# Known-positive control: the reading must still be able to say 0d, or 11c
|
||||
# would pass just as well against a function that always prints 2.
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$SEND" --to o11fresh --from dispatcher \
|
||||
--subject "fresh order" --message "body" >/dev/null 2>&1
|
||||
o11_fresh="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11fresh 2>/dev/null)"
|
||||
printf '%s' "$o11_fresh" | grep -q 'pending, 0d old'
|
||||
check "11d: control - a freshly delivered order still reports 0d" $?
|
||||
|
||||
# FLY is the OTHER question and must not move: the claim marker's mtime is when
|
||||
# the claim happened, and an order with an ancient filename claimed just now has
|
||||
# been in flight for 0 days.
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$CLAIM" --repo o11repo "$o11_aged_id" >/dev/null 2>&1
|
||||
o11_fly="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11repo 2>/dev/null)"
|
||||
printf '%s' "$o11_fly" | grep -q 'CLAIMED 0d ago'
|
||||
check "11e: FLY age still comes from the claim marker's mtime, not the filename" $?
|
||||
|
||||
# A name the grammar does not produce has no readable delivery time. It must
|
||||
# read "?" - the same fail-safe the mtime path already used, never a fabricated
|
||||
# 0, which would make an unmeasured order look brand new.
|
||||
mkdir -p "$O11DIR/o11bad/orders"
|
||||
printf -- '---\nfrom: x\nsubject: s\n---\nbody\n' > "$O11DIR/o11bad/orders/not-a-timestamp.md"
|
||||
o11_bad="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11bad 2>/dev/null)"
|
||||
printf '%s' "$o11_bad" | grep -q 'pending, ?d old'
|
||||
check "11f: an unparseable filename timestamp reads ?, never 0" $?
|
||||
|
||||
echo
|
||||
echo "orders-selftest: $PASS passed, $FAIL failed, $SKIP skipped (of $((PASS+FAIL+SKIP)) checks)"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ check "all 81 trait combinations emit one of the 4 calculator rows" $?
|
|||
# to show the same string back. Runs for all six rows.
|
||||
ROOT="$(mktemp -d)"
|
||||
MBOX="$(mktemp -d)"
|
||||
cleanup() { /bin/rm -rf "$ROOT" "$MBOX" 2>/dev/null; }
|
||||
RL_ROOT=""
|
||||
cleanup() { /bin/rm -rf "$ROOT" "$MBOX" ${RL_ROOT:+"$RL_ROOT"} 2>/dev/null; }
|
||||
trap cleanup EXIT
|
||||
|
||||
rt_bad=0
|
||||
|
|
@ -169,6 +170,16 @@ done
|
|||
# rather than produced by "$R".
|
||||
rt_case "rt-5" "Fable 5/high"
|
||||
rt_case "rt-6" "Fable 5/xhigh"
|
||||
# A hand-written Fable 5.1 board line. next-cost extraction is free text, so
|
||||
# board.sh parses the point release back unchanged - pinned here so a later
|
||||
# narrowing of that extraction fails in this suite rather than in the
|
||||
# operator's eye. MEASURED GAP, stated rather than closed: at 15 characters it
|
||||
# overflows the %-14s KOST column and shifts the rest of that row one column
|
||||
# right. That is a board.sh rendering change nobody ordered in this session, so
|
||||
# it is reported to .claude, not fixed here - which is also why "Fable
|
||||
# 5.1/xhigh" is deliberately absent from the widest-value loop below. Adding it
|
||||
# there would go red, and the red would be the unfixed gap, not a broken test.
|
||||
rt_case "rt-51" "Fable 5.1/xhigh"
|
||||
|
||||
OUT="$(CLAUDE_COORD_DIR="$MBOX" "$BOARD" --roots "$ROOT" 2>/dev/null)"
|
||||
for want in "Sonnet 5/high" "Sonnet 5/xhigh" "Opus 5/high" "Opus 5/xhigh" \
|
||||
|
|
@ -176,6 +187,8 @@ for want in "Sonnet 5/high" "Sonnet 5/xhigh" "Opus 5/high" "Opus 5/xhigh" \
|
|||
printf '%s' "$OUT" | grep -q "$want" || { rt_bad=$((rt_bad+1)); echo " board lost: [$want]"; }
|
||||
done
|
||||
[ "$rt_bad" -eq 0 ]; check "round trip: board.sh parses back all 6 emitted values" $?
|
||||
printf '%s' "$OUT" | grep -q 'Fable 5\.1/xhigh'
|
||||
check "board parses back a hand-written Fable 5.1 next-cost" $?
|
||||
|
||||
# board.sh renders KOST with %-14s; a longer value shoves the whole row right
|
||||
# even though it parsed fine. Measure the widest string the table can emit -
|
||||
|
|
@ -272,6 +285,43 @@ check "no route-last line when the record is omitted" "$rc"
|
|||
--last-completed yes --last-corrections 0 >/dev/null 2>&1
|
||||
[ $? -eq 0 ]; check "--last-model/-effort accept every legal value" $?
|
||||
|
||||
# Fable 5.1 shipped 2026-09-01 and the closed set refused it, so a session that
|
||||
# actually ran it could not record what it ran: the record was either omitted
|
||||
# or LIED, and a lied record reads back months later as a measurement. The set
|
||||
# is WIDENED, never replaced by form validation - the check below is what makes
|
||||
# that choice machine-verified instead of prose. "Fable 5" stays legal for a
|
||||
# reason stronger than the one STATE.md on this machine that still carries it:
|
||||
# route.sh's OWN row table spells rows 5-6 "Fable 5/high" and "Fable 5/xhigh",
|
||||
# so dropping it would make the script refuse to record a value its own spec
|
||||
# names. The check above this one is what goes red if anyone drops it.
|
||||
"$R" --path known --verification strong --reversibility cheap --scope local \
|
||||
--rationale x --last-model "Fable 5.1" --last-effort xhigh \
|
||||
--last-completed yes --last-corrections 0 >/dev/null 2>&1
|
||||
[ $? -eq 0 ]; check "--last-model accepts the Fable 5.1 point release" $?
|
||||
|
||||
# The set is still CLOSED after being widened, and this is the whole cost of
|
||||
# NOT switching to form validation. A pattern like "<family> <digits>[.<digits>]"
|
||||
# would accept every line below, and would stop catching a version that does
|
||||
# not exist - which reads back later as evidence that a model ran when it never
|
||||
# shipped. That is the positive-looking null this repo refuses everywhere else.
|
||||
fable_bad=0
|
||||
for bad in "Fable 5.2" "Fable 6" "Fable 5.10"; do
|
||||
"$R" --path known --verification strong --reversibility cheap --scope local \
|
||||
--rationale x --last-model "$bad" --last-effort xhigh \
|
||||
--last-completed yes --last-corrections 0 >/dev/null 2>&1
|
||||
[ $? -eq 2 ] || { fable_bad=$((fable_bad+1)); echo " accepted a model that does not exist: [$bad]"; }
|
||||
done
|
||||
[ "$fable_bad" -eq 0 ]; check "the model set stays CLOSED after Fable 5.1 (no form validation)" $?
|
||||
|
||||
# Accepting the value is not the same as RECORDING it. The record is what the
|
||||
# next session reads back, so the emitted line must carry the point release
|
||||
# verbatim rather than collapsing it to the family name.
|
||||
LAST51="$("$R" --path known --verification strong --reversibility cheap --scope local \
|
||||
--rationale x --last-model "Fable 5.1" --last-effort xhigh \
|
||||
--last-completed yes --last-corrections 0 2>/dev/null | sed -n 's/^route-last=//p')"
|
||||
printf '%s' "$LAST51" | grep -q '^<!-- route-last: model=Fable 5.1; effort=xhigh; completed=yes; corrections=0 -->$'
|
||||
check "route-last carries Fable 5.1 verbatim into the emitted line" $?
|
||||
|
||||
# The record is telemetry and must NOT silently change what the calculator
|
||||
# outputs - a "completed=no" record describes what happened, and covers
|
||||
# context exhaustion, an operator interrupt and a block on another repo just
|
||||
|
|
@ -310,13 +360,14 @@ check "board + route + route-last stacked still yield prose in NESTE" $?
|
|||
# --- 9. Startup command and fallback --------------------------------------
|
||||
# Two spellings of ONE decision: the rubric name for the board line, the CLI
|
||||
# alias for the command the operator pastes. They must never disagree.
|
||||
# These assert the WHOLE string, advisor included, so section 14's rule cannot
|
||||
# be widened without a deliberate edit here - the exact-match is the tripwire.
|
||||
# These assert the WHOLE string, so no flag can be added back to the emitted
|
||||
# command without a deliberate edit here - the exact-match is the tripwire that
|
||||
# keeps section 14's absence rule from being widened around.
|
||||
cmd="$(field partial strong cheap local command)"
|
||||
[ "$cmd" = "claude --model opus --effort high" ]; check "command mirrors the row (opus/high)" $?
|
||||
|
||||
cmd="$(field known strong cheap local command)"
|
||||
[ "$cmd" = "claude --model sonnet --effort high --advisor opus" ]; check "command mirrors the row (sonnet/high)" $?
|
||||
[ "$cmd" = "claude --model sonnet --effort high" ]; check "command mirrors the row (sonnet/high)" $?
|
||||
|
||||
# The rubric requires ALWAYS naming one row cheaper as the quota fallback.
|
||||
fb="$(field partial strong cheap local fallback)"
|
||||
|
|
@ -328,7 +379,7 @@ fb="$(field known strong cheap local fallback)"
|
|||
# The fallback needs its own pasteable command or the operator translates by
|
||||
# hand at exactly the moment they are under quota pressure.
|
||||
fbc="$(field partial strong cheap local fallback-command)"
|
||||
[ "$fbc" = "claude --model sonnet --effort xhigh --advisor opus" ]; check "fallback ships its own command" $?
|
||||
[ "$fbc" = "claude --model sonnet --effort xhigh" ]; check "fallback ships its own command" $?
|
||||
|
||||
# --- 10. The command carries no 'cd' --------------------------------------
|
||||
# One repo per terminal tab: a startup command prefixed with cd is wrong by
|
||||
|
|
@ -344,9 +395,9 @@ check "no emitted command contains a cd prefix" "$rc"
|
|||
# Model aliases are whatever the INSTALLED claude accepts - never hardcoded
|
||||
# without a gate, because an alias that stops resolving turns every emitted
|
||||
# command into a paste that fails.
|
||||
# Capture the effort TOKEN only. Anything may legitimately follow it on the
|
||||
# command line (--advisor does, since section 14), and a match that swallowed
|
||||
# the tail would report a valid effort as invalid.
|
||||
# Capture the effort TOKEN only. The command ends at the effort today, but the
|
||||
# pattern stays tail-tolerant: a match that swallowed whatever a later flag
|
||||
# added would report a valid effort as invalid.
|
||||
efforts="$(printf '%s' "$out" | sed -n 's/^command=claude --model [a-z]* --effort \([a-z]*\).*/\1/p')"
|
||||
case "|low|medium|high|xhigh|max|" in *"|$efforts|"*) rc=0 ;; *) rc=1 ;; esac
|
||||
check "emitted effort is in the verified effort set" "$rc"
|
||||
|
|
@ -418,117 +469,129 @@ check "route.sh no longer claims effort is unobservable from inside" "$rc"
|
|||
grep -q 'CLAUDE_EFFORT' "$R"
|
||||
check "route.sh names the source the caller should measure from" $?
|
||||
|
||||
# --- 14. The advisor: capability gap first, stakes second -----------------
|
||||
# The advisor is a second, stronger model consulted mid-task, so a session
|
||||
# carrying one costs more than a session without. It must therefore fire where
|
||||
# there is a NEED and nowhere else, or it decays into the always-on
|
||||
# advisorModel setting it exists to replace. Two independent needs qualify,
|
||||
# and they turn out to be almost disjoint.
|
||||
# --- 14. The advisor is NOT the rubric's to emit --------------------------
|
||||
# Struck by operator decision 2026-09-12
|
||||
# (~/.claude/docs/2026-09-12-helhetlig-vurdering-arbeidssystemet.md, cut row 3).
|
||||
# The rule it replaces fired on two needs - Sonnet rows always, Opus rows at
|
||||
# costly|one-way stakes - and read well. What killed it was a measurement, not
|
||||
# a change of taste: of 54 dispatches the PM issued 08.-12.09, ZERO carried
|
||||
# --advisor opus, because the operator starts sessions by hand and pastes the
|
||||
# model and effort, not the whole line. A rule nothing honours is not a policy,
|
||||
# it is a claim about the world that the world disagrees with - and this repo's
|
||||
# own standing rule is that an emitted value must be evidence, never decoration.
|
||||
#
|
||||
# 1. THE MAIN MODEL IS SONNET (rows 1-2). Here opus is a genuine capability
|
||||
# lift, not a peer: opus judgement at sonnet cost. This is the load-bearing
|
||||
# half, because every fallback-command is one row cheaper and the cheapest
|
||||
# rows are Sonnet - so this is what makes the QUOTA FALLBACK safe to take.
|
||||
# Pinned below by the row-3-falls-back-to-row-2 case.
|
||||
# So the calculator emits no advisor at all, and the advisor becomes what it
|
||||
# already was in practice: an operator decision per session. That is a strictly
|
||||
# SAFER direction for the one thing the old rule protected - the quota fallback
|
||||
# is still one row cheaper, it just no longer implies a capability lift nobody
|
||||
# was taking.
|
||||
#
|
||||
# 2. A MISTAKE IS EXPENSIVE TO UNDO (reversibility costly|one-way). On an Opus
|
||||
# row the advisor is a peer review rather than a lift, which is worth
|
||||
# paying for when being wrong is not cheap to reverse.
|
||||
#
|
||||
# The two barely overlap: costly forces row 3 and one-way forces row 4, so a
|
||||
# Sonnet row ALWAYS has reversibility=cheap and trigger 2 can never reach it.
|
||||
# Rule 1 covers the Sonnet rows, rule 2 covers the Opus rows.
|
||||
#
|
||||
# What is deliberately NOT a trigger: verification=none on its own. The set it
|
||||
# would add beyond rule 2 is exactly {verification=none AND
|
||||
# reversibility=cheap} - mistakes that are cheap to reverse. It survives on the
|
||||
# Sonnet rows only via rule 1, which is about the model, not the trait.
|
||||
# Pinned as an ABSENCE over the whole trait space rather than on four sampled
|
||||
# rows, because the claim is that no path emits it - the same "no write path
|
||||
# exists" argument the channel split uses. An absence check is worthless
|
||||
# without a known-positive control, so the sweep's own grep is proven able to
|
||||
# find a planted advisor before it is trusted to report none.
|
||||
|
||||
adv="$(field known strong cheap local command)"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "row 1 (Sonnet/high) always carries an advisor" $?
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 1 (Sonnet/high) carries no advisor" "$rc"
|
||||
|
||||
adv="$(field known weak cheap local command)"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "row 2 (Sonnet/xhigh) always carries an advisor" $?
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 2 (Sonnet/xhigh) carries no advisor" "$rc"
|
||||
|
||||
adv="$(field known strong costly local command)"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "reversibility=costly carries an advisor onto an Opus row" $?
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "reversibility=costly no longer pulls an advisor onto an Opus row" "$rc"
|
||||
|
||||
adv="$(field known strong one-way local command)"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "reversibility=one-way carries an advisor onto an Opus row" $?
|
||||
|
||||
# The Opus rows reached WITHOUT expensive stakes are the whole no-advisor set.
|
||||
adv="$(field partial strong cheap local command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 3 via path=partial at cheap stakes takes NO advisor" "$rc"
|
||||
|
||||
adv="$(field known strong cheap multi-file command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 3 via scope=multi-file at cheap stakes takes NO advisor" "$rc"
|
||||
|
||||
adv="$(field known strong cheap cross-cutting command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 4 via scope=cross-cutting at cheap stakes takes NO advisor" "$rc"
|
||||
|
||||
# Rows 5-6 (Fable) are unreachable through this calculator now - section 1
|
||||
# pins that directly, and section 3's closed vocabulary covers every command
|
||||
# this script can ever emit. There is nothing left here for "Fable takes no
|
||||
# advisor" to test: that fact still holds (gated against the installed claude
|
||||
# by the CLI check below), but row_advisor()'s branch for it can no longer be
|
||||
# exercised through the public interface.
|
||||
check "reversibility=one-way no longer pulls an advisor onto an Opus row" "$rc"
|
||||
|
||||
# The fallback is the half the old rule called load-bearing, so it is pinned
|
||||
# in its own right: dropping a row under quota pressure must not reintroduce
|
||||
# the flag by the back door.
|
||||
adv="$(field known strong one-way local fallback-command)"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "row 4 fallback to row 3 keeps the advisor at one-way stakes" $?
|
||||
|
||||
# THE POINT OF RULE 1, pinned. Row 3 at cheap stakes takes no advisor, but its
|
||||
# quota fallback is row 2 - a Sonnet row - which does. Dropping a row under
|
||||
# quota pressure must not silently drop the safety net with it.
|
||||
adv="$(field partial strong cheap local fallback-command)"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "an Opus row with no advisor still falls back to an advised Sonnet row" $?
|
||||
|
||||
adv="$(field known strong cheap cross-cutting fallback-command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 4 falling back to row 3 at cheap stakes stays unadvised" "$rc"
|
||||
check "the row-4 fallback carries no advisor either" "$rc"
|
||||
|
||||
# Closed vocabulary, same reason as the row table: one spelling in circulation.
|
||||
# fable is not merely weaker here, it is REJECTED as an advisor outright.
|
||||
allout="$("$R" --path known --verification strong --reversibility one-way \
|
||||
--scope cross-cutting --rationale x 2>/dev/null)"
|
||||
bad="$(printf '%s' "$allout" | grep -o -- '--advisor [a-z0-9-]*' | grep -v -- '--advisor opus')"
|
||||
[ -z "$bad" ]; check "the only advisor value ever emitted is opus:${bad:- none other}" $?
|
||||
adv="$(field partial strong cheap local fallback-command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "an Opus row falling back to a Sonnet row carries no advisor" "$rc"
|
||||
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
# A real gate that spends NO tokens. Advisor validation runs BEFORE the
|
||||
# empty-prompt check, so `-p ""` reaches the validator and then exits: a
|
||||
# valid advisor fails on the missing prompt, an invalid one fails on itself.
|
||||
# --help cannot gate this - it short-circuits before option validation, so
|
||||
# even an unknown flag exits 0 and the gate would pass without testing.
|
||||
# Row 1-2's trigger is "sonnet main + opus advisor" specifically, so the
|
||||
# main model is pinned rather than left to the ambient default.
|
||||
ADVOUT="$(claude --model sonnet --advisor opus -p "" 2>&1 | head -3)"
|
||||
if printf '%s' "$ADVOUT" | grep -q 'unknown option'; then rc=1; else rc=0; fi
|
||||
check "installed claude accepts the --advisor flag at all" "$rc"
|
||||
# THE SWEEP: every trait combination the calculator accepts, every line of
|
||||
# output. 81 combinations, so a rule surviving on one unsampled corner cannot
|
||||
# hide. Accumulated into one string and grepped once - a per-combination check
|
||||
# would add 81 lines to the summary and drown the rest of the suite.
|
||||
sweep=""
|
||||
for s_path in known partial undetermined; do
|
||||
for s_ver in strong weak none; do
|
||||
for s_rev in cheap costly one-way; do
|
||||
for s_sc in local multi-file cross-cutting; do
|
||||
sweep="$sweep
|
||||
$("$R" --path "$s_path" --verification "$s_ver" --reversibility "$s_rev" \
|
||||
--scope "$s_sc" --rationale x 2>/dev/null)"
|
||||
done
|
||||
done
|
||||
done
|
||||
done
|
||||
if printf '%s' "$sweep" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "no advisor in any output over all 81 trait combinations" "$rc"
|
||||
|
||||
if printf '%s' "$ADVOUT" | grep -q 'cannot advise'; then rc=1; else rc=0; fi
|
||||
check "installed claude accepts opus advising a sonnet main" "$rc"
|
||||
# Known-positive control: the grep above reports an absence, so it must be
|
||||
# shown able to report a presence. Without this the sweep would pass just as
|
||||
# happily against an empty string.
|
||||
if printf '%s' "$sweep claude --advisor opus" | grep -q -- '--advisor'; then rc=0; else rc=1; fi
|
||||
check "control: the sweep's grep does find a planted advisor" "$rc"
|
||||
|
||||
# The help text is the rubric's published form, so the rule has to leave there
|
||||
# too - a doc line nothing tests is a doc line that drifts, and a struck rule
|
||||
# still written down is worse than one never removed: it reads as current.
|
||||
# The literal flag string is absent from route.sh ENTIRELY, including the
|
||||
# paragraph that records what was struck - that paragraph names "an opus
|
||||
# advisor flag" in words on purpose. A blunt grep cannot tell a description
|
||||
# from a specification (the same reason the cache assertion in coord-selftest
|
||||
# runs on an extracted path rather than the whole file), and keeping the
|
||||
# string out is cheaper than teaching the check to read prose. Do not
|
||||
# "restore the quote" here.
|
||||
HELPOUT="$("$R" --help 2>/dev/null)"
|
||||
if printf '%s' "$HELPOUT" | grep -q -- '--advisor opus'; then rc=1; else rc=0; fi
|
||||
check "--help no longer documents emitting --advisor opus" "$rc"
|
||||
|
||||
if printf '%s' "$HELPOUT" | grep -q 'THE ADVISOR is emitted'; then rc=1; else rc=0; fi
|
||||
check "--help no longer carries the advisor rule block" "$rc"
|
||||
|
||||
# Removing the rule silently would leave a reader guessing whether the advisor
|
||||
# is forbidden, forgotten, or someone else's. It is the third, and the help
|
||||
# says which.
|
||||
printf '%s' "$HELPOUT" | grep -q 'advisor is an operator decision per session'
|
||||
check "--help states whose decision the advisor is instead" $?
|
||||
|
||||
grep -q 'advisor is an operator decision per session' "$R"
|
||||
check "route.sh itself carries that sentence, not just its help output" $?
|
||||
|
||||
# --- 14b. Old route lines still parse ------------------------------------
|
||||
# Backward compatibility, pinned rather than assumed. Measured on the real
|
||||
# tree the day the rule was struck: 0 of 48 `<!-- route:` lines anywhere in
|
||||
# ~/repos carry an advisor field at all, so nothing in circulation depends on
|
||||
# it - but board.sh's route_cmd_for() reads these lines by trait NAME, and a
|
||||
# reader that broke on an unknown field would turn a STATE.md written last
|
||||
# month into "that repo has no route line", which is the misreading this whole
|
||||
# file exists to prevent.
|
||||
RL_ROOT="$(mktemp -d)"; mkdir -p "$RL_ROOT/repo-legacy"
|
||||
( cd "$RL_ROOT/repo-legacy" && git init -q . 2>/dev/null ) || true
|
||||
{
|
||||
printf '# STATE\n\n## NESTE - START HER\n'
|
||||
printf '<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/high -->\n'
|
||||
printf '<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; advisor=opus; rationale=legacy line written before the rule was struck -->\n'
|
||||
printf 'legacy next step\n'
|
||||
} > "$RL_ROOT/repo-legacy/STATE.md"
|
||||
RL_OUT="$("$BOARD" --roots "$RL_ROOT" --plan 2>/dev/null)"
|
||||
printf '%s' "$RL_OUT" | grep -q '^command=claude --model sonnet --effort high$'
|
||||
check "a route line carrying a legacy advisor= field still yields a command" $?
|
||||
if printf '%s' "$RL_OUT" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "and the command derived from it carries no advisor" "$rc"
|
||||
|
||||
# row_advisor() never emits '--advisor fable' - it depends on the opposite
|
||||
# direction: whether opus can advise a FABLE main model. That is the fact
|
||||
# rows 5-6's suppression rests on, so that is what must be gated. (Measured
|
||||
# at CC 2.1.226: fable is now itself accepted as an advisor for opus/sonnet
|
||||
# mains - a capability-ordering change worth flagging to the operator, but
|
||||
# not what this check exists to pin.)
|
||||
FABOUT="$(claude --model fable --advisor opus -p "" 2>&1 | head -3)"
|
||||
printf '%s' "$FABOUT" | grep -q 'cannot advise'
|
||||
check "installed claude still rejects opus advising a fable main" $?
|
||||
else
|
||||
echo " skip - claude not on PATH, advisor flag gate not run"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "route-selftest: $PASS passed, $FAIL failed"
|
||||
|
|
|
|||
110
scripts/route.sh
110
scripts/route.sh
|
|
@ -29,10 +29,7 @@
|
|||
# departure from it is recorded in STATE as an OVERRIDE, never produced here.
|
||||
# Write "Fable 5/high" or "Fable 5/xhigh" into the board line by hand when
|
||||
# that is the right call; board.sh still parses both (route-selftest.sh
|
||||
# section 6 pins that half). The one fact worth carrying: a Fable session
|
||||
# runs without an advisor (row_advisor() below, and gated against the
|
||||
# installed claude by route-selftest.sh section 14) - informational, never a
|
||||
# gate on reaching the row, since there is no longer a gate to reach.
|
||||
# section 6 pins that half).
|
||||
#
|
||||
# Cheapest first, so the rubric's "always name one row cheaper as the quota
|
||||
# fallback" is row minus one, floored at row 1, correct by construction.
|
||||
|
|
@ -66,32 +63,14 @@
|
|||
# overkill costs quota every session - but a wrong architecture decision in a
|
||||
# published plugin costs more than either.
|
||||
#
|
||||
# THE ADVISOR is emitted into the command as '--advisor opus' - a second,
|
||||
# stronger model consulted at key moments during the session. It is added on a
|
||||
# NEED, never unconditionally: an always-on advisor is the global advisorModel
|
||||
# setting, which burns quota on every session in every repo and is the thing
|
||||
# this rule exists to replace. Two independent needs qualify:
|
||||
#
|
||||
# rows 1-2 ALWAYS. The main model is Sonnet, so opus is a capability LIFT
|
||||
# rather than a peer - opus judgement at sonnet cost. This is what
|
||||
# makes the FALLBACK safe to take: every fallback is one row
|
||||
# cheaper, and the cheapest rows are the Sonnet ones.
|
||||
# rows 3-4 only at reversibility=costly|one-way. The main model is already
|
||||
# Opus, so the advisor buys peer review, worth paying for when a
|
||||
# mistake is not cheap to undo.
|
||||
#
|
||||
# Rows 5-6 never reach this logic at all - the calculator cannot select them
|
||||
# (see above). Informational only: were the operator to hand-write a Fable
|
||||
# command, it would carry no advisor either way, since the CLI rejects every
|
||||
# advisor for a Fable main model.
|
||||
#
|
||||
# The two triggers barely overlap: costly forces row 3 and one-way forces row
|
||||
# 4, so a Sonnet row always has reversibility=cheap. verification=none is
|
||||
# deliberately not a third trigger - beyond the stakes rule it would only add
|
||||
# mistakes that are cheap to reverse, docs sessions among them.
|
||||
#
|
||||
# Applied per ROW, so 'fallback-command' carries its own correct answer rather
|
||||
# than the winning row's.
|
||||
# THE ADVISOR IS NOT EMITTED, and that is a decision rather than an omission:
|
||||
# the advisor is an operator decision per session, never the rubric's. Until
|
||||
# 2026-09-12 this calculator appended an opus advisor flag on a NEED - always on
|
||||
# the Sonnet rows, and on the Opus rows at costly|one-way stakes. It was struck
|
||||
# on a measurement: of 54 dispatches issued 08.-12.09 not one carried the flag,
|
||||
# because sessions are started by hand from the model and effort, not from the
|
||||
# whole line. A rule nothing honours is not policy, and an emitted value that
|
||||
# nobody acts on is decoration in a field whose only job is to be evidence.
|
||||
#
|
||||
# WHERE IT DISAGREES WITH THE RUBRIC'S EXAMPLES. The rows are task-type labels;
|
||||
# the traits are a different classification over the same six outcomes. They
|
||||
|
|
@ -113,7 +92,7 @@
|
|||
# route.sh --path <v> --verification <v> --reversibility <v> --scope <v>
|
||||
# --rationale <text>
|
||||
#
|
||||
# route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5>
|
||||
# route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5|Fable 5.1>
|
||||
# --last-effort <low|medium|high|xhigh|max>
|
||||
# --last-completed <yes|no> --last-corrections <n>
|
||||
#
|
||||
|
|
@ -208,9 +187,31 @@ if [ "$L_SET" -eq 1 ]; then
|
|||
# this record back as evidence months from now, so a drifted spelling
|
||||
# ("opus 5" for "Opus 5") rebuilds the reader-versus-writer drift this whole
|
||||
# script exists to remove, one field over.
|
||||
# THE SET IS CLOSED, AND STAYS CLOSED - decided 2026-09-01 when Fable 5.1
|
||||
# shipped and was refused here. Both boundary questions were live:
|
||||
#
|
||||
# (a) "Fable 5" is KEPT alongside the point release. The reason is not
|
||||
# backward compatibility with the one STATE.md on this machine that still
|
||||
# carries it (measured: 1 of 45 route-last lines) - it is that the row
|
||||
# table above spells rows 5-6 "Fable 5/high" and "Fable 5/xhigh".
|
||||
# Dropping the value would make this script refuse to record a name its
|
||||
# own spec writes.
|
||||
#
|
||||
# (b) The set was WIDENED rather than replaced by form validation. A pattern
|
||||
# like "<family> <digits>[.<digits>]" would still catch a misspelled
|
||||
# family and a drifted case, and would stop catching A VERSION THAT DOES
|
||||
# NOT EXIST: "Fable 5.2" and "Opus 7" would both pass and read back
|
||||
# months later as evidence that a model ran when it never shipped. This
|
||||
# field is telemetry read as evidence, so a silently-accepted lie is
|
||||
# worse than a loud refusal.
|
||||
#
|
||||
# The cost of that choice is real and was paid before it was made: a session
|
||||
# that genuinely ran Fable 5.1 could not record it, so its record was omitted
|
||||
# or lied. The list must therefore be extended the day a model ships, and the
|
||||
# die message says so rather than leaving the caller to guess.
|
||||
case "$L_MODEL" in
|
||||
"Sonnet 5"|"Opus 5"|"Fable 5") ;;
|
||||
*) die "--last-model: '$L_MODEL' is not a row-table model (Sonnet 5|Opus 5|Fable 5)" ;;
|
||||
"Sonnet 5"|"Opus 5"|"Fable 5"|"Fable 5.1") ;;
|
||||
*) die "--last-model: '$L_MODEL' is not a row-table model (Sonnet 5|Opus 5|Fable 5|Fable 5.1) - a newly shipped model must be added to this list in route.sh, never approximated to a name that is already in it" ;;
|
||||
esac
|
||||
case "$L_EFFORT" in
|
||||
low|medium|high|xhigh|max) ;;
|
||||
|
|
@ -266,41 +267,14 @@ row_base_cmd() {
|
|||
esac
|
||||
}
|
||||
|
||||
# THE ADVISOR is a second, stronger model consulted mid-task. It costs real
|
||||
# tokens per session, so it fires on a NEED and nowhere else - an unconditional
|
||||
# advisor is just the global advisorModel setting, which is the thing this
|
||||
# replaces. Two independent needs qualify, and they are almost disjoint:
|
||||
#
|
||||
# rows 1-2 (Sonnet) ALWAYS. opus is a capability LIFT here, not a peer:
|
||||
# opus judgement at sonnet cost. This half is what makes
|
||||
# the fallback-command safe, since every fallback is one
|
||||
# row cheaper and the cheapest rows are Sonnet.
|
||||
# rows 3-4 (Opus) only at costly|one-way stakes, where the advisor is a
|
||||
# peer review and being wrong is not cheap to undo.
|
||||
#
|
||||
# Rows 5-6 (Fable) never reach this function - $ROW can only be 1-4 (see
|
||||
# SELECTION above). Informational only: the only advisor this script ever
|
||||
# emits is opus (pinned by selftest 14's "the only advisor value ever emitted
|
||||
# is opus"), and opus is refused as under-capable for a fable main model -
|
||||
# measured against the installed claude, still true at CC 2.1.226 - so a
|
||||
# hand-written Fable command carries no advisor either way.
|
||||
#
|
||||
# costly forces row 3 and one-way forces row 4, so a Sonnet row always has
|
||||
# reversibility=cheap - the stakes rule can never reach rows 1-2, and the model
|
||||
# rule never reaches rows 3-4. verification=none is deliberately NOT a trigger:
|
||||
# beyond the stakes rule it would only add cheap-to-reverse mistakes, and it
|
||||
# would put an advisor on every docs session (known/none/cheap/local).
|
||||
#
|
||||
# Applied per ROW rather than once, because the fallback is a real command the
|
||||
# operator pastes under quota pressure and must carry its own correct answer.
|
||||
row_advisor() {
|
||||
case "$1" in
|
||||
1|2) echo " --advisor opus" ;;
|
||||
3|4) case "$2" in costly|one-way) echo " --advisor opus" ;; *) echo "" ;; esac ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
row_cmd() { printf '%s%s\n' "$(row_base_cmd "$1")" "$(row_advisor "$1" "$REVERS")"; }
|
||||
# No advisor is appended here or anywhere else - the advisor is an operator
|
||||
# decision per session, not a property this rubric computes (struck
|
||||
# 2026-09-12, see the header). row_cmd() is therefore the row's base command
|
||||
# and nothing more; it stays a function rather than collapsing into
|
||||
# row_base_cmd() because the emitted command and the row table are two
|
||||
# separate things that happened to converge, and a later flag would attach
|
||||
# here, to one place, for both the winning row and its fallback.
|
||||
row_cmd() { row_base_cmd "$1"; }
|
||||
|
||||
FB=$((ROW - 1)); [ "$FB" -ge 1 ] || FB=1
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,14 @@ 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.
|
||||
version: "0.31.0"
|
||||
version: "0.34.0"
|
||||
---
|
||||
|
||||
# board — which repo deserves the next session
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ description: >-
|
|||
covers retiring a broadcast that has become wrong or obsolete: "retract that
|
||||
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
|
||||
"den broadcasten er utdatert".
|
||||
version: "0.31.0"
|
||||
version: "0.34.0"
|
||||
---
|
||||
|
||||
# coord-send — natural-language front door for inter-repo messages
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ description: >-
|
|||
skill. Not for choosing WHICH repo deserves the next session (that is `board`),
|
||||
not for scoring model and effort alone (that is `route`), and not for sending a
|
||||
message to another repo (that is `coord-send`).
|
||||
version: "0.31.0"
|
||||
version: "0.34.0"
|
||||
---
|
||||
|
||||
# dispatch — hand a session a task it can actually start on
|
||||
|
|
@ -124,8 +124,8 @@ Two facts about this measurement, both verified 2026-08-16 against the installed
|
|||
|
||||
### 4. Score the four traits and call `--dispatch`
|
||||
|
||||
Scoring is judgement and it is yours; the model, effort and advisor flag are a
|
||||
lookup and are `route.sh`'s. Score the task **the dispatched session** will do,
|
||||
Scoring is judgement and it is yours; the model and effort are a lookup and are
|
||||
`route.sh`'s. Score the task **the dispatched session** will do,
|
||||
using the `route` skill's trait table.
|
||||
|
||||
Pass `--order-id <id>`, not `--prompt-file`. The emitted command is then a thin
|
||||
|
|
@ -140,13 +140,18 @@ empty prompt file, one level up.
|
|||
queue to write to. Passing both is refused: the session would be told two
|
||||
things.
|
||||
|
||||
`--dispatch` deliberately takes no `--model`/`--effort`. `--advisor opus` is a
|
||||
property of the rubric *row* — two rows share a model/effort pair while
|
||||
differing on it, and the CLI accepts a wrong advisor silently — so a dispatch
|
||||
that took the model directly would have no honest source for that flag. If the
|
||||
right call is a Fable row, the rubric cannot produce it: write that command by
|
||||
hand, and say in the handover that it is a recorded override, running without an
|
||||
advisor.
|
||||
`--dispatch` deliberately takes no `--model`/`--effort`. The rubric is a policy
|
||||
with exactly one copy, and a dispatch that took the model directly would be a
|
||||
second, unscored way to reach the same decision — one that records no traits, no
|
||||
rationale and no `next-cost`, so nothing afterwards could say whether the
|
||||
routing or the scoring was wrong. Score the traits and let the lookup spell the
|
||||
command. If the right call is a Fable row, the rubric cannot produce it: write
|
||||
that command by hand and say in the handover that it is a recorded override.
|
||||
|
||||
**No `--advisor` is emitted, by the rubric or by this skill.** That rule was
|
||||
struck 2026-09-12 on a measurement — 0 of 54 dispatches carried the flag. The
|
||||
advisor is an operator decision per session; do not add one to a dispatch
|
||||
command.
|
||||
|
||||
### 5. Verify, then hand it over
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ description: >-
|
|||
the operator names no model and no tool — choosing the model for the next
|
||||
session IS this skill. Not for choosing which REPO gets the next session:
|
||||
that is the `board` skill.
|
||||
version: "0.31.0"
|
||||
version: "0.34.0"
|
||||
---
|
||||
|
||||
# route — what the next session should run with
|
||||
|
|
@ -104,8 +104,7 @@ in STATE as an **override**, never as something this skill produces. If Fable
|
|||
is the right call for the next step's *form* (big-picture, review, planning),
|
||||
write the board line and the `rule` by hand — `board.sh` still parses
|
||||
"Fable 5/high" and "Fable 5/xhigh" — and say so plainly in the rationale rather
|
||||
than scoring the four traits to land there. One fact worth carrying into that
|
||||
override: a Fable session runs without an advisor.
|
||||
than scoring the four traits to land there.
|
||||
|
||||
## The last-session record
|
||||
|
||||
|
|
@ -186,24 +185,26 @@ Give the operator the two closing-line items and nothing more:
|
|||
the working directory is already right. If the next step belongs in a
|
||||
different repo, say so in plain words — that is a different tab, not a `cd`.
|
||||
|
||||
**Paste `command` verbatim, `--advisor opus` included.** The calculator decides
|
||||
the advisor per row, and it is not decoration: on a Sonnet row it is what lifts
|
||||
the session to Opus judgement at Sonnet cost, which is what makes the cheaper
|
||||
`fallback-command` safe to take under quota pressure. Dropping it because it
|
||||
looks like noise silently removes that. Equally, never *add* it to a command
|
||||
that came back without one — an unconditional advisor is the global
|
||||
`advisorModel` setting, which costs quota in every session in every repo and is
|
||||
the failure mode this rule replaces. `route.sh --help` carries the full rule.
|
||||
**Paste `command` verbatim.** It is the row's whole spelling: model and effort,
|
||||
and nothing else. Do not add flags the calculator did not emit.
|
||||
|
||||
**The advisor is not this skill's to decide.** Until 2026-09-12 the rubric
|
||||
appended an opus advisor on some rows; that rule is struck, on a measurement —
|
||||
of 54 dispatches issued 08.–12.09 not one carried it, because sessions are
|
||||
started by hand from the model and effort. The advisor is now an operator
|
||||
decision per session. Never add `--advisor` to an emitted command on your own
|
||||
initiative; if the operator asks for one, that is their call and it belongs in
|
||||
the handover as what it is — an operator choice, not a rubric outcome.
|
||||
|
||||
If `command` and `fallback-command` are the same as the current session's model,
|
||||
say `/clear` is enough instead — but only if no newly installed plugin or skill
|
||||
needs a fresh process to be picked up.
|
||||
|
||||
**`--advisor` is part of that comparison, not an afterthought.** It is a launch
|
||||
flag, so `/clear` reuses the process and keeps whatever advisor the session
|
||||
started with. If `command` carries `--advisor opus` and this session was not
|
||||
launched with it, `/clear` is *not* enough — the operator needs `/exit` and the
|
||||
full command, or the advisor silently never appears.
|
||||
**An advisor the operator asked for is part of that comparison.** It is a
|
||||
launch flag, so `/clear` reuses the process and keeps whatever advisor the
|
||||
session started with — a session that is to gain or lose one needs `/exit` and
|
||||
a full command, never `/clear`. The rubric never puts it there; the operator
|
||||
does.
|
||||
|
||||
Do not paste the whole output block. One row, the rule that produced it, the
|
||||
command.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
|
@ -12,8 +12,31 @@ import { fileURLToPath } from 'node:url';
|
|||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const hook = join(root, 'hooks', 'scripts', 'session-start.mjs');
|
||||
|
||||
// Every bash suite already prints its own total on its last line, and this
|
||||
// wrapper already runs all five. Capturing that line here is what makes the
|
||||
// README's numbers testable without a SECOND copy of the counting: nothing
|
||||
// re-counts `check` calls (loops make that undecidable anyway) and nothing
|
||||
// re-runs a suite to read a number the run in progress is already printing.
|
||||
// The five suites cost 212s sequentially, measured 2026-09-05 under /bin/bash
|
||||
// 3.2 - the marginal cost of the README check is zero because it consumes a
|
||||
// run that happens regardless.
|
||||
const summaries = new Map();
|
||||
|
||||
function runSuite(name) {
|
||||
const script = join(root, 'scripts', `${name}-selftest.sh`);
|
||||
try {
|
||||
summaries.set(name, execFileSync('bash', [script], { encoding: 'utf8' }));
|
||||
} catch (err) {
|
||||
// Record what the suite managed to print before failing, then let the
|
||||
// failure through: a red suite must stay red here, and the README check
|
||||
// below still gets a number to compare rather than a silent absence.
|
||||
if (typeof err.stdout === 'string') summaries.set(name, err.stdout);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
test('coord bash selftest passes', () => {
|
||||
execFileSync('bash', [join(root, 'scripts', 'coord-selftest.sh')], { encoding: 'utf8' });
|
||||
runSuite('coord');
|
||||
});
|
||||
|
||||
// board.sh reads this plugin's mailbox for its INN column, so the board ships
|
||||
|
|
@ -22,7 +45,7 @@ test('coord bash selftest passes', () => {
|
|||
// through CLAUDE_PLUGIN_ROOT, so a board.sh that exists only in
|
||||
// ~/.claude/scripts/ would be missing on exactly the path production uses.
|
||||
test('board bash selftest passes', () => {
|
||||
execFileSync('bash', [join(root, 'scripts', 'board-selftest.sh')], { encoding: 'utf8' });
|
||||
runSuite('board');
|
||||
});
|
||||
|
||||
// route.sh is the WRITER for the next-cost field board.sh already reads, so its
|
||||
|
|
@ -31,7 +54,7 @@ test('board bash selftest passes', () => {
|
|||
// CLAUDE_PLUGIN_ROOT, and a calculator proven only elsewhere is unproven on the
|
||||
// one path production uses.
|
||||
test('route bash selftest passes', () => {
|
||||
execFileSync('bash', [join(root, 'scripts', 'route-selftest.sh')], { encoding: 'utf8' });
|
||||
runSuite('route');
|
||||
});
|
||||
|
||||
// pre-state-line-guard.mjs is a PreToolUse hook, so like session-start.mjs it
|
||||
|
|
@ -39,7 +62,7 @@ test('route bash selftest passes', () => {
|
|||
// CLAUDE_PLUGIN_ROOT, and a guard proven only elsewhere is unproven on the
|
||||
// path production actually runs.
|
||||
test('state-line-guard bash selftest passes', () => {
|
||||
execFileSync('bash', [join(root, 'scripts', 'state-line-guard-selftest.sh')], { encoding: 'utf8' });
|
||||
runSuite('state-line-guard');
|
||||
});
|
||||
|
||||
// The order queue is the second channel beside the mailbox, with the opposite
|
||||
|
|
@ -48,7 +71,7 @@ test('state-line-guard bash selftest passes', () => {
|
|||
// CLAUDE_PLUGIN_ROOT, so a queue proven only elsewhere is unproven where it
|
||||
// runs.
|
||||
test('orders bash selftest passes', () => {
|
||||
execFileSync('bash', [join(root, 'scripts', 'orders-selftest.sh')], { encoding: 'utf8' });
|
||||
runSuite('orders');
|
||||
});
|
||||
|
||||
// The engine refuses to invent an identity from the cwd, but the hook is the
|
||||
|
|
@ -188,3 +211,69 @@ test('hook keeps mail and orders in separate blocks, mail first', () => {
|
|||
assert.ok(ctx.includes('UNTRUSTED DATA'), 'the mail block lost its authorization framing');
|
||||
assert.ok(ctx.includes('OPERATOR-AUTHORIZED'), 'the order block lost its authorization framing');
|
||||
});
|
||||
|
||||
// --- README's selftest numbers must rot loudly ------------------------------
|
||||
//
|
||||
// The badge and the five `## Development` comments are the only public claim
|
||||
// about how much this engine is pinned by, and they are the number furthest
|
||||
// from the meter: they rotted twice in a row (529 from 0.25.0; then a badge
|
||||
// saying 868 beside comments summing to 792 - two different wrong sums of the
|
||||
// same fact, neither matching the other, on the same screen). Nothing caught
|
||||
// either, because nothing compared them to anything.
|
||||
//
|
||||
// It lives HERE rather than in one of the five bash suites, and the choice is
|
||||
// not arbitrary. The order's parenthetical suggested the suite that already
|
||||
// pins README/catalog invariants; measured before choosing, no such suite
|
||||
// exists - `grep -ln README scripts/*selftest*.sh` returns board-selftest.sh
|
||||
// alone, on two incidental hits (a prose comment and a `research/README.md`
|
||||
// fixture). Of the places that could host it, this wrapper is the only one
|
||||
// where all five numbers exist at once in a run that already happens: a check
|
||||
// inside a suite could see its own count but would have to RE-RUN the other
|
||||
// four (212s, measured 2026-09-05) to see theirs, and reading counters out of
|
||||
// the scripts is the second copy of the counting this check was asked not to
|
||||
// be. `check` calls sit inside loops, so a static count is not merely a second
|
||||
// copy - it is a wrong one.
|
||||
//
|
||||
// The truth source is each suite's own summary line, verbatim, and a suite
|
||||
// that stops printing one FAILS here rather than being skipped: an absent
|
||||
// measurement must not read as a matching one.
|
||||
function suiteTotal(name) {
|
||||
const out = summaries.get(name);
|
||||
assert.ok(out !== undefined,
|
||||
`${name}-selftest produced no captured output: its total was never measured, ` +
|
||||
'so the README comparison below would be resting on nothing');
|
||||
// Two summary grammars, both already in the tree: coord prints
|
||||
// `PASS=N FAIL=M`, the other four print `<name>-selftest: N passed, M failed`
|
||||
// and orders adds `, S skipped (of T checks)`. README documents the TOTAL
|
||||
// number of checks, so skipped ones count.
|
||||
let m = out.match(/^\S+-selftest: (\d+) passed, (\d+) failed(?:, (\d+) skipped)?/m);
|
||||
if (m) return Number(m[1]) + Number(m[2]) + Number(m[3] ?? 0);
|
||||
m = out.match(/^PASS=(\d+) FAIL=(\d+)/m);
|
||||
assert.ok(m, `${name}-selftest printed no summary line this parser recognises`);
|
||||
return Number(m[1]) + Number(m[2]);
|
||||
}
|
||||
|
||||
test('README states the selftest counts the suites actually reported', () => {
|
||||
const readme = readFileSync(join(root, 'README.md'), 'utf8');
|
||||
const suites = ['coord', 'board', 'route', 'orders', 'state-line-guard'];
|
||||
|
||||
let sum = 0;
|
||||
for (const name of suites) {
|
||||
const measured = suiteTotal(name);
|
||||
sum += measured;
|
||||
const line = readme.match(
|
||||
new RegExp(`^\\s*bash scripts/${name}-selftest\\.sh\\s+#\\s+(\\d+) checks`, 'm'));
|
||||
assert.ok(line,
|
||||
`README's ## Development block has no "N checks" comment for ${name}-selftest.sh`);
|
||||
assert.equal(Number(line[1]), measured,
|
||||
`README says ${name}-selftest has ${line[1]} checks; it reported ${measured}`);
|
||||
}
|
||||
|
||||
// The badge is the sum, and it is compared against the MEASURED total rather
|
||||
// than against the five README comments: a badge agreeing with five stale
|
||||
// comments is exactly the 868-beside-792 shape, one layer down.
|
||||
const badge = readme.match(/badge\/selftest_checks-(\d+)-/);
|
||||
assert.ok(badge, 'README has no selftest_checks badge to check');
|
||||
assert.equal(Number(badge[1]), sum,
|
||||
`README's badge says ${badge[1]} selftest checks; the five suites reported ${sum}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue