Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b278dbd23d | |||
| 6f4908a5dc | |||
| 87fb78334e | |||
| f079debdee | |||
| 0008dbbc3a | |||
| 20fcd67f4a | |||
| 5e5bc4a66e | |||
| 671e275a97 | |||
| a1ef1fb555 | |||
| 9a154950eb | |||
| b57a1ea286 |
14 changed files with 1368 additions and 302 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.33.1",
|
||||
"version": "0.35.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"
|
||||
|
|
|
|||
170
CHANGELOG.md
170
CHANGELOG.md
|
|
@ -5,6 +5,176 @@ 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.35.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
|
||||
- **`PLAN` column in the board table, and `plan=` in `--row`.** A filesystem
|
||||
reading of the repo root: `-` = no file named exactly `PLAN.md`; `Nd` =
|
||||
whole days the plan's mtime lies behind the last commit (`0d` when as new or
|
||||
newer); `?` = a plan with no commit to compare against. A symlinked
|
||||
`PLAN.md` is aged by its target. The content is never
|
||||
read, and the column does not judge whether a criterion can be falsified -
|
||||
the legend says so. The name is matched byte for byte, because APFS's
|
||||
case-insensitivity makes `[ -f PLAN.md ]` true for `plan.md`. Display only:
|
||||
not a sort key, not in `--plan`, not summed. board-selftest section 34
|
||||
(written red before the column existed) pins all of it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`board.sh --dispatch` put three other record fields in front of `neste=`.**
|
||||
The index that takes "the last field onward" was written when the record had
|
||||
14 fields and was never moved as ORDRE, FLY and VOY were inserted before
|
||||
`neste`, so a dispatch block read `neste=0|-|0|<the NESTE prose>`. It now
|
||||
takes the field after the last column onward (field 18, since the `PLAN`
|
||||
column below moved it again), and a selftest check pins the exact start of
|
||||
the prose so the next inserted field turns it red.
|
||||
- **`--row` and `--dispatch` for a repo with no STATE.md carried shifted
|
||||
values.** That record is written by its own printf, which lacked the `VOY`
|
||||
field, so `--row` read `voy=(ingen STATE.md)` and an empty `neste=`. The
|
||||
record is now complete; a fixture with no STATE.md pins it.
|
||||
- **`--inbox-plan` cut NESTE at its first `|`.** It read `neste` as one field
|
||||
instead of "that field onward"; the whole prose now comes through, as it
|
||||
already did in `--row` and `--dispatch`.
|
||||
- **README said "macOS or Linux".** `board.sh` uses BSD `stat` throughout and
|
||||
renders no rows under GNU `stat`; the requirement now says macOS.
|
||||
|
||||
## [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
|
||||
|
|
|
|||
192
CLAUDE.md
192
CLAUDE.md
|
|
@ -467,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` (393 checks).
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (478 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
|
||||
|
|
@ -639,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
|
||||
|
|
@ -1066,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
|
||||
|
|
@ -1398,6 +1417,95 @@ marketplace plugin. Three components, one boundary:
|
|||
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.
|
||||
|
||||
- **`PLAN` is a table column read from the FILESYSTEM, never from the file**
|
||||
(operator decision 2026-09-17). `-` = no file named exactly `PLAN.md` in the
|
||||
repo root; `Nd` = whole days its mtime lies behind the last commit; `?` = a
|
||||
plan with no commit to compare against. It answers where a destination is
|
||||
missing or untouched and nothing else - it does not judge whether a criterion
|
||||
can be falsified, and the legend says so. Three alternatives were weighed
|
||||
first and rejected on measurement, not taste: a free-text `goal=` field
|
||||
(the case that motivated this HAD a goal in words; a field cannot tell words
|
||||
from a falsifiable criterion), a mandatory acceptance flag on orders (every
|
||||
order in that case was precisely specified - the gap was at version level),
|
||||
and an "activity without a release" heuristic (a release silenced it on the
|
||||
one known case, three days before the loop was diagnosed).
|
||||
|
||||
**The name is matched BYTE FOR BYTE through a glob (`"$d"/PLAN.m[d]` plus a
|
||||
basename compare), never by `[ -f PLAN.md ]`.** APFS is case-insensitive by
|
||||
default, so that test is true for `plan.md` and would report a plan that is
|
||||
not there; a glob matches the names readdir returns, as stored, and costs no
|
||||
subprocess. Selftest section 34 asserts the trap is ARMED on the machine
|
||||
running it, so the byte-exact check is measuring something. Display only:
|
||||
not a sort key, not in `--plan`, not in SUM - pinned by swapping two repos'
|
||||
PLAN values between runs and requiring the same order.
|
||||
|
||||
**The checks were written RED first, in a separate file, and moved into
|
||||
board-selftest once the column passed them unchanged.** Red checks inside
|
||||
the suite would have turned main red and broken the README count check, so
|
||||
"red for the right reason" could not have been told apart from anything
|
||||
else. The separate file is gone - two judges for one column would drift.
|
||||
Mutation-verified against four broken variants (case-blind test, sorting on
|
||||
the field, a `plan=` key in `--plan`, a legend without IKKE), each turning
|
||||
exactly one check red. Adding the field before `neste` exposed that
|
||||
`--dispatch` had taken `neste` from field 14 onward since the record had 14
|
||||
fields, gluing three columns onto the prose; fixed separately, and every
|
||||
"last field onward" index now moves with the record (17 -> 18 here). F6
|
||||
counts DRT from the right, so its `NF-3` became `NF-4` for the same reason.
|
||||
|
||||
**An independent review then ran 16 mutants against the 461 checks and 5
|
||||
survived** - three of them the same class as the dispatch defect (`--row`
|
||||
and `--inbox-plan` reading `neste` from a stale index, and the no-STATE.md
|
||||
record one field short), one the column's DEFINITION (days behind now
|
||||
instead of behind the last commit - every fixture's commit was close enough
|
||||
to now that the two coincided), and one the `?` branch. Section 34 part 8
|
||||
now has a fixture for each, and all 16 die. Every "last field onward"
|
||||
reader is pinned on the EXACT start of the prose with a `|` inside it,
|
||||
because a reader taking one field instead of "onward" was a second,
|
||||
pre-existing defect in `--inbox-plan` that the same fixture exposed.
|
||||
A symlinked PLAN.md is aged by its target (`stat -L`). Stated, not closed:
|
||||
the 34-character nested key still shifts its row (the section's own header
|
||||
splitter would misread such a row; fixture names are short on purpose), and
|
||||
the "trap is armed" fixture check is red on a case-sensitive volume - this
|
||||
repo is macOS-only (BSD `stat`), and a case-sensitive APFS volume is the
|
||||
remaining gap.
|
||||
|
||||
- **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 —
|
||||
|
|
@ -1446,7 +1554,7 @@ obligations in another repo.
|
|||
builtins only in hook and tests.
|
||||
- TDD: no behavior change without a failing selftest check first.
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (257/257),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (393/393),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (478/478),
|
||||
`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).
|
||||
|
|
@ -1464,6 +1572,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
|
||||
|
|
@ -1471,7 +1609,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`
|
||||
|
|
|
|||
25
README.md
25
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.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
@ -115,6 +115,7 @@ Ownership is explicit rather than implied. An order is pending until a session *
|
|||
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
|
||||
|
|
@ -143,13 +144,17 @@ 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` 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.
|
||||
**`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 --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`.
|
||||
**`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`.
|
||||
|
||||
**The table's `PLAN` column shows whether a repo keeps a destination where sessions actually look.** It is a filesystem reading and nothing more: `-` means no file named exactly `PLAN.md` in the repo root, `Nd` means the plan's mtime lies `N` whole days behind the last commit (`0d` when it is as new or newer), and `?` means a plan with no commit to compare against; a symlinked `PLAN.md` is aged by its target. The file's content is never read, so no wording inside it can move the cell — and the column does not judge whether the criterion inside can be falsified. The name is matched **byte for byte** through a glob rather than with `[ -f PLAN.md ]`, because APFS is case-insensitive by default and that test is true for a `plan.md`. It is display only: never a sort key, never in `--plan`, never summed; `--row` carries it as `plan=`. `touch` and a checkout reset the mtime, the same caveat `ALDER` carries. It replaced nothing: a heuristic on "activity without a release" was measured and rejected first, because a release made it go silent on the one known case it existed to catch.
|
||||
|
||||
**`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.
|
||||
|
||||
|
|
@ -189,18 +194,18 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
|
|||
|
||||
## Requirements
|
||||
|
||||
- macOS or Linux with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only).
|
||||
- macOS with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only). `board.sh` uses BSD `stat` (`stat -f`), so it does not run on Linux as written.
|
||||
- Node.js >= 18 for the SessionStart and PreToolUse hooks (zero npm dependencies).
|
||||
- `git` is required to derive repo identity automatically. Without it, pass `--from`/`--repo` explicitly; the engine refuses to guess an identity from the working directory.
|
||||
|
||||
## Development
|
||||
|
||||
bash scripts/coord-selftest.sh # 220 checks against a throwaway mailbox
|
||||
bash scripts/board-selftest.sh # 360 checks against a throwaway repo tree
|
||||
bash scripts/coord-selftest.sh # 257 checks against a throwaway mailbox
|
||||
bash scripts/board-selftest.sh # 478 checks against a throwaway repo tree
|
||||
bash scripts/route-selftest.sh # 73 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/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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.33.1",
|
||||
"version": "0.35.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -581,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
|
||||
|
|
@ -900,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
|
||||
|
|
@ -932,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 ]
|
||||
|
|
@ -1530,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
|
||||
|
|
@ -1766,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,24 +1788,41 @@ printf '%s\n' "$d8" | grep -q '^tab=1$'; check "dispatch: plan block carries tab
|
|||
printf '%s\n' "$d8" | grep -q '^repo=repo-a$'; check "dispatch: plan block carries repo=" $?
|
||||
printf '%s\n' "$d8" | grep -q "^dir=$ROOT/repo-a\$"; check "dispatch: plan block carries dir=" $?
|
||||
printf '%s\n' "$d8" | grep -q '^paste=cd '; check "dispatch: plan block carries a paste= that cds first" $?
|
||||
# neste= is the record's LAST field, taken as "field N onward". The index was
|
||||
# written when the record had 14 fields and never moved as ORDRE, FLY and VOY
|
||||
# were inserted before it, so the shipped block read `neste=0|-|0|Lukk ...` -
|
||||
# three other columns glued onto the prose. Pinned on the exact start of
|
||||
# repo-a's NESTE, so any future field added before `neste` without moving this
|
||||
# index turns the check red instead of silently growing the prefix.
|
||||
printf '%s\n' "$d8" | grep -q '^neste=Lukk to MAJOR fra reviewen'
|
||||
check "dispatch: neste= carries the NESTE prose alone, no other record fields in front of it" $?
|
||||
# The prompt reaches the session through argv. A bare model+effort command
|
||||
# forces the operator to type Go, and the session must then GUESS the task out
|
||||
# of STATE instead of being handed it - measured twice on 2026-08-16.
|
||||
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
|
||||
|
|
@ -2133,7 +2155,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" $?
|
||||
|
|
@ -2357,19 +2379,19 @@ check "F6: fixture ground truth - git really does fail on the broken worktree" $
|
|||
|
||||
F6TBL="$(CLAUDE_COORD_DIR="$F6COORD" "$BOARD" --roots "$F6ROOT" 2>/dev/null)"
|
||||
|
||||
# DRT is counted from the RIGHT (NF-3: DRT ALDER SISTE NESTE), never from the
|
||||
# DRT is counted from the RIGHT (NF-4: DRT ALDER SISTE PLAN NESTE), never from the
|
||||
# left: the KOST field is free text and its realistic spelling ("Sonnet 5/low")
|
||||
# contains a space, so $7 reads FLY instead. Measured, not assumed - the first
|
||||
# cut of this section passed its clean-tree control by accident that way.
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '0'
|
||||
check "F6: control - a healthy CLEAN tree still reads DRT 0" $?
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '1'
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '1'
|
||||
check "F6: control - a healthy DIRTY tree still reads DRT 1" $?
|
||||
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '0'
|
||||
[ $? -ne 0 ]
|
||||
check "F6: a git failure does NOT read as DRT 0 (clean tree)" $?
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '?'
|
||||
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '?'
|
||||
check "F6: a git failure reads as ? (not measured), the same token coord-count uses" $?
|
||||
|
||||
# The load-bearing consequence. All three fixtures are status=done with no mail
|
||||
|
|
@ -3252,6 +3274,495 @@ 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
|
||||
|
||||
|
||||
# --- 34. PLAN column: a filesystem reading, display only --------------------
|
||||
# Operator decision 2026-09-17. The cell answers one question: does this repo
|
||||
# keep a destination where sessions actually look (a root PLAN.md), and is
|
||||
# anyone touching it? `-` = no file named exactly PLAN.md in the root; `Nd` =
|
||||
# whole days its mtime lies behind the last commit. It never reads the file,
|
||||
# never enters a sort, a --plan block or SUM, and does not judge whether a
|
||||
# criterion inside it can be falsified - the legend says so.
|
||||
#
|
||||
# These checks were written RED in a separate file before the column existed
|
||||
# (24 of 33 failing because the column was missing, fixture ground truth
|
||||
# green), kept out of this suite so main stayed green, and moved here once the
|
||||
# column passed them unchanged. That file is gone: two judges for one column
|
||||
# would drift. Mutation-verified against four broken variants, each turning
|
||||
# exactly one check red: a case-blind `[ -f PLAN.md ]`, sorting rows on the
|
||||
# plan field, a plan= key in the --plan blocks, and a legend without IKKE.
|
||||
#
|
||||
# Table cells are located through the HEADER (split on runs of 2+ spaces),
|
||||
# never a fixed offset, so a later column move does not break this section.
|
||||
PC_ROOT="$(mktemp -d)"
|
||||
PC_COORD="$(mktemp -d)"
|
||||
PC_NOW="$(date +%s)"
|
||||
PC_DAY=86400
|
||||
|
||||
pc_state() {
|
||||
{
|
||||
echo "# STATE - $2"
|
||||
echo "## NESTE - START HER"
|
||||
echo "<!-- board: status=$3; blocked-on=-; next-cost=Sonnet 5/high -->"
|
||||
echo "${4:-Next step for $2.}"
|
||||
} > "$1/STATE.md"
|
||||
}
|
||||
# pc_commit_at <dir> <epoch> <msg>: commit everything with a fixed committer date.
|
||||
pc_commit_at() {
|
||||
git -C "$1" add -A >/dev/null 2>&1
|
||||
GIT_AUTHOR_DATE="@$2 +0000" GIT_COMMITTER_DATE="@$2 +0000" \
|
||||
git -C "$1" -c user.email=t@t -c user.name=t commit -qm "$3" >/dev/null 2>&1
|
||||
}
|
||||
pc_touch_at() { touch -t "$(date -r "$2" +%Y%m%d%H%M.%S)" "$1"; }
|
||||
pc_last_commit() { git -C "$1" log -1 --format=%ct 2>/dev/null; }
|
||||
pc_mtime() { stat -f %m "$1"; }
|
||||
|
||||
# pc_table_cell <table-output> <repo>: the PLAN cell of <repo>'s row, read under
|
||||
# the nearest preceding header line. Empty when the header has no PLAN column.
|
||||
pc_table_cell() {
|
||||
printf '%s\n' "$1" | awk -v repo="$2" '
|
||||
function split2(s, arr, n) { gsub(/ +/, "\t", s); sub(/^\t/, "", s); return split(s, arr, "\t") }
|
||||
/^REPO / { n = split2($0, h); col = 0; for (i = 1; i <= n; i++) if (h[i] == "PLAN") col = i; next }
|
||||
$1 == repo && col > 0 { split2($0, r); print r[col]; exit }'
|
||||
}
|
||||
pc_row_plan() { printf '%s\n' "$1" | sed -n 's/^plan=//p'; }
|
||||
pc_board() { CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_ROOT" "$@" 2>/dev/null; }
|
||||
|
||||
|
||||
# --- Fixtures ---------------------------------------------------------------
|
||||
|
||||
# p-none: no PLAN.md anywhere.
|
||||
mkrepo "$PC_ROOT/p-none"; pc_state "$PC_ROOT/p-none" p-none planned
|
||||
pc_commit_at "$PC_ROOT/p-none" $((PC_NOW - 3 * PC_DAY)) init
|
||||
|
||||
# p-fresh: PLAN.md newer than the last commit -> 0d. The file is EMPTY: a value
|
||||
# computed from content could not produce 0d here, so this also shows that
|
||||
# the column is a filesystem reading.
|
||||
mkrepo "$PC_ROOT/p-fresh"; pc_state "$PC_ROOT/p-fresh" p-fresh planned
|
||||
pc_commit_at "$PC_ROOT/p-fresh" $((PC_NOW - 2 * PC_DAY)) init
|
||||
: > "$PC_ROOT/p-fresh/PLAN.md"
|
||||
pc_touch_at "$PC_ROOT/p-fresh/PLAN.md" "$PC_NOW"
|
||||
|
||||
# p-stale: PLAN.md 5 days and 3 hours older than the last commit -> 5d. Its
|
||||
# content looks like a value on purpose ("PLAN=99d"); it must not be read.
|
||||
# Its NESTE carries a literal '|' and it holds one unhandled message, so the
|
||||
# same repo pins where every "last field onward" reader starts AND that it
|
||||
# really reads onward (section 8 below).
|
||||
mkrepo "$PC_ROOT/p-stale"
|
||||
pc_state "$PC_ROOT/p-stale" p-stale planned "Next step for p-stale | then the rest."
|
||||
mkdir -p "$PC_COORD/p-stale/inbox"
|
||||
printf -- '---\nfrom: x\nto: p-stale\nsubject: s\nreply-expected: yes\n---\nm\n' \
|
||||
> "$PC_COORD/p-stale/inbox/20260901T101010Z-1-from-x.md"
|
||||
printf '# PLAN\nPLAN=99d\n' > "$PC_ROOT/p-stale/PLAN.md"
|
||||
PC_P_STALE_CT=$((PC_NOW - 3600))
|
||||
pc_commit_at "$PC_ROOT/p-stale" "$PC_P_STALE_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-stale/PLAN.md" $((PC_P_STALE_CT - 5 * PC_DAY - 3 * 3600))
|
||||
|
||||
# p-docs: PLAN.md in docs/ only - the wrong place by convention.
|
||||
mkrepo "$PC_ROOT/p-docs"; pc_state "$PC_ROOT/p-docs" p-docs planned
|
||||
mkdir -p "$PC_ROOT/p-docs/docs"; echo "# PLAN" > "$PC_ROOT/p-docs/docs/PLAN.md"
|
||||
pc_commit_at "$PC_ROOT/p-docs" $((PC_NOW - PC_DAY)) init
|
||||
|
||||
# p-lower: lowercase plan.md in the root. On a case-insensitive filesystem
|
||||
# (APFS default) `[ -f PLAN.md ]` is TRUE here, so a board that asks the
|
||||
# filesystem alone would report a plan that is not there. The name has to be
|
||||
# compared byte for byte against what the directory actually stores (e.g. a
|
||||
# directory listing matched exactly against PLAN.md).
|
||||
mkrepo "$PC_ROOT/p-lower"; pc_state "$PC_ROOT/p-lower" p-lower planned
|
||||
echo "# plan" > "$PC_ROOT/p-lower/plan.md"
|
||||
pc_commit_at "$PC_ROOT/p-lower" $((PC_NOW - PC_DAY)) init
|
||||
|
||||
# The next four fixtures exist because an independent review ran 16 mutants of
|
||||
# the column against this suite and 5 survived. Each fixture below is the one
|
||||
# a surviving mutant could not get past.
|
||||
|
||||
# p-nostate: a repo with NO STATE.md. Its record is written by a separate
|
||||
# printf, which is exactly where a field was once missing (0.34.0 printed
|
||||
# voy=(ingen STATE.md) and an empty neste=), so a short record must show here.
|
||||
# The last commit is 40 days old.
|
||||
mkrepo "$PC_ROOT/p-nostate"
|
||||
: > "$PC_ROOT/p-nostate/PLAN.md"
|
||||
PC_NS_CT=$((PC_NOW - 40 * PC_DAY))
|
||||
pc_commit_at "$PC_ROOT/p-nostate" "$PC_NS_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-nostate/PLAN.md" $((PC_NS_CT - 3 * PC_DAY - 3600))
|
||||
|
||||
# p-old: the column's DEFINITION. The last commit is 30 days old, so "days
|
||||
# behind the last commit" (10) and "days behind now" (40) are different
|
||||
# numbers; every fixture above has a commit close enough to now that the two
|
||||
# readings coincide.
|
||||
mkrepo "$PC_ROOT/p-old"; pc_state "$PC_ROOT/p-old" p-old planned
|
||||
: > "$PC_ROOT/p-old/PLAN.md"
|
||||
PC_OLD_CT=$((PC_NOW - 30 * PC_DAY))
|
||||
pc_commit_at "$PC_ROOT/p-old" "$PC_OLD_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-old/PLAN.md" $((PC_OLD_CT - 10 * PC_DAY - 5 * 3600))
|
||||
|
||||
# p-nocommit: a plan and no commit at all. There is nothing to lag behind, so
|
||||
# the honest cell is '?' - never '-', which would claim there is no plan.
|
||||
mkrepo "$PC_ROOT/p-nocommit"; pc_state "$PC_ROOT/p-nocommit" p-nocommit planned
|
||||
: > "$PC_ROOT/p-nocommit/PLAN.md"
|
||||
|
||||
# p-link: PLAN.md is a symlink. The cell must read the PLAN's age, i.e. the
|
||||
# target's mtime; the link's own mtime is when the link was made, and reading
|
||||
# it turns an 8-day-old plan into 0d.
|
||||
mkrepo "$PC_ROOT/p-link"; pc_state "$PC_ROOT/p-link" p-link planned
|
||||
mkdir -p "$PC_ROOT/p-link/docs"; echo "# PLAN" > "$PC_ROOT/p-link/docs/real-plan.md"
|
||||
ln -s docs/real-plan.md "$PC_ROOT/p-link/PLAN.md"
|
||||
PC_LINK_CT=$((PC_NOW - 2 * 3600))
|
||||
pc_commit_at "$PC_ROOT/p-link" "$PC_LINK_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-link/docs/real-plan.md" $((PC_LINK_CT - 8 * PC_DAY - 3600))
|
||||
touch -h "$PC_ROOT/p-link/PLAN.md"
|
||||
|
||||
# --- Ground truth for the fixtures (these must be green today) -------------
|
||||
[ -z "$(git -C "$PC_ROOT/p-stale" status --porcelain)" ]
|
||||
check "plan: fixture - p-stale is committed clean" $?
|
||||
PC_P_STALE_EXP="$(( ($(pc_last_commit "$PC_ROOT/p-stale") - $(pc_mtime "$PC_ROOT/p-stale/PLAN.md")) / PC_DAY ))d"
|
||||
[ "$PC_P_STALE_EXP" != "0d" ]
|
||||
check "plan: fixture - p-stale's expected value is computed from its timestamps and is not 0d ($PC_P_STALE_EXP)" $?
|
||||
[ "$(pc_mtime "$PC_ROOT/p-fresh/PLAN.md")" -ge "$(pc_last_commit "$PC_ROOT/p-fresh")" ]
|
||||
check "plan: fixture - p-fresh's PLAN.md is newer than its last commit" $?
|
||||
if [ -f "$PC_ROOT/p-lower/PLAN.md" ]; then PC_LOWER_ARMED=0; else PC_LOWER_ARMED=1; fi
|
||||
check "plan: fixture - the case trap is armed here - [ -f PLAN.md ] is true for plan.md (case-insensitive fs)" "$PC_LOWER_ARMED"
|
||||
ls -1 "$PC_ROOT/p-lower" | grep -qx 'PLAN.md'; PC_rc=$?
|
||||
[ "$PC_rc" -ne 0 ]; check "plan: fixture - a byte-exact directory listing does NOT see PLAN.md in p-lower" $?
|
||||
|
||||
PC_TABLE="$(pc_board)"
|
||||
PC_N_REPOS="$(printf '%s\n' "$PC_TABLE" | sed -n 's/^BOARD .*(\([0-9][0-9]*\) repo).*/\1/p' | head -1)"
|
||||
[ "${PC_N_REPOS:-0}" -eq 9 ]; check "plan: fixture - board scanned the 9 fixture repos (header says ${PC_N_REPOS:-nothing})" $?
|
||||
|
||||
# --- 1. Three different values from three repos ----------------------------
|
||||
printf '%s\n' "$PC_TABLE" | grep -q '^REPO .* PLAN '
|
||||
check "plan 1: the table header carries a PLAN column" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-none)" = "-" ]; check "plan 1: no root PLAN.md -> '-'" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-fresh)" = "0d" ]; check "plan 1: PLAN.md newer than the last commit -> '0d'" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-stale)" = "$PC_P_STALE_EXP" ]
|
||||
check "plan 1: PLAN.md older than the last commit -> '$PC_P_STALE_EXP' (from the fixture's own timestamps)" $?
|
||||
PC_V3="$(pc_table_cell "$PC_TABLE" p-none) $(pc_table_cell "$PC_TABLE" p-fresh) $(pc_table_cell "$PC_TABLE" p-stale)"
|
||||
[ "$(printf '%s\n' $PC_V3 | sort -u | wc -l | tr -d ' ')" -eq 3 ]
|
||||
check "plan 1: the three fixtures show three DIFFERENT values ($PC_V3)" $?
|
||||
|
||||
# --- 2. Wrong place and wrong case are not a plan ---------------------------
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-docs)" = "-" ]; check "plan 2: docs/PLAN.md only -> '-'" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-lower)" = "-" ]; check "plan 2: lowercase plan.md in the root -> '-' (byte-exact name)" $?
|
||||
|
||||
# --- 3. --row carries plan= with the table's value ---------------------------
|
||||
for PC_r in p-none p-fresh p-stale p-docs p-lower p-nostate p-old p-nocommit p-link; do
|
||||
PC_RV="$(pc_row_plan "$(pc_board --row "$PC_r")")"
|
||||
PC_TV="$(pc_table_cell "$PC_TABLE" "$PC_r")"
|
||||
[ -n "$PC_RV" ] && [ "$PC_RV" = "$PC_TV" ]
|
||||
check "plan 3: --row $PC_r has plan=${PC_RV:-<missing>} equal to the table cell (${PC_TV:-<missing>})" $?
|
||||
done
|
||||
[ "$(pc_board --row p-none | grep -c '^plan=')" -eq 1 ]
|
||||
check "plan 3: --row emits exactly one plan= line" $?
|
||||
[ "$(pc_board --row p-none | tail -1 | cut -d= -f1)" = "neste" ]
|
||||
check "plan 3: neste= is still the last --row line" $?
|
||||
|
||||
# --- 4. Not in the sort, not in --plan blocks, not summed --------------------
|
||||
# Two repos identical in every sorted property; run 1 gives s-a the stale plan
|
||||
# and s-b the fresh one, run 2 swaps them. Any sort key built on PLAN would
|
||||
# reverse them in one of the two runs.
|
||||
PC_SROOT="$(mktemp -d)"
|
||||
for PC_r in s-a s-b; do
|
||||
mkrepo "$PC_SROOT/$PC_r"; pc_state "$PC_SROOT/$PC_r" "$PC_r" planned
|
||||
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=x -->" >> "$PC_SROOT/$PC_r/STATE.md"
|
||||
: > "$PC_SROOT/$PC_r/PLAN.md"
|
||||
pc_commit_at "$PC_SROOT/$PC_r" $((PC_NOW - PC_DAY)) init
|
||||
pc_touch_at "$PC_SROOT/$PC_r/STATE.md" $((PC_NOW - 2 * PC_DAY))
|
||||
done
|
||||
pc_touch_at "$PC_SROOT/s-a/PLAN.md" $((PC_NOW - 9 * PC_DAY)); pc_touch_at "$PC_SROOT/s-b/PLAN.md" "$PC_NOW"
|
||||
PC_T1="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" 2>/dev/null)"; PC_P1="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" --plan 2>/dev/null)"
|
||||
pc_touch_at "$PC_SROOT/s-a/PLAN.md" "$PC_NOW"; pc_touch_at "$PC_SROOT/s-b/PLAN.md" $((PC_NOW - 9 * PC_DAY))
|
||||
PC_T2="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" 2>/dev/null)"; PC_P2="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" --plan 2>/dev/null)"
|
||||
PC_A1="$(pc_table_cell "$PC_T1" s-a)"; PC_A2="$(pc_table_cell "$PC_T2" s-a)"
|
||||
[ -n "$PC_A1" ] && [ -n "$PC_A2" ] && [ "$PC_A1" != "$PC_A2" ]
|
||||
check "plan 4: precondition - the swap really changed s-a's PLAN value (${PC_A1:-<missing>} -> ${PC_A2:-<missing>})" $?
|
||||
PC_O1="$(printf '%s\n' "$PC_T1" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
|
||||
PC_O2="$(printf '%s\n' "$PC_T2" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
|
||||
[ -n "$PC_A1" ] && [ "$PC_O1" = "$PC_O2" ]; check "plan 4: table order is identical across the swap ($PC_O1| $PC_O2)" $?
|
||||
PC_Q1="$(printf '%s\n' "$PC_P1" | sed -n 's/^repo=//p' | tr '\n' ' ')"
|
||||
PC_Q2="$(printf '%s\n' "$PC_P2" | sed -n 's/^repo=//p' | tr '\n' ' ')"
|
||||
[ -n "$PC_A1" ] && [ -n "$PC_Q1" ] && [ "$PC_Q1" = "$PC_Q2" ]; check "plan 4: --plan order is identical across the swap ($PC_Q1| $PC_Q2)" $?
|
||||
if printf '%s\n' "$PC_P1" | grep -qi '^plan='; then PC_rc=1; else PC_rc=0; fi
|
||||
[ -n "$PC_A1" ] && [ "$PC_rc" -eq 0 ]; check "plan 4: --plan blocks carry no plan= key" $?
|
||||
PC_S1="$(printf '%s\n' "$PC_T1" | grep '^SUM:')"; PC_S2="$(printf '%s\n' "$PC_T2" | grep '^SUM:')"
|
||||
if printf '%s' "$PC_S1" | grep -qi 'plan'; then PC_rc=1; else PC_rc=0; fi
|
||||
[ -n "$PC_A1" ] && [ "$PC_rc" -eq 0 ] && [ "$PC_S1" = "$PC_S2" ]; check "plan 4: the SUM line neither mentions PLAN nor moves with it" $?
|
||||
/bin/rm -rf "$PC_SROOT" 2>/dev/null
|
||||
|
||||
# --- 5. Replay: the shape C failed on ----------------------------------------
|
||||
# Many commits, no root PLAN.md, the destination written in docs/*.local.md,
|
||||
# and a release tag late in the window. A heuristic keyed on "activity without
|
||||
# a release" went silent at the tag; this column must read '-' at every step.
|
||||
PC_LROOT="$(mktemp -d)"
|
||||
PC_L="$PC_LROOT/loop-repo"
|
||||
mkrepo "$PC_L"; pc_state "$PC_L" loop-repo in-progress
|
||||
mkdir -p "$PC_L/docs"; echo "destination lives here" > "$PC_L/docs/destination.local.md"
|
||||
PC_BASE=$((PC_NOW - 60 * PC_DAY)); PC_HITS=0; PC_STEPS=0
|
||||
PC_i=0
|
||||
while [ "$PC_i" -lt 6 ]; do
|
||||
echo "step $PC_i" > "$PC_L/work.txt"
|
||||
pc_commit_at "$PC_L" $((PC_BASE + PC_i * 10 * PC_DAY)) "step $PC_i"
|
||||
[ "$PC_i" -eq 4 ] && git -C "$PC_L" -c tag.gpgSign=false tag v9.9.0 >/dev/null 2>&1
|
||||
PC_LT="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_LROOT" 2>/dev/null)"
|
||||
PC_STEPS=$((PC_STEPS + 1))
|
||||
[ "$(pc_table_cell "$PC_LT" loop-repo)" = "-" ] && PC_HITS=$((PC_HITS + 1))
|
||||
PC_i=$((PC_i + 1))
|
||||
done
|
||||
[ -n "$(git -C "$PC_L" tag -l v9.9.0)" ]; check "plan 5: fixture - the release tag exists in the replay" $?
|
||||
[ "$PC_HITS" -eq "$PC_STEPS" ]; check "plan 5: '-' at every step of the replay, before and after the tag ($PC_HITS of $PC_STEPS)" $?
|
||||
/bin/rm -rf "$PC_LROOT" 2>/dev/null
|
||||
|
||||
# --- 6. Denominator: one value per scanned repo ------------------------------
|
||||
PC_WITH=0
|
||||
for PC_r in p-none p-fresh p-stale p-docs p-lower p-nostate p-old p-nocommit p-link; do
|
||||
[ -n "$(pc_table_cell "$PC_TABLE" "$PC_r")" ] && PC_WITH=$((PC_WITH + 1))
|
||||
done
|
||||
# A non-empty, non-zero denominator first: with an empty scan both sides are 0
|
||||
# and the comparison alone would pass on nothing.
|
||||
[ "${PC_N_REPOS:-0}" -gt 0 ] && [ "$PC_WITH" -eq "$PC_N_REPOS" ]
|
||||
check "plan 6: every scanned repo has a PLAN value ($PC_WITH of ${PC_N_REPOS:-?})" $?
|
||||
printf '%s\n' "$PC_TABLE" | grep -q '^undersoekt: '
|
||||
check "plan 6: the scan's own denominator line is still printed" $?
|
||||
|
||||
# --- 7. Legend -----------------------------------------------------------------
|
||||
PC_LEG="$(printf '%s\n' "$PC_TABLE" | awk '/^PLAN = /{on=1} on && /^[A-Z]+ = / && !/^PLAN = /{on=0} on')"
|
||||
[ -n "$PC_LEG" ]; check "plan 7: the legend has a 'PLAN = ' entry" $?
|
||||
printf '%s' "$PC_LEG" | grep -q 'IKKE' && printf '%s' "$PC_LEG" | grep -qi 'felle'
|
||||
check "plan 7: the legend says the column does NOT judge whether the criterion can be falsified" $?
|
||||
printf '%s' "$PC_LEG" | grep -q 'touch' && printf '%s' "$PC_LEG" | grep -q 'checkout'
|
||||
check "plan 7: the legend says touch/checkout reset the mtime" $?
|
||||
if printf '%s' "$PC_LEG" | LC_ALL=C grep -q '[^ -~]'; then PC_rc=1; else PC_rc=0; fi
|
||||
[ -n "$PC_LEG" ] && [ "$PC_rc" -eq 0 ]; check "plan 7: the legend is ASCII like the rest of the table" $?
|
||||
|
||||
# --- 8. What the surviving mutants got past --------------------------------
|
||||
# neste= start, pinned EXACTLY in the two renderings that take "the last field
|
||||
# onward" besides --dispatch (which has its own check in section 18). A field
|
||||
# inserted before `neste` with an index left behind shows up as a prefix
|
||||
# ("5d|Next step ..."), and a reader that takes ONE field instead of "onward"
|
||||
# cuts the prose at its first '|'.
|
||||
PC_NESTE='Next step for p-stale | then the rest.'
|
||||
pc_board --row p-stale | grep -qxF "neste=$PC_NESTE"
|
||||
check "plan 8: --row neste= is the NESTE prose exactly, '|' included" $?
|
||||
PC_IP="$(pc_board --inbox-plan)"
|
||||
printf '%s\n' "$PC_IP" | grep -qx 'repo=p-stale'
|
||||
check "plan 8: fixture - --inbox-plan has a block for p-stale" $?
|
||||
printf '%s\n' "$PC_IP" | awk 'BEGIN{RS=""} /(^|\n)repo=p-stale(\n|$)/' | grep -qxF "neste=$PC_NESTE"
|
||||
check "plan 8: --inbox-plan neste= is the NESTE prose exactly, '|' included" $?
|
||||
|
||||
# A repo with no STATE.md still gets its measured value, and its neste= is the
|
||||
# placeholder rather than an empty line or a value shifted from another field.
|
||||
PC_NS_EXP="$(( (PC_NS_CT - $(pc_mtime "$PC_ROOT/p-nostate/PLAN.md")) / PC_DAY ))d"
|
||||
PC_NS_ROW="$(pc_board --row p-nostate)"
|
||||
printf '%s\n' "$PC_NS_ROW" | grep -qx "plan=$PC_NS_EXP"
|
||||
check "plan 8: no STATE.md - --row plan= is the measured value ($PC_NS_EXP)" $?
|
||||
printf '%s\n' "$PC_NS_ROW" | grep -qxF 'neste=(ingen STATE.md)'
|
||||
check "plan 8: no STATE.md - --row neste= is the placeholder, not empty or shifted" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-nostate)" = "$PC_NS_EXP" ]
|
||||
check "plan 8: no STATE.md - the table cell is the measured value too" $?
|
||||
|
||||
# The definition: behind the LAST COMMIT, not behind now.
|
||||
PC_OLD_EXP="$(( (PC_OLD_CT - $(pc_mtime "$PC_ROOT/p-old/PLAN.md")) / PC_DAY ))d"
|
||||
PC_OLD_NOW="$(( (PC_NOW - $(pc_mtime "$PC_ROOT/p-old/PLAN.md")) / PC_DAY ))d"
|
||||
[ "$PC_OLD_EXP" != "$PC_OLD_NOW" ]
|
||||
check "plan 8: fixture - p-old separates 'behind commit' ($PC_OLD_EXP) from 'behind now' ($PC_OLD_NOW)" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-old)" = "$PC_OLD_EXP" ]
|
||||
check "plan 8: Nd counts days behind the LAST COMMIT, not behind now ($PC_OLD_EXP)" $?
|
||||
|
||||
# No commit: '?' in both renderings.
|
||||
git -C "$PC_ROOT/p-nocommit" log -1 >/dev/null 2>&1
|
||||
[ $? -ne 0 ]; check "plan 8: fixture - p-nocommit really has no commit" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-nocommit)" = "?" ]
|
||||
check "plan 8: a plan with no commit reads '?', never '-'" $?
|
||||
pc_board --row p-nocommit | grep -qxF 'plan=?'
|
||||
check "plan 8: --row carries the same '?'" $?
|
||||
|
||||
# Symlink: the target's age, not the link's.
|
||||
PC_LINK_EXP="$(( (PC_LINK_CT - $(pc_mtime "$PC_ROOT/p-link/docs/real-plan.md")) / PC_DAY ))d"
|
||||
[ "$(stat -f %m "$PC_ROOT/p-link/PLAN.md")" -ge "$PC_LINK_CT" ] && [ "$PC_LINK_EXP" != "0d" ]
|
||||
check "plan 8: fixture - the link itself is newer than the commit, its target is $PC_LINK_EXP behind" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-link)" = "$PC_LINK_EXP" ]
|
||||
check "plan 8: a symlinked PLAN.md reads the TARGET's age ($PC_LINK_EXP), not the link's" $?
|
||||
|
||||
/bin/rm -rf "$PC_ROOT" "$PC_COORD" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "board-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
254
scripts/board.sh
254
scripts/board.sh
|
|
@ -9,6 +9,9 @@
|
|||
# STATE.md board line - optional machine-readable field (see below)
|
||||
# git status --porcelain - uncommitted risk
|
||||
# git log -1 --format=%ct - when anything last landed (the SISTE column)
|
||||
# <repo>/PLAN.md mtime - the PLAN column: existence (byte-exact name) and
|
||||
# days behind the last commit. Never its content.
|
||||
# DISPLAY only and not in the sort.
|
||||
# ~/.claude/coord/<repo>/orders - PENDING orders (the ORDRE column).
|
||||
# Rendered N:Md - the count AND the age of the
|
||||
# OLDEST order, never the newest and never a mean:
|
||||
|
|
@ -80,8 +83,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 +204,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|--voyage ...]
|
||||
# [--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.
|
||||
|
|
@ -224,6 +238,7 @@ 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=""
|
||||
|
||||
|
|
@ -238,10 +253,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; VOYAGE=0; shift ;;
|
||||
--plan) PLAN=1; BRIEF=0; INBOX_PLAN=0; VOYAGE=0; shift ;;
|
||||
--inbox-plan) INBOX_PLAN=1; BRIEF=0; PLAN=0; VOYAGE=0; shift ;;
|
||||
# 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.
|
||||
|
|
@ -249,8 +270,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; VOYAGE=0; shift ;;
|
||||
--voyage) VOYAGE=1; BRIEF=0; PLAN=0; INBOX_PLAN=0; DISPATCH=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; }
|
||||
|
|
@ -802,13 +823,40 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
lastd=-1; lastcol="-"
|
||||
fi
|
||||
|
||||
# PLAN: does this repo keep a destination where sessions actually look, and
|
||||
# is anyone touching it? A FILESYSTEM reading only - PLAN.md's content is
|
||||
# never read, so no wording inside it can move this cell. `-` = no file
|
||||
# named exactly PLAN.md in the root; `Nd` = whole days its mtime lies behind
|
||||
# the last commit (0d when it is as new or newer). The name is matched BYTE
|
||||
# FOR BYTE through a glob, never by `[ -f PLAN.md ]` alone: APFS is
|
||||
# case-insensitive by default, so that test is true for a `plan.md` and
|
||||
# would report a plan that is not there. A glob matches the names readdir
|
||||
# returns, as stored. A PLAN.md with no commit to compare against, or an
|
||||
# unreadable mtime, is `?` - never `-`, which means "no plan", and never
|
||||
# `0d`, which would be a measurement that did not happen.
|
||||
# Display only: not in any sort key, not in --plan, not in SUM.
|
||||
plancol="-"
|
||||
for pl_f in "$d"/PLAN.m[d]; do
|
||||
[ "${pl_f##*/}" = "PLAN.md" ] && [ -f "$pl_f" ] || continue
|
||||
# -L: the PLAN's age is the target's mtime; a symlink's own mtime is only
|
||||
# when the link was made.
|
||||
pl_m="$(stat -L -f %m "$pl_f" 2>/dev/null)"
|
||||
if [ -z "$pl_m" ] || [ -z "$lastct" ]; then
|
||||
plancol="?"
|
||||
elif [ "$pl_m" -ge "$lastct" ]; then
|
||||
plancol="0d"
|
||||
else
|
||||
plancol="$(( (lastct - pl_m) / 86400 ))d"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -f "$state" ]; then
|
||||
# No plan file, so no plan age: ALDER is blank rather than quietly showing
|
||||
# the commit age under a heading that means something else everywhere else
|
||||
# in the table. The sort key keeps using it - order is unchanged.
|
||||
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|-|%s|%s|%s|%s|(ingen STATE.md)\n' \
|
||||
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|-|%s|%s|%s|%s|-|%s|(ingen STATE.md)\n' \
|
||||
"$lastd" "$name" "$inbox" "$dirty" "$lastcol" "$d" \
|
||||
"$orders" "$ordersage" "$claimed" "$claimedage"
|
||||
"$orders" "$ordersage" "$claimed" "$claimedage" "$plancol"
|
||||
continue
|
||||
fi
|
||||
|
||||
|
|
@ -900,10 +948,11 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
# `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.
|
||||
# `plan` sits between voy and neste for the same reason.
|
||||
voy="$(voyage_cell "$d")"
|
||||
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
|
||||
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%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" "$voy" "$neste"
|
||||
"${blockedon:--}" "$orders" "$ordersage" "$claimed" "$claimedage" "$voy" "$plancol" "$neste"
|
||||
done > "${TMPDIR:-/tmp}/board.$$"
|
||||
|
||||
RECORDS="${TMPDIR:-/tmp}/board.$$"
|
||||
|
|
@ -1060,28 +1109,28 @@ awk -F'|' '$4 ~ /^blocked/ && $11 != "-" && $11 != "" {print $11}' "$RECORDS" \
|
|||
|
||||
hdr() {
|
||||
printf '\n%s\n' "$1"
|
||||
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %s\n' \
|
||||
"REPO" "STATUS" "KOST" "INN" "ORDRE" "FLY" "VOY" "DRT" "ALDER" "SISTE" "NESTE"
|
||||
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %5s %s\n' \
|
||||
"REPO" "STATUS" "KOST" "INN" "ORDRE" "FLY" "VOY" "DRT" "ALDER" "SISTE" "PLAN" "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 voy neste; do
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy plan 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 %7s %4s %6s %6s %s\n' \
|
||||
"$name" "$status" "$cost" "$inbox" "$o_cell" "$c_cell" "$voy" "$dirty" "$age" "$last" "$(trunc "$neste" "$NESTE_WIDTH")"
|
||||
printf '%-32s %-34s %-14s %4s %7s %7s %7s %4s %6s %6s %5s %s\n' \
|
||||
"$name" "$status" "$cost" "$inbox" "$o_cell" "$c_cell" "$voy" "$dirty" "$age" "$last" "$plan" "$(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.
|
||||
|
|
@ -1263,7 +1312,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 voy neste; do
|
||||
while IFS='|' read -r owed bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy plan 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
|
||||
|
|
@ -1303,13 +1352,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
|
||||
|
|
@ -1383,12 +1437,15 @@ dispatch() {
|
|||
[ -n "$d_rec" ] || { echo "board: no repo named '$D_REPO' in the scanned roots ($ROOTS)" >&2; exit 2; }
|
||||
d_status="$(printf '%s' "$d_rec" | cut -d'|' -f4)"
|
||||
d_dir="$(printf '%s' "$d_rec" | cut -d'|' -f10)"
|
||||
d_neste="$(printf '%s' "$d_rec" | cut -d'|' -f14-)"
|
||||
# `neste` is the LAST record field and is taken as "field N onward". This
|
||||
# index once read 14 and was never moved as fields were inserted before
|
||||
# `neste`, gluing three other columns onto the prose - move it with the record.
|
||||
d_neste="$(printf '%s' "$d_rec" | cut -d'|' -f18-)"
|
||||
[ -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
|
||||
|
|
@ -1867,7 +1924,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 voy neste; do
|
||||
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir blockedon orders ordersage claimed claimedage voy plan neste; do
|
||||
[ -n "$name" ] || continue
|
||||
pn=$((pn + 1))
|
||||
printf 'tab=%s\n' "$pn"
|
||||
|
|
@ -1927,7 +1984,11 @@ 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] = $17
|
||||
# neste is the LAST field and may itself contain a pipe: take field 18
|
||||
# ONWARD, never field 18 alone - a single field cuts the prose at its
|
||||
# first pipe. The index moves with the record.
|
||||
rc_dir[name] = $10; rc_neste[name] = $18
|
||||
for (i = 19; i <= NF; i++) rc_neste[name] = rc_neste[name] "|" $i
|
||||
next
|
||||
}
|
||||
{
|
||||
|
|
@ -2097,6 +2158,116 @@ if [ "$VOYAGE" -eq 1 ]; then
|
|||
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)"
|
||||
r_plan="$(printf '%s' "$r_rec" | cut -d'|' -f17)"
|
||||
# `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 18 on", never
|
||||
# as field 18.
|
||||
r_neste="$(printf '%s' "$r_rec" | cut -d'|' -f18-)"
|
||||
|
||||
# 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"
|
||||
echo "plan=$r_plan"
|
||||
# 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)"
|
||||
|
|
@ -2125,6 +2296,15 @@ echo " Baerer samme N:Md - alderen paa den ELDSTE claimen. Ingenting frigir
|
|||
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."
|
||||
# PLAN is a filesystem reading and nothing more. It shows WHERE a destination
|
||||
# is missing or untouched; whether the criterion inside it can be falsified is
|
||||
# a judgement no column can make, and the legend says so rather than letting
|
||||
# `0d` read as "this repo has a good plan".
|
||||
echo "PLAN = rot-PLAN.md: '-' = ingen fil som heter noyaktig PLAN.md. Nd = dager"
|
||||
echo " planens mtime ligger bak siste commit (0d = like ny eller nyere)."
|
||||
echo " '?' = plan uten commit aa sammenligne med. Bedommer IKKE om kriteriet"
|
||||
echo " kan felles, og leser aldri innholdet. touch/checkout nullstiller mtime"
|
||||
echo " (samme forbehold som ALDER). Kun visning: aldri sortert, aldri summert."
|
||||
echo "ALDER = dager siden STATE.md endret. SISTE = dager siden siste commit."
|
||||
|
||||
[ "$(count 1)" -gt 0 ] && { hdr "BLOKKERT (venter paa ekstern avhengighet)"; rows 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
|
||||
|
|
@ -359,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)"
|
||||
|
|
@ -377,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
|
||||
|
|
@ -393,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"
|
||||
|
|
@ -467,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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -288,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
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: >-
|
|||
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.33.1"
|
||||
version: "0.35.0"
|
||||
---
|
||||
|
||||
# board — which repo deserves the next session
|
||||
|
|
@ -75,6 +75,7 @@ the `VOY` column).
|
|||
| `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. |
|
||||
| `PLAN` | Root `PLAN.md` as a filesystem reading: `-` = no file named exactly `PLAN.md` in the root; `Nd` = days the plan's mtime lies behind the last commit (`0d` = as new or newer); `?` = a plan with no commit to compare against. Read it as *where a destination is missing or untouched* — it never judges whether the criterion inside can be falsified, and it **never ranks**: do not order repos by it. |
|
||||
| `NESTE` | First line of the STATE.md next-step block, truncated. |
|
||||
|
||||
## Briefs in flight — `--voyage`
|
||||
|
|
|
|||
|
|
@ -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.33.1"
|
||||
version: "0.35.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.33.1"
|
||||
version: "0.35.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.33.1"
|
||||
version: "0.35.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