Compare commits
No commits in common. "main" and "v0.27.0" have entirely different histories.
26 changed files with 385 additions and 5543 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.34.0",
|
||||
"version": "0.27.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"
|
||||
|
|
|
|||
596
CHANGELOG.md
596
CHANGELOG.md
|
|
@ -5,602 +5,6 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.34.0] - 2026-09-12
|
||||
|
||||
### Changed
|
||||
|
||||
- **The advisor rule is struck from the rubric: `route.sh` and `board.sh
|
||||
--dispatch` no longer emit `--advisor` at all.** It fired on two needs -
|
||||
always on the Sonnet rows, and on the Opus rows at `costly|one-way` stakes -
|
||||
and it read well. What killed it was a measurement, not a change of taste:
|
||||
of 54 dispatches the PM issued 08.-12.09, **zero** carried the flag, because
|
||||
sessions are started by hand from the model and effort rather than from the
|
||||
whole emitted line. A rule nothing honours is not a policy, and an emitted
|
||||
value nobody acts on is decoration in a field whose only job is to be
|
||||
evidence. The advisor is now what it already was in practice: an operator
|
||||
decision per session, said in one sentence in `route.sh --help`. The
|
||||
direction is the safe one for the single thing the old rule protected - the
|
||||
quota fallback is still exactly one row cheaper, it just no longer implies a
|
||||
capability lift nobody was taking.
|
||||
- **The comments that rested on it were rewritten, not left standing.**
|
||||
`board.sh --dispatch` still refuses a `--model`/`--effort` pair, but the
|
||||
reason is no longer "the advisor is a property of the ROW": it is that the
|
||||
rubric has exactly one copy, and a dispatch taking the model directly would
|
||||
be a second, unscored way to reach the same decision - one recording no
|
||||
traits, no rationale and no `next-cost`, so nothing afterwards could say
|
||||
whether the routing or the scoring was wrong. A comment defending a
|
||||
mechanism that has been removed is how the next session restores it.
|
||||
`skills/route/SKILL.md` and `skills/dispatch/SKILL.md` carry the same
|
||||
correction.
|
||||
- Pinned as an ABSENCE over the whole trait space rather than on sampled rows -
|
||||
81 trait combinations, every line of output, with a known-positive control
|
||||
proving the sweep's grep can find a planted advisor - plus `--dispatch` at
|
||||
`reversibility=costly`, the exact input a reintroduced rule would fire on.
|
||||
Backward compatibility is pinned too: a route line carrying a legacy
|
||||
`advisor=` field still parses and still yields a command (measured the same
|
||||
day: 0 of 48 `<!-- route:` lines anywhere in `~/repos` carry one, so nothing
|
||||
in circulation depends on it - but a reader that broke on an unknown field
|
||||
would turn last month's STATE.md into "that repo has no route line").
|
||||
|
||||
### Added
|
||||
|
||||
- **`board.sh --row <repo>`: the table's row for one repo, as `field=value`
|
||||
lines.** A seventh rendering of the same scan, never a second scan, and
|
||||
read-only like every other one. The motivation is a measured misreading: on
|
||||
11.09 the PM read the FLY column off the table **by eye** and got it wrong.
|
||||
Every other rendering a program consumes (`--plan`, `--inbox-plan`,
|
||||
`--dispatch`) is already `key=value`; the default view was the one left out,
|
||||
and a 34-character repo key already shifts a row two characters right, which
|
||||
makes counting spaces worse than it looks. `inn`, `ordre` and `fly` are three
|
||||
separate fields because they are three separate facts, and confusing them is
|
||||
what this exists to close. `status` is the bare token, never the table's
|
||||
`blocked>target` display, with `blocked-on` beside it; `neste` is last and
|
||||
uncut, for the same reason it is last in the record. An unknown repo exits
|
||||
**2** and writes nothing to stdout - an empty block would read as a repo whose
|
||||
every column is blank, which is a real and different state.
|
||||
- `upushet=` is the one field that is **not** a rendering of the scan, and it is
|
||||
named rather than blended in: nothing in the scan measures it, so it is read
|
||||
once, for the named repo only, and never enters the table, the plan or the
|
||||
briefing. It reads the remote-TRACKING ref, not the remote, so `upushet=N`
|
||||
honestly means "the local ref says N"; a repo with no upstream reports `?`,
|
||||
never `0`, because "nothing is waiting to be pushed" and "nobody looked" are
|
||||
exactly the pair this engine refuses to render alike.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **README's selftest numbers were re-measured, not re-derived.** The badge said
|
||||
`selftest_checks-868` and the five `## Development` comments said
|
||||
220/360/73/99/40 = 792 - two different wrong sums on the same public surface,
|
||||
neither matching the other. The five suites were run under `/bin/bash` (3.2)
|
||||
on `b57a1ea` and reported `coord 257`, `board 393`, `route 73`, `orders 116`,
|
||||
`state-line-guard 54` = **893**, 0 failed in every summary. 0.33.0's entry
|
||||
below records 257+368+73+116+54 = 868 and was true when written; 0.33.1 added
|
||||
25 board checks (368 -> 393) without re-summing, and the comment block had been
|
||||
stale far longer. No code, no version bump: the number furthest from the meter
|
||||
rots first, and only the number moved.
|
||||
|
||||
## [0.33.1] - 2026-09-04
|
||||
|
||||
### Added
|
||||
|
||||
- **A git repo nested under a depth-1 repo now enters the board, on a STATE.md
|
||||
and nothing wider.** Discovery adds a depth-1 repo and stops, and the
|
||||
else-branch container scan - the only place children are ever looked at - is
|
||||
unreachable for an entry that is itself a repo. Measured before anything was
|
||||
written: **12 nested repos across the real tree, exactly 1 with a STATE.md**
|
||||
(`from-ai-to-chitta/content-sadhguru`, which had been running work and
|
||||
reporting to nobody). The other 11 are vendored or experimental checkouts and
|
||||
stay invisible on purpose - they do not even reach the `UTEN STATE.md` bucket.
|
||||
`add_nested_repos()` sits beside `add_dot_repos()` rather than widening the
|
||||
`*` loops, for the same reason ordre `20260818T124828Z` gave for dot repos:
|
||||
routing a depth-1 repo into the container branch would admit every nested
|
||||
checkout. One level only; depth 3 is pinned as NOT admitted. A dot-prefixed
|
||||
depth-1 repo gets the same nested scan, since nothing in the criterion
|
||||
distinguishes it.
|
||||
|
||||
A nested repo carries **two names**, and conflating them would have put a
|
||||
fabricated 0 in three columns. The board KEY is `<parent>/<child>`; the
|
||||
MAILBOX name is `basename(git toplevel)`, so `$COORD/<parent>/<child>/inbox`
|
||||
finds no directory and INN/ORDRE/FLY would read 0 for a repo that may have
|
||||
mail. The record loop carries `mbox` beside `name`, and `--voyage`'s order
|
||||
lookup takes `basename($vy_dir)`. Which directories are nested is RECORDED by
|
||||
discovery (`NESTED_LIST`), never re-derived from "is my parent a repo?".
|
||||
|
||||
- **The scan reports its own denominator.** `undersoekt: N katalog(er) depth 1,
|
||||
M polyrepo-container(e), K nestede repo (J med STATE.md tatt med).` The header
|
||||
count answered how many repos were found and nothing about how many were
|
||||
looked at, so a criterion excluding 11 of 12 nested repos was invisible on the
|
||||
surface built to show it. Real tree 2026-09-04: `43 / 5 / 12 (1 tatt med)`. A
|
||||
non-repo dot-directory counts in N and never in M - the line is a denominator,
|
||||
not a partition.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The denominator line's own wording broke an existing check: `polyrepo
|
||||
container itself is not listed as a repo` grepped the whole output for
|
||||
`polyrepo` and matched the new footer's `polyrepo-container(e)` - the same
|
||||
class as a grep reading a comment that EXPLAINS a pattern as an instance of
|
||||
it. Now anchored at column 1, which is what it always meant.
|
||||
|
||||
### Known gaps, stated rather than closed
|
||||
|
||||
- The mailbox-keyed JOINs (`$OWED`, `brief_orphans`, `--inbox-plan`) still key
|
||||
on the board name, so a nested repo owing a reply gets no debt tab and is
|
||||
listed under `UTENFOR REPO-SKANNEN`. Neither created nor worsened here: before
|
||||
this change the repo was absent from `RECORDS` entirely, so both readings were
|
||||
already exactly as wrong.
|
||||
- A 34-character key overflows the table's `%-32s` REPO column, shifting that
|
||||
row two characters right. Same class as `Fable 5.1/xhigh` in KOST; parsing is
|
||||
unaffected, and widening the column moves three `cut -c89-` selftest helpers.
|
||||
- A dot-prefixed NESTED repo is not looked for. The combination was neither
|
||||
measured nor ordered.
|
||||
|
||||
### Testing
|
||||
|
||||
- `board-selftest.sh` 368 -> 393 checks.
|
||||
|
||||
## [0.33.0] - 2026-09-04
|
||||
|
||||
### Added
|
||||
|
||||
- **The FYI sweep is scheduled. Invocation was the gap, not the mechanism.**
|
||||
`coord-sweep.sh` shipped in 0.10.0 and had never run once against the real
|
||||
mailbox. Measured 2026-09-03 with a denominator
|
||||
(`docs/2026-09-03-coordination-debt-measurement.md`): 55 mailbox directories,
|
||||
52 carrying an `inbox/`, 27 pending directed messages - of which **23 were
|
||||
pure notices**, re-injected at every session start in repos nobody had
|
||||
opened. The script was correct and unreachable.
|
||||
|
||||
The order behind this (WP5, `20260902T113745Z-1254925290`) asked for a
|
||||
mechanism and named two candidates; the measurement chose neither, and the
|
||||
first session returned the order saying so. Bulk-ack for pure notices *was
|
||||
already built* - it is this script - so building the second candidate would
|
||||
have been two copies of one policy. The broadcast class converges on its own
|
||||
(reading sets `seen`), and 34 of 263 unread pairs belong to two mailboxes no
|
||||
session can hold, so a TTL would have **closed** those rather than reduced
|
||||
them. The operator then chose the window and authorized the schedule.
|
||||
|
||||
`launchd/com.ktg.repo-mailbox-sweep.plist` runs `--write --days 14` daily at
|
||||
05:30. That is the entire behavioural change; no new mechanism was built.
|
||||
|
||||
- **The 14-day window is written out in the plist, not inherited from the
|
||||
script's default.** It is a policy constant chosen on a measured distribution
|
||||
(30 days would have closed 0 messages, 14 closed 7, 7 would have closed 13),
|
||||
of the same class as the STATE.md line limit. Leaving it implicit would let a
|
||||
later change to `DAYS=14` silently change what an unattended job closes every
|
||||
night across every other mailbox on this machine.
|
||||
|
||||
It runs at 05:30, clear of the 06:00 briefing agent, because the briefing
|
||||
scans the same mailbox this mutates and the two must not overlap. It does
|
||||
**not** change what the briefing reports as debt: since 0.22.0 that figure
|
||||
comes from `coord-count.sh`'s `owed` column, and this sweep closes only
|
||||
messages that owe nothing. What moves is the raw pending count — the table's
|
||||
`INN` column and the volume injected at every session start. Selftest section
|
||||
38 asserts the two agents never share an hour.
|
||||
|
||||
- **coord-selftest.sh section 38 pins the launchd templates (+15 checks, 242 ->
|
||||
257).** A wrong program path in a plist is the one defect here that nothing
|
||||
catches at runtime: the agent loads cleanly and then silently never runs -
|
||||
no output to be wrong, no exit status to read, a failure indistinguishable
|
||||
from a quiet machine. `launchctl list` proves an agent is *loaded*, never
|
||||
that it is *right*.
|
||||
|
||||
The section covers **every** plist in `launchd/`, not just the new one: the
|
||||
plist grammar gets one reader here rather than one per agent, which is the
|
||||
two-copies-of-one-policy defect this repo has named repeatedly.
|
||||
`board-selftest.sh` section 9 still owns `brief-nightly.sh`'s behaviour. Each
|
||||
plist must name a script that exists in this checkout, carry a `Label`
|
||||
matching its filename, keep its `__CHECKOUT__`/`__HOME__` placeholders (the
|
||||
repo is mirrored publicly, and a plist is the one file that would otherwise
|
||||
need an absolute home path), and never point into the version-pinned plugin
|
||||
cache.
|
||||
|
||||
The cache assertion runs on the **extracted program path, never the whole
|
||||
file** - caught by the check itself on its first run: the brief plist's header
|
||||
explains in prose why it does *not* point at the cache, and a file-wide grep
|
||||
read that explanation as the defect it warns about. Same shape as the board
|
||||
line, where prose saying `status=done` must never trigger the done-guard.
|
||||
Four controls are mandatory here and present: the extractor really does read
|
||||
a path, a plist naming a missing script is judged missing, a `Label`
|
||||
disagreeing with its filename is caught, and a program path inside the plugin
|
||||
cache is caught. Mutation-verified against the real file: a one-letter typo
|
||||
(`coord-sweeep.sh`) turns exactly that check red.
|
||||
|
||||
XML well-formedness is deliberately **not** checked. `plutil` is not
|
||||
coreutils, and malformed XML already fails loudly at `launchctl load` - the
|
||||
opposite of the silent failure this section exists for. Both files were
|
||||
linted by hand at 0.33.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The README's selftest-checks badge had read 529 since 0.25.0; the real
|
||||
total is 868.** A stale count in the one place a reader takes as the
|
||||
headline number, corrected while adding to it: 257 + 368 + 73 + 116 + 54.
|
||||
|
||||
## [0.32.1] - 2026-09-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A pending order's age is read from the FILENAME, never the mtime.**
|
||||
`coord-order-done --return` rewrites the order file's mtime, and both age
|
||||
surfaces read mtime, so returning an order reset the very reading that says
|
||||
how long it has waited. An order returned three times could never look old.
|
||||
Found by reading the board immediately after this repo returned an order of
|
||||
its own: a file whose name says 2026-09-02 rendered `ORDRE 1:0d` and
|
||||
`pending, 0d old` minutes later. The filename is written once, at delivery,
|
||||
and nothing rewrites it - which is exactly the fact "how long has this sat
|
||||
with no owner" is asking about.
|
||||
|
||||
Two questions, two sources, and the second one does not move: a CLAIMED
|
||||
order's age is "how long has it been in flight", which is the claim's own
|
||||
mtime and was already right. `board.sh` gains `oldest_pending_age()` beside
|
||||
`oldest_order_age()`; `coord-order-inbox.sh` gains `pending_age_of()` beside
|
||||
`age_of()`. Switching FLY to the filename would answer the delivery question
|
||||
in the column that asks the flight question.
|
||||
|
||||
An unparseable filename yields `?` for the whole reading, never a fabricated
|
||||
`0` - the same fail-safe the mtime path already carried, and for the same
|
||||
reason: an unmeasured order could be the oldest one.
|
||||
|
||||
Pinned by `orders-selftest.sh` section 11 (116 checks, up from 110) and
|
||||
`board-selftest.sh` section 30 (368, up from 360), each with its ground truth
|
||||
asserted before anything depends on it and with known-positive controls that
|
||||
a freshly delivered order still reads `0d` and that FLY did not move.
|
||||
Mutation-verified in both files: restoring the mtime read turns exactly the
|
||||
defect checks red and leaves every control green.
|
||||
|
||||
Section 28's fixtures were rewritten as part of this, not incidentally: they
|
||||
encoded their intended ages in `touch -t` with fixed 2026-01/2026-08
|
||||
filenames, which a filename-based reading makes wrong and time-dependent.
|
||||
They now compute their stems from `date -v`, and the section asserts two
|
||||
ground truths - the filename for ORDRE, the mtime for FLY - because the two
|
||||
columns no longer read the same source.
|
||||
|
||||
## [0.32.0] - 2026-09-01
|
||||
|
||||
Two commits, and the first of them was already written when this release
|
||||
started: `board.sh --voyage` landed on 2026-08-31 and then sat one step past
|
||||
`v0.31.0`, unreleased AND unpushed. Every entry point on this machine runs the
|
||||
version-pinned plugin cache, so the feature existed in git and did not exist
|
||||
for anyone using it. That gap is the reason both changes ship together - they
|
||||
are the same plugin and the same bump.
|
||||
|
||||
### Added
|
||||
|
||||
- **`board.sh --voyage` reports the Voyage briefs in flight** (committed
|
||||
2026-08-31, released here). A sixth rendering of the same read-only scan.
|
||||
`board` reads STATE lines, which say nothing about a brief, so a programme
|
||||
running Voyage across several repos had no shared surface at all: nobody
|
||||
could answer which briefs were running, in what phase, and who was waiting on
|
||||
whom. Detection is by PROPERTY, never by directory name - a directory counts
|
||||
only if it holds `brief.md` or `brief.md.draft` under one of the three
|
||||
planning locations the convention recognises - and it walks the FILESYSTEM,
|
||||
never the git index: `llm-ingestion-okf` gitignores `.claude/projects/`, so
|
||||
`git ls-files` would have reported zero briefs in the one repo actually
|
||||
running one. The phase ladder measures ARTIFACTS, not sessions, and says so
|
||||
in its legend; three fields refuse to let an absence borrow the shape of a
|
||||
measurement (`kvalitet=-` is never `complete`, `research=-` is not
|
||||
`research=0`, and the rendering always prints its own denominator). A
|
||||
matching `VOY` column sits beside `ORDRE` and `FLY`, never summed with them.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`route.sh --last-model` now accepts `Fable 5.1`, and the set stays
|
||||
CLOSED.** Fable 5.1 shipped 2026-09-01 and the three-value set refused it, so
|
||||
a session that actually ran it could not record what it ran: the record was
|
||||
either omitted or written as a name already in the set. A lied record reads
|
||||
back months later as a measurement rather than as the gap it is, which is the
|
||||
failure mode of the one field whose entire purpose is to be believed later.
|
||||
|
||||
Two boundary decisions were taken rather than deferred. `Fable 5` is KEPT -
|
||||
not for backward compatibility (measured: exactly 1 of 45 `route-last` lines
|
||||
on this machine carries it) but because `route.sh`'s own row table spells
|
||||
rows 5-6 `Fable 5/high` and `Fable 5/xhigh`, so dropping it would make the
|
||||
script refuse to record a name its own spec writes. And the set was WIDENED
|
||||
rather than replaced by form validation: a `<family> <digits>[.<digits>]`
|
||||
pattern would still catch a misspelled family, but would stop catching a
|
||||
version that does not exist, letting `Fable 5.2` and `Opus 7` read back as
|
||||
evidence that a model ran when it never shipped. The selftest pins that
|
||||
choice - `Fable 5.2` / `Fable 6` / `Fable 5.10` are rejected beside the new
|
||||
accept case - so a later switch to form validation fails there instead of
|
||||
quietly widening what the record can claim. The die message now names the
|
||||
repair, since the cost of a hand-maintained list is exactly what was paid
|
||||
here.
|
||||
|
||||
Not touched: the row table (still six rows, still emitting only 1-4, Fable
|
||||
still a hand-written override) and the note that a Fable session runs without
|
||||
an advisor because the CLI does not enforce it.
|
||||
|
||||
Known gap, stated rather than closed: `Fable 5.1/xhigh` is 15 characters and
|
||||
overflows `board.sh`'s `%-14s` KOST column, shifting the rest of that row one
|
||||
column right. Parsing is unaffected and is now pinned by the round-trip
|
||||
section. Widening the column was not ordered in this session and is reported
|
||||
rather than made.
|
||||
|
||||
`route-selftest.sh`: 69 -> 73 checks. The other four suites are unchanged
|
||||
(coord 242, board 360, orders 110, state-line-guard 54).
|
||||
|
||||
## [0.31.0] - 2026-08-29
|
||||
|
||||
Two commits landed after `v0.30.0` and, as with that release, neither reaches
|
||||
anyone until the tag exists: every entry point on this machine runs the
|
||||
version-pinned plugin cache, not the tree. Minor rather than patch because the
|
||||
board's table gained a reading it did not have - the age of the oldest queued
|
||||
order - while the other change repairs a consumer that was ignoring a signal
|
||||
the engine already sent.
|
||||
|
||||
Both are the same class the previous release closed and this one continues: a
|
||||
failed or missing measurement rendering as a reassuring value.
|
||||
|
||||
### Added
|
||||
|
||||
- **`ORDRE` and `FLY` now carry the AGE of the oldest order, rendered `N:Md`.**
|
||||
Reading the board, `ORDRE 2` used to look identical whether both orders
|
||||
arrived yesterday or both had sat for ten days. The problem was never that
|
||||
orders wait - that is a prioritisation call owned by humans - it was that no
|
||||
cross-repo surface showed the wait at all: `coord-order-inbox.sh` reports an
|
||||
age, but only inside the one repo at SessionStart, so a repo nobody opens
|
||||
shows nobody anything. Measured across the live mailbox 2026-08-29: 30
|
||||
pending orders over 22 repos, 5 of them ten days old. The age is the OLDEST
|
||||
order, never the newest and never a mean - a repo holding one fresh order and
|
||||
one ten-day-old order has a ten-day-old problem, and the newest reading is
|
||||
exactly what would hide it. `FLY` carries the same reading, where it matters
|
||||
at least as much: nothing un-claims an order when the session that took it
|
||||
dies. An empty queue still prints a bare `0` ("no orders" and "an order that
|
||||
arrived today" are different facts), and an unreadable mtime prints `?` for
|
||||
the whole reading rather than silently skipping a file that could have been
|
||||
the oldest one.
|
||||
|
||||
The age is DISPLAY only. It is deliberately not in the sort, and `--plan`'s
|
||||
`fly=` and every other emitted `key=value` block are untouched, so nothing
|
||||
that parses this board changes shape.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`board.sh` listened to `coord-count.sh`'s output and ignored its exit
|
||||
status, so the failure signal 0.30.0 had just built stopped at the
|
||||
producer.** Every call site piped the counter into `awk`, and a pipeline
|
||||
reports the last stage's status, so exit 3 - "the mailbox root you named is
|
||||
not there; this is not a count of zero" - was discarded each time. Measured
|
||||
on the shipped 0.30.0 against a mailbox root that does not exist:
|
||||
`coord-count.sh` said "not counted, not zero" and exited 3, while
|
||||
`board.sh --brief` answered "Ingen repo skylder noen et svar i dag." and
|
||||
`--plan` emitted no warning key at all. For a reader that meant a briefing
|
||||
confidently reporting that nobody owes anyone a reply, on the basis of a
|
||||
measurement that never happened.
|
||||
|
||||
The board now reports the failure at all five places it speaks, and keeps the
|
||||
two causes apart - a missing sibling script and a mailbox root that is not
|
||||
there are different repairs. A partial count captured before a failure is
|
||||
discarded rather than shown, because a half-count also looks measured. The
|
||||
table and `--plain` views are unchanged: they never invoked the counter, and
|
||||
now say so instead of implying a measurement nobody attempted.
|
||||
|
||||
Bounded gap, stated rather than closed: the table's own `INN`/`ORDRE`/`FLY`
|
||||
columns count with `ls` and read 0 for every repo when the mailbox root is
|
||||
absent. Same shape, one source over, and not a `coord-count.sh` consumer -
|
||||
named here rather than silently widened into.
|
||||
|
||||
## [0.30.0] - 2026-08-26
|
||||
|
||||
Two commits landed after `v0.29.0` and neither is reachable: every entry point
|
||||
on this machine runs the version-pinned plugin cache, not the tree. Minor
|
||||
rather than patch because the behaviour is EXTENDED, not merely repaired -
|
||||
`coord-count.sh` and `board.sh` gained a new exit status and the state-line
|
||||
guard gained an environment variable. Exit codes are a contract, and the
|
||||
installed cache still runs the old one.
|
||||
|
||||
All five defects here are the same class: **a failed measurement rendering as
|
||||
a reassuring value** - a broken or uninstrumented query returning a
|
||||
positive-looking null, consumed as a fact about the world.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`coord-count.sh`: a mailbox root that does not exist was byte-identical to
|
||||
one where nobody has pending mail** - zero lines on stdout, exit 0, nothing
|
||||
on stderr. `board.sh` consumes that TSV. The contract is now three statuses:
|
||||
0 = counted, 2 = usage error, **3 = mailbox root absent**. 3 rather than 2 on
|
||||
purpose: 2 already means "you called me wrong" and this means "the world you
|
||||
named is not there" - two different repairs, and a consumer that only ever
|
||||
sees one integer cannot tell them apart. Both nonzero paths print NOTHING on
|
||||
stdout, so neither can be mistaken for a count of zero. What the old
|
||||
"always 0" claim protected is kept and made precise: no state OF THE MAILBOX
|
||||
can produce a nonzero exit, so a SessionStart still cannot be failed by mail.
|
||||
An existing-but-empty root stays a silent, clean 0, and the selftest pins
|
||||
that silence as hard as it pins the failure.
|
||||
- **`coord-count.sh`'s header promised exit 0 unconditionally, and that was
|
||||
already false** - `--exclude` with no value exited 2 - so the documented
|
||||
contract contradicted code a reader could run. The header text is now pinned
|
||||
by a check of its own: a doc line nothing tests is a doc line that drifts,
|
||||
which is how this one drifted.
|
||||
- **`board.sh`: `git status --porcelain | wc -l` yields zero lines whether the
|
||||
tree is CLEAN or git refused to answer**, so DRT printed **0** and every
|
||||
reader took it as "nothing uncommitted here". Reachable, not theoretical:
|
||||
discovery tests `.git` with `-e` so worktrees are found, and a worktree whose
|
||||
parent checkout was deleted exits 128. The exit status is now the
|
||||
discriminator and an unmeasured tree reads **`?`** - the same token
|
||||
`coord-count.sh` already uses for an age it could not compute.
|
||||
|
||||
**The fix was worthless without its two awk consumers, and one of them is
|
||||
the point:** `--plan`'s free-capacity test compared `$7 + 0`, and `"?"`
|
||||
coerces to **0** in arithmetic - so an UNMEASURED tree would have been
|
||||
certified as free capacity. That is the defect reappearing one layer down,
|
||||
wearing the fix as a disguise. It now compares the field as a STRING against
|
||||
exactly `"0"`; only a tree measured clean is clean. The SUM roll-up, where a
|
||||
`?` would silently add 0, now NAMES the repos it could not add.
|
||||
- **`board.sh`: a scan root that does not exist was skipped in silence**, so a
|
||||
typo in `--roots`, a moved home directory and a genuinely empty tree were one
|
||||
single output with the denominator never reported. Every bad root is now
|
||||
named on stderr; the exit status changes only when NO root was scanned at all
|
||||
(**3**, for the same reason as above). A mix still exits 0 and still prints
|
||||
the board - some repos really were scanned. This REPLACES an older assertion
|
||||
reading `check "missing root is a clean no-op"`: the defect encoded as a
|
||||
passing test, which is why nothing ever caught it. `brief-nightly.sh` needs
|
||||
no change and gains from this - it already treats a nonzero `--brief` as
|
||||
failure and keeps yesterday's briefing, so a mistyped root now preserves the
|
||||
file instead of overwriting it with an empty render.
|
||||
|
||||
### Added
|
||||
|
||||
- **`CLAUDE_STATE_MAX_LINES` overrides the state-line guard's limit**, the same
|
||||
kind of knob `CLAUDE_COORD_DIR` is for the mailbox root. As a bare constant
|
||||
the limit forced every boundary fixture to hardcode the number the code
|
||||
carried - two copies of one policy, both rewritten by hand the last time the
|
||||
operator moved it (60 -> 120). It is **not** a bypass claim; this guard has
|
||||
always been escapable by writing the file another way. The one thing it must
|
||||
never do is silently fail to take effect, so an UNUSABLE value (`""`, `"0"`,
|
||||
negatives, anything not made of digits) **denies with a message naming the
|
||||
variable** instead of falling back to 120 - a caller who set it and got the
|
||||
default anyway would be reading a limit that was never in force.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The README's Install block is now fenced (` ```bash `)** while the rest of
|
||||
the README keeps its indented blocks. The org-side D-census reported this
|
||||
repo as the only plugin of 12 with no start command; the premise was
|
||||
imprecise - the lines were present, indented - but the finding held, because
|
||||
the machine-readable entry point keys on a fenced block and 10 of 11 sibling
|
||||
plugin READMEs use one. Measured before and after: 11 of 11 now match. Only
|
||||
the Install block was converted; the other 39 indented blocks were not
|
||||
ordered changed and converting them would bury a two-line fix in a
|
||||
whole-file diff.
|
||||
|
||||
### Known gaps, stated rather than closed
|
||||
|
||||
- `coord-inbox.sh:57` and `coord-order-inbox.sh:60/64` carry the identical
|
||||
`[ -d "$COORD" ] || exit 0` shape that was fixed in `coord-count.sh`. The
|
||||
denominator was measured, not estimated, and they were deliberately left
|
||||
alone: they are the INJECTION path, their output is prose a session reads
|
||||
rather than a TSV a program parses, and their contract really is "never fail
|
||||
a SessionStart".
|
||||
- `board.sh` does not yet CONSUME `coord-count.sh`'s new exit 3. The signal
|
||||
exists; the `HAVE_COUNT` machinery does not read it. Tracked as separate
|
||||
work rather than widened into this release in silence.
|
||||
|
||||
### Testing
|
||||
|
||||
Suites at this release, verified under system bash 3.2 (not Homebrew 5.3):
|
||||
coord 242, board 300, route 69, orders 110, state-line-guard 54; `npm test`
|
||||
11/11. Every design choice above was mutation-tested, and every negative check
|
||||
carries a known-positive control - one selftest section's first cut was
|
||||
VACUOUSLY green (wrong basename, plus a fixture path that was never exported)
|
||||
and is recorded in `CLAUDE.md` rather than quietly fixed.
|
||||
|
||||
## [0.29.0] - 2026-08-23
|
||||
|
||||
Answers a question the board could not answer before: which repos are finished
|
||||
and can take more work. Ordered as an investigation, not as a column - the
|
||||
argument for what was NOT built is in
|
||||
`docs/2026-08-23-free-capacity-investigation.md`.
|
||||
|
||||
### Added
|
||||
|
||||
- **`FLY` column: orders in flight (`orders/claimed/`).** `ORDRE` counted
|
||||
pending orders only, so a repo holding one order in flight and a repo holding
|
||||
no orders at all both printed `0` - the same digit for two opposite facts.
|
||||
Measured the day this was ordered: two panes stood open and idle for 45 hours
|
||||
holding finished orders, with full quota authorised, and no column reported
|
||||
it. FLY is the same queue in its other state, never summed with ORDRE and
|
||||
never a fourth axis.
|
||||
- **`--plan` names free capacity**: `ledig_antall=N` plus one
|
||||
`ledig=<repo> (<status>)` line per repo with nothing owed, no pending order,
|
||||
nothing in flight, a clean tree, at `done` or `deferred`. Such a repo has no
|
||||
next step to open a tab for and used to fall out of the plan silently. All
|
||||
four conditions are required - measured on the real tree, 4 of 17
|
||||
done/deferred repos were not free. Emitted as LINES, never blocks: the plan's
|
||||
second consumer discards a block with no `tab=`, so a block would be visible
|
||||
to the operator and invisible to the driver. The count prints at 0, so "none
|
||||
found" and "not computed" cannot render the same.
|
||||
- **`fly=N` on a tab block** whose repo already holds a claimed order. The
|
||||
driver types into live panes.
|
||||
|
||||
### Unchanged, deliberately
|
||||
|
||||
- **No process inspection.** `pgrep`/`ps`/`lsof` were considered and refused,
|
||||
and the selftest now asserts their absence structurally with a known-positive
|
||||
control. Every other column is a durable filesystem fact reproducible in a
|
||||
fixture tree; a process column measures one instant on one machine, needs
|
||||
`lsof` to map a process to a repo, and rests on a CPU-vs-elapsed THRESHOLD -
|
||||
which would make the board decide a session is dead, the same thing the order
|
||||
queue is already forbidden from doing.
|
||||
- **FLY does not mean a session is alive**, and the on-screen legend says so.
|
||||
Nothing un-claims an order when the session that claimed it dies: one order on
|
||||
the live mailbox had been claimed for 117 hours. Selftest section 24 pins the
|
||||
denial as a check on the legend text.
|
||||
|
||||
### Known gap, stated rather than closed
|
||||
|
||||
An open, idle pane in a `planned` repo with no claimed order and no queued work
|
||||
is still invisible, and cannot be seen from the filesystem at all. Closing it
|
||||
needs a terminal-side measurement supplied inward by the driver that already
|
||||
probes panes - the same direction `--dispatch --target-pane` already uses.
|
||||
|
||||
## [0.28.0] - 2026-08-20
|
||||
|
||||
Five fixes were committed and pushed after `v0.27.0` and none of them was
|
||||
reachable: every entry point on this machine runs the version-pinned plugin
|
||||
cache, not the tree. This release is what makes them reachable.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`board.sh` repo discovery skipped dot-prefixed repos.** A repo named
|
||||
`.profile` (the Forgejo/GitHub org-profile convention) was invisible to the
|
||||
board entirely - not listed, and `--dispatch --repo .profile` refused with
|
||||
"no repo named '.profile' in the scanned roots". The fix is a dedicated
|
||||
`add_dot_repos()` rather than `shopt -s dotglob`: with dotglob on, the same
|
||||
`[ -e "$entry/.git" ]` test would route a dot-directory that is NOT a repo
|
||||
(e.g. `.claude`) into the polyrepo-container branch, silently widening what
|
||||
counts as a container. Bounded gap left open deliberately: a dot-directory
|
||||
that is not itself a repo is still never treated as a container.
|
||||
- **The three order-verb emitters told a session to run the verbs by a BARE
|
||||
name, and neither verb is on PATH** - command-not-found on step one,
|
||||
misreadable as "the order does not exist". `board.sh`'s dispatch starter,
|
||||
`coord-order-inbox.sh`'s SessionStart injection and `coord-order-claim.sh`'s
|
||||
own `WHEN DONE` lines now call the verb by the absolute path sibling to the
|
||||
printing script itself, correct for whichever install location is live.
|
||||
- **`--to` is now REFUSED on a control character rather than sanitized, on
|
||||
both write sides.** A target name is also the destination DIRECTORY name, so
|
||||
collapsing a newline to a space the way `sanitize_field` does everywhere else
|
||||
delivers to a mailbox the sender never named. Both corruptions were measured
|
||||
on the live engine before the guard existed, each with exit 0 and a
|
||||
"delivered" line: a newline lands its payload inside the frontmatter block
|
||||
and silences the debt the engine itself declared; a tab makes `coord-count`
|
||||
print five tab-separated fields where its contract is four, which `board.sh`
|
||||
consumes. `coord-order-send.sh` had the identical defect, where it costs
|
||||
more - an order filed under a name no session can hold is the silent
|
||||
evaporation the ownership chain exists to prevent.
|
||||
- **A third `NR==FNR` join survived in `plan()`'s focus filter**, next to two
|
||||
comments stating the file's own rule against it. Measured before touching it:
|
||||
unlike the two prior instances, this one has no reachable defect - it is
|
||||
fixed as a live counter-example to a rule the file states about itself, and
|
||||
pinned structurally rather than behaviourally, because there is no behaviour
|
||||
to pin.
|
||||
- **`route_cmd_for()` read the RATIONALE instead of the field.** Its four trait
|
||||
extractions searched the whole route line with a greedy `.*trait=`, and
|
||||
`route.sh --help` asks for a rationale per score - so a rationale that names
|
||||
a trait won the match over the field itself. `route.sh` correctly refused the
|
||||
prose, `--plan` printed `command_missing=`, and the operator read a broken
|
||||
PARSER as "that repo has no route line". A/B-ing the pre-fix script against
|
||||
the fixed one over the real tree at the same moment: 25 tabs,
|
||||
`command_missing` 7 -> 2. The cut is at the first `rationale=`; F4's
|
||||
`[^;>]*` class is untouched, since the two answer different questions.
|
||||
|
||||
### Changed
|
||||
|
||||
- Two claims about the catalog's `release-plugin.mjs` were corrected in
|
||||
`CLAUDE.md` after being read off the script rather than assumed: it no longer
|
||||
pushes a tag without `--write`, and its pre-flight gate now runs BEFORE the
|
||||
catalog write, so the half-applied-catalog recovery the old text prescribed
|
||||
has nothing left to recover.
|
||||
|
||||
### Testing
|
||||
|
||||
`board-selftest.sh` 252 -> 259 checks (section 23, both failure directions plus
|
||||
two known-positive controls). All five suites green: coord 230, board 259,
|
||||
route 69, orders 110, guard 40; `npm test` 11/11.
|
||||
|
||||
## [0.27.0] - 2026-08-17
|
||||
|
||||
`v0.26.0` was bumped and pushed but **never tagged**, and two changes landed
|
||||
|
|
|
|||
49
README.md
49
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.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
@ -71,10 +71,8 @@ Message format (filename `<UTC-timestamp>-<uniq>-from-<sender>.md`):
|
|||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
claude plugin install repo-mailbox@ktg-plugin-marketplace
|
||||
```
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
claude plugin install repo-mailbox@ktg-plugin-marketplace
|
||||
|
||||
The plugin ships empty: your mailbox is created lazily on first send, on your machine, and stays there.
|
||||
|
||||
|
|
@ -86,7 +84,7 @@ The plugin ships empty: your mailbox is created lazily on first send, on your ma
|
|||
|
||||
Since v0.8.0 it closes with one aggregate line about mail pending in *other* mailboxes, so an empty inbox no longer reads as "all clear" while messages sit unanswered elsewhere. Two integers, never a roster: naming the other mailboxes would put their situation inside your repo's injection, and the line explicitly disclaims the obligation it sits beneath — those counts are not yours to handle, and counting them delivered nothing.
|
||||
|
||||
**Choosing between repos (the `board` skill).** "What should I work on?", "who is waiting on me?", "what unblocks the most?" — `board.sh` scans every repo it can find and reads three sources per repo: the STATE.md next-step block and its optional board line, `git status`, and the pending count in that repo's mailbox. The skill runs it, ranks by leverage (what unblocks the most, cheapest first) and answers with one repo and the rule that fired, never the table. Read-only: it writes to no repo, no STATE.md, and no mailbox. The board is deliberately *not* wired into session start — it runs when asked. Discovery takes git repos at depth 1, the children of a polyrepo container, and — since 0.33.1 — a repo nested under a depth-1 repo **only when it carries a `STATE.md`**: measured on the real tree, 12 such checkouts existed and exactly 1 had one, so admitting all of them would have buried the board in vendored clones. Every run prints its own denominator (`undersoekt: N katalog(er) depth 1, M polyrepo-container(e), K nestede repo (J med STATE.md tatt med)`), because a repo count alone says how many were found and nothing about how many were looked at.
|
||||
**Choosing between repos (the `board` skill).** "What should I work on?", "who is waiting on me?", "what unblocks the most?" — `board.sh` scans every repo it can find and reads three sources per repo: the STATE.md next-step block and its optional board line, `git status`, and the pending count in that repo's mailbox. The skill runs it, ranks by leverage (what unblocks the most, cheapest first) and answers with one repo and the rule that fired, never the table. Read-only: it writes to no repo, no STATE.md, and no mailbox. The board is deliberately *not* wired into session start — it runs when asked.
|
||||
|
||||
The mailbox is one of its three inputs, which is why the board lives here. Note the axis: a repo's pending count means *others are waiting on it*, an obligation it owes outward. Who a repo waits *on* comes only from its own board line, because the message format has no reply-to field.
|
||||
|
||||
|
|
@ -114,8 +112,6 @@ Ownership is explicit rather than implied. An order is pending until a session *
|
|||
coord-count.sh [--exclude <mailbox>] # per mailbox: pending + replies owed, delivering nothing
|
||||
coord-sweep.sh [--write] [--days <n>] [--log <path>] # close aged notices machine-wide (dry-run by default)
|
||||
board.sh [--roots <dir>[,<dir>...]] [--brief|--plan] [--focus "<prose>"] # cross-repo attention board (read-only)
|
||||
board.sh --voyage # Voyage briefs in flight (read-only)
|
||||
board.sh --row <repo> # one repo's row as field=value lines (read-only)
|
||||
board.sh --dispatch --repo <name> --order-id <id> \
|
||||
--target-pane <yes|no> --path <v> ... --rationale "<why>" # startup command for a session in <name>
|
||||
coord-order-send.sh --to <repo> --subject "<s>" --prompt-file <abs path> # deliver a work order into <repo>'s queue
|
||||
|
|
@ -131,28 +127,11 @@ The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to …
|
|||
|
||||
**`coord-sweep.sh` is the only script that closes a message without a human in the loop**, and it is bounded to one mechanically decidable class: a directed message whose sender declared `reply-expected: no`, older than a grace window (default 14 days). A message that owes a reply is never touched, at any age, with any flag — answering it would mean deciding something on the receiving repo's behalf. Dry-run is the default, inverted from every other script here, because this is the one that destroys pending state. Every closure appends a line naming the sender and subject: a directed message has no seen-tracking, so the sweep cannot tell "seen and ignored" from "never delivered", and a notice to a repo left unopened for the whole window is closed *unread*. The log is what keeps that from being silent.
|
||||
|
||||
**Since 0.33.0 that sweep is scheduled, and the schedule is the whole feature — invocation was the gap, not the mechanism.** `coord-sweep.sh` shipped in 0.10.0 and had then never run once: measured 2026-09-03 across 52 mailboxes, 27 pending directed messages, of which 23 were pure notices being re-injected at every session start in repos nobody had opened. The script was correct and unreachable, so nothing new was built — a second mechanism would have been two copies of a policy that already existed. `launchd/com.ktg.repo-mailbox-sweep.plist` runs `--write --days 14` daily at 05:30, half an hour *before* the briefing agent — not because it changes what the briefing reports, but because the briefing scans the same mailbox this mutates, and the two must not overlap. The briefing's *debt* figure is in fact unaffected: since 0.22.0 it is computed from `coord-count.sh`'s `owed` column, and this sweep closes only messages that owe nothing. What the sweep moves is the raw pending count — the table's `INN` column, and the volume every repo gets injected at session start. The window is written out in the plist rather than left to the script's default: it is a policy constant decided on a measured distribution (30 days would have closed 0 messages, 14 closed 7, 7 would have closed 13), and changing a default must never silently change what an unattended job closes every night.
|
||||
**`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.
|
||||
|
||||
Both agents are **templates**, carrying `__CHECKOUT__`/`__HOME__` placeholders rather than absolute paths, because this repo is mirrored publicly. Substitute them at install time:
|
||||
It makes **zero model calls**, which is the point rather than a detail. Under subscription auth a headless session draws from the same quota pool as interactive work, and `--max-budget-usd` is a runaway brake rather than a pre-flight gate — measured against 2.1.220, it aborts *after* the first turn, never before it. `board.sh --brief` writes nothing; the file write lives in `brief-nightly.sh`, which renders to a temp file and renames it into place, and refuses to replace a good briefing with an empty render. `launchd/` holds a sample agent that runs it nightly; it points at a checkout, never at the version-pinned plugin cache.
|
||||
|
||||
```bash
|
||||
sed -e "s|__CHECKOUT__|$PWD|g" -e "s|__HOME__|$HOME|g" \
|
||||
launchd/com.ktg.repo-mailbox-sweep.plist \
|
||||
> ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
```
|
||||
|
||||
**`launchctl list` proves an agent is *loaded*, never that it is *right*.** A plist naming a script that does not exist loads cleanly and then silently never runs — there is no output to be wrong and no exit status to read, so the failure looks exactly like a quiet machine. Two separate things close that: `launchctl start <label>` followed by a line appearing in `~/Library/Logs/repo-mailbox-sweep.log` is the only runtime proof the program path resolves, and `coord-selftest.sh` section 38 asserts statically, for *every* plist in `launchd/`, that the path it names is a file that exists in this repo, that the `Label` matches the filename, that the placeholders survive, and that no agent points into the version-pinned plugin cache. Note that the launchd log is not the closure log: `$CLAUDE_COORD_DIR/_sweep.log` is where the record of each closed notice lives.
|
||||
|
||||
**`board.sh --brief` renders the nightly briefing**, a second rendering of the scan the board already does rather than a second scan: the repos with an unhandled inbox, each one's next step *in full* (the 38-character cut belongs to the table column, not to the record), and the exact command to start a session there — derived by calling `route.sh` with that repo's own four traits, since `next-cost` names the row rather than spelling the command and there must be exactly one spelling in circulation. A repo with no route line is told so rather than handed a guessed command. It also cross-checks itself against `coord-count.sh`, because the repo scan and the mailbox are different populations: a mailbox can carry a name no scan will ever produce, such as a declared non-git surface (`CLAUDE_COORD_REPO`) or a checkout outside the roots, and a briefing that only walked the scan would answer "who is waiting on you" with a number it quietly knew was short.
|
||||
|
||||
It makes **zero model calls**, which is the point rather than a detail. Under subscription auth a headless session draws from the same quota pool as interactive work, and `--max-budget-usd` is a runaway brake rather than a pre-flight gate — measured against 2.1.220, it aborts *after* the first turn, never before it. `board.sh --brief` writes nothing; the file write lives in `brief-nightly.sh`, which renders to a temp file and renames it into place, and refuses to replace a good briefing with an empty render. `launchd/` holds a sample agent that runs it nightly at 06:00; like the sweep agent above it points at a checkout, never at the version-pinned plugin cache, and it is pinned by the same section 38 checks.
|
||||
|
||||
**`board.sh --voyage` reports the Voyage briefs in flight**, a sixth rendering of the same scan. `board` reads STATE lines, which say nothing about a brief, so a programme running Voyage across several repos had no shared surface: nobody could answer which briefs were running, in what phase, and who was waiting on whom. Detection is by **property, never by directory name** — a directory holding `brief.md` or `brief.md.draft` under any of the three planning locations the convention recognises (`.claude/projects/`, `docs/`, `features/<n>-<name>/`) — and it walks the **filesystem, never the git index**: a repo that gitignores `.claude/projects/` would otherwise report zero briefs while actually running one. The phase ladder measures *artifacts*, not sessions: a plan executed in a single session leaves no file behind, so `plan` is the last thing the filesystem can prove, and nothing here claims a session is alive. `brief_quality` is read out of the brief's frontmatter and an absent field reads `-`, never `complete`; a research directory that exists and holds nothing reads `0`, distinct from the `-` that means no research step was ever started. The table carries a matching `VOY` column beside `ORDRE` and `FLY` — the same class of durable filesystem fact, and never summed with them.
|
||||
|
||||
**`board.sh --row <repo>` renders one repo's row as `field=value` lines**, a seventh rendering of the same scan and the only one that answers about a single repo. It exists because the table's columns *were* misread: the fixed-width view is for a human, and reading a column off it by eye is how `FLY` was read as `ORDRE` on 2026-09-11 — while every other rendering a program consumes is already `key=value`. `inn`, `ordre` and `fly` are three separate fields because they are three separate facts; `status` is the bare token rather than the table's `blocked>target` display, with `blocked-on` beside it; `neste` is last and uncut. An unknown repo exits **2** and writes nothing, because an empty block would read as a repo whose every column is blank — a real and different state. One field is deliberately *not* a rendering of the scan and is named as such: `upushet` is measured once, for that repo only, from the remote-**tracking** ref rather than the remote, so a repo with no upstream reports `?` and never `0`.
|
||||
|
||||
**`board.sh --plan` renders the day plan**, a *third* rendering of that same scan and the only one that takes a position: which repos to open a tab for today, in what order, and the command to start each. The order is the position, and there is no cutoff — nothing is hidden, and **five ordered groups** decide it, each a lookup over a field the scan already read rather than a weighted score: (1) **chain-root credit**, most repos released first; (2) **debt**, most-owed-first, whatever the status; (3) `planned`; (4) `in-progress`; (5) `?`/`MALFORMED` — undeclared, last and labelled. Within a group, ties break on a cheap Sonnet `next-cost` row, then oldest plan first. A 0.19.0 weighted score (`40 ×` repos released, `15 ×` unhandled inbox messages) briefly stood in this spot and *could* express "this repo owes one message and releases two others" as a single number — but re-tuning those two coefficients would have silently reordered a parser living in another repo, with no test here able to hold a ranking stable for a consumer it can't see; the operator replaced it with the group order in 0.20.0 for that reason. Groups 3 and 4 are `planned` above `in-progress`, inverted from every earlier version by the same decision: turning a decision into motion is the slow step, live work is already moving. **Chain-root credit** follows `blocked-on` transitively to the first repo that is not itself blocked and credits only that root: opening a blocked repo releases nobody, since its own next step is by definition waiting. A cycle, or a `blocked-on` naming a repo the scan never produced, credits nobody rather than inventing a root — a plan that looks correct while sending you to the wrong repo is worse than one that says nothing. Repos owing mail still rank high *whatever their status*, and debt is deliberately **uncapped**: excluding `blocked` or `done` is a statement about a repo's own next step, which cannot be moved, while owing an answer is a different axis and answering is often what unblocks it. Repos with no board line come last and labelled — the table already prints a note about those, so a plan that dropped them silently would repeat exactly that defect. `why=` names the group that placed the repo, so a block reads `unblocks:2` rather than the `inbox:N` every block used to repeat. Still zero model calls, still read-only, and still cross-checked against `coord-count.sh`.
|
||||
**`board.sh --plan` renders the day plan**, a *third* rendering of that same scan and the only one that takes a position: which repos to open a tab for today, in what order, and the command to start each. The order is the position, and there is no cutoff — nothing is hidden, and one deterministic score decides it: `40 ×` repos released transitively, `15 ×` unhandled inbox messages, plus small bonuses for live work and for a cheap `next-cost` row. Four hard buckets preceded it and could not express "this repo owes one message and releases two others" — which is how a blocked chain's root ended up ranked *below* the repos waiting on it. **Chain-root credit** follows `blocked-on` transitively to the first repo that is not itself blocked and credits only that root: opening a blocked repo releases nobody, since its own next step is by definition waiting. A cycle, or a `blocked-on` naming a repo the scan never produced, credits nobody rather than inventing a root — a plan that looks correct while sending you to the wrong repo is worse than one that says nothing. Repos owing mail still rank high *whatever their status*, and debt is deliberately **uncapped**: excluding `blocked` or `done` is a statement about a repo's own next step, which cannot be moved, while owing an answer is a different axis and answering is often what unblocks it. Repos with no board line come last and labelled — the table already prints a note about those, so a plan that dropped them silently would repeat exactly that defect. `why=` names the dominant term, so a block reads `unblocks:2` rather than the `inbox:N` every block used to repeat. Still zero model calls, still read-only, and still cross-checked against `coord-count.sh`.
|
||||
|
||||
**`--focus "<prose>"` narrows that plan to one subject, and reports what it held back.** It is the only cutoff this format has, which is why the report is a condition of the feature rather than a refinement of it: `--plan` documents that it hides nothing and labels what it cannot rank, so a silent filter would break a property already written down. The same run prints the subjects the prose resolved to, how many blocks were removed, how many `STATE.md` were searched, and — named, not counted — the repos that *mention* a resolved subject without declaring a marker line. That last class is enumerated because it is where the misses live: a repo can be a heavy participant and never have written a marker, and no string measurement finds it until the held-back population is listed. Each surviving block carries the declaration it survived on. Prose matching nothing prints the *full* plan and says so, since the phrase arrives verbatim from a person and a typo must not empty the day. The subject vocabulary is read from the scanned `STATE.md` themselves, so the "reads `STATE.md` and no other file" invariant is untouched.
|
||||
|
||||
|
|
@ -198,12 +177,12 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
|
|||
|
||||
## Development
|
||||
|
||||
bash scripts/coord-selftest.sh # 257 checks against a throwaway mailbox
|
||||
bash scripts/board-selftest.sh # 427 checks against a throwaway repo tree
|
||||
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
|
||||
bash scripts/orders-selftest.sh # 116 checks, incl. the 20-way barriered claim race
|
||||
bash scripts/state-line-guard-selftest.sh # 54 checks, incl. the Edit replace_all projection and the ratchet
|
||||
npm test # all five selftests, the hook tests, and the README-number check
|
||||
bash scripts/coord-selftest.sh # 220 checks against a throwaway mailbox
|
||||
bash scripts/board-selftest.sh # 237 checks against a throwaway repo tree
|
||||
bash scripts/route-selftest.sh # 69 checks, incl. the route->board round trip
|
||||
bash scripts/orders-selftest.sh # 99 checks, incl. the 20-way barriered claim race
|
||||
bash scripts/state-line-guard-selftest.sh # 40 checks, incl. the Edit replace_all projection and the ratchet
|
||||
npm test # all five selftests plus the hook tests, via node --test
|
||||
|
||||
TDD is the house rule: every behavior change lands with a failing selftest check first.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
# Which repos are free? The investigation, and where it stopped
|
||||
|
||||
Ordered 2026-08-23. The operator's words, translated: *"we need more precision
|
||||
about you knowing which repos are finished and can take more work."* What was
|
||||
ordered was an investigation and a design, not a named column — the sending repo
|
||||
supplied the problem and the measurements and left the shape of the answer here.
|
||||
|
||||
Repos are unnamed throughout. This is a public mirror, and which repo was idle
|
||||
for how long is not a fact this file needs to carry to make its argument.
|
||||
|
||||
## The incident
|
||||
|
||||
A session read the board's `ORDRE` column, ran `pgrep -fl claude`, and told the
|
||||
operator that four sessions were working. The operator looked at their screen:
|
||||
one was. Measured afterwards, two of the four processes had accumulated ~38
|
||||
minutes of CPU across **45 hours** of wall clock. They were open panes sitting at
|
||||
a prompt, holding finished orders, during a window in which full quota had been
|
||||
authorised against a deadline four days out.
|
||||
|
||||
Nothing on the board reported it, and that is the part this file is about.
|
||||
|
||||
## Three blind spots, one of which was ours
|
||||
|
||||
1. **`ORDRE` counted pending orders only.** So a repo with one order in flight
|
||||
and a repo with no orders at all both printed `0`. The same digit for two
|
||||
opposite facts — "work is happening here" and "nothing is waiting here" —
|
||||
with no way to tell them apart. This one is a defect in this repo's own
|
||||
rendering, and it is fixed.
|
||||
2. **`STATUS` describes the plan, not the capacity.** `done` does mean "no open
|
||||
step", which is close to what the operator wanted; `planned` and
|
||||
`in-progress` say nothing about whether anyone is actually sitting there. One
|
||||
of the idle repos was `planned` for the full 45 hours, entirely correctly.
|
||||
3. **A process proves existence, not activity.** `pgrep` finds a session that
|
||||
finished everything and went quiet. This one is not ours to fix — see below.
|
||||
|
||||
## What was changed
|
||||
|
||||
### `FLY`: the order queue's other state
|
||||
|
||||
A second count over `orders/claimed/`, printed in its own column beside `ORDRE`
|
||||
and never summed with it. Same queue, other state — **not a fourth axis**, which
|
||||
is why it is a second reading of a source the board already had rather than a new
|
||||
source.
|
||||
|
||||
Verified live on the day it shipped: one repo went from `ORDRE 0` (reading as
|
||||
"nothing here") to `ORDRE 0 / FLY 1`, next to another repo still reading
|
||||
`ORDRE 0 / FLY 0` and genuinely holding nothing. Those two rows had been
|
||||
byte-identical the day before.
|
||||
|
||||
**What `FLY` does not mean, and must never be reworded into: that a session is
|
||||
alive.** A claim is a `mv` a session performed once. Nothing un-claims it when
|
||||
that session dies — which is exactly why the order queue's read path already
|
||||
shows claimed orders with their in-flight age. Measured on the live mailbox the
|
||||
same day: one order had been sitting claimed for **117 hours**. `FLY` is evidence
|
||||
that someone took the order. It is not evidence that anyone is still working it,
|
||||
and the on-screen legend says so in those words, because a column that read as "a
|
||||
session is running here" would be the process axis smuggled in as a file count.
|
||||
|
||||
### `--plan` names free capacity
|
||||
|
||||
`ledig_antall=N`, then one `ledig=<repo> (<status>)` line per free repo. Free
|
||||
means all four at once: nothing **owed** (not merely nothing pending — a notice
|
||||
is not an obligation), no pending order, nothing in flight, clean tree, at
|
||||
`done` or `deferred`.
|
||||
|
||||
All four conditions are load-bearing. Measured on the real tree the same day: of
|
||||
17 `done`/`deferred` repos, **13 were free and 4 were not** — two held a pending
|
||||
order, one owed a reply, one had an uncommitted tree. `status=done` alone would
|
||||
have named the wrong set roughly a quarter of the time.
|
||||
|
||||
It is emitted as **lines, never as blocks**, and that is the whole design rather
|
||||
than a formatting preference. The plan's second consumer opens one pane per
|
||||
block and discards any block without a `tab=` key; a free repo written as a block
|
||||
would therefore be visible to the operator and invisible to the driver. A line is
|
||||
visible to both and can never be opened as a tab by accident. The count prints
|
||||
even when it is zero, so "none found" and "not computed" cannot render as the
|
||||
same output.
|
||||
|
||||
A tab block whose repo already holds a claimed order additionally carries
|
||||
`fly=N`. The driver types into live panes; it should be able to see that first.
|
||||
|
||||
## What was NOT changed, and the argument for it
|
||||
|
||||
**The board still inspects no processes.** No `pgrep`, no `ps`, no `lsof`, and a
|
||||
structural check in the selftest now says so with a known-positive control
|
||||
proving the grep can find a planted call. This was the order's open design
|
||||
question, and the answer is no, for four reasons that compound:
|
||||
|
||||
1. **Every other column is a durable filesystem fact.** They survive a reboot and
|
||||
they are reproducible in a throwaway fixture tree under `CLAUDE_COORD_DIR`. A
|
||||
process column measures the operator's machine at one instant. There is no
|
||||
fixture for it, so it would ship as an unmeasured assumption wearing a passing
|
||||
test — this repo's own named defect class.
|
||||
2. **Mapping a process to a repo needs its working directory**, which on macOS
|
||||
means `lsof`. That is a new external dependency against a stated zero-
|
||||
dependency convention, for a number that would still not mean what a reader
|
||||
would take it to mean.
|
||||
3. **The discriminator is a threshold, and a threshold is a verdict.** "38
|
||||
minutes of CPU across 45 hours is idle" is a judgement about a session's
|
||||
liveness. Encoding it makes the board decide that a session is dead. The order
|
||||
queue is already forbidden from doing exactly this: nothing there expires
|
||||
anything, because building expiry would require the engine to know something
|
||||
it cannot. A process column is that same rule broken on a different surface.
|
||||
4. **The file-based signal is not a substitute either**, and pretending otherwise
|
||||
would just move the error. The 117-hour claim is the proof. So the board
|
||||
reports the claim and refuses the inference, which is what "prints evidence
|
||||
and takes no position" has always meant here.
|
||||
|
||||
The gap this leaves is real and is stated rather than closed: **a pane that is
|
||||
open and idle with no claimed order and no queued work is still invisible to the
|
||||
board.** Such a repo now shows up in `ledig=` if it is `done`/`deferred` and
|
||||
clean, which covers the common case; a `planned` repo with an idle pane does not,
|
||||
and cannot, because nothing in the filesystem distinguishes it from a `planned`
|
||||
repo nobody has opened. Closing that needs a terminal-side measurement, and that
|
||||
belongs to whatever drives the terminal, not to the mailbox.
|
||||
|
||||
## What this does not answer
|
||||
|
||||
The order asked whether the board should say anything about open tabs *at all*.
|
||||
It now says nothing about them, deliberately. If the operator later wants pane
|
||||
occupancy on the board, the honest construction is for the terminal driver —
|
||||
which already probes panes and already consumes `--plan` — to supply that fact
|
||||
inward, the same way `--dispatch` requires `--target-pane` to be measured by the
|
||||
caller and passed in rather than looked up here. That direction keeps the
|
||||
dependency running one way and keeps this repo unable to break on a terminal
|
||||
API. It was not built, because it was not ordered and the operator has not asked
|
||||
for it.
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
# Coordination debt: what does not converge, and why building a second
|
||||
# mechanism would have been wrong
|
||||
|
||||
Measured 2026-09-03 against the live mailbox, for order
|
||||
`20260902T113745Z-1254925290-from-.claude` (WP5). Every number below was
|
||||
produced by a command, and every negative result carries the control that
|
||||
proves the query could have found something.
|
||||
|
||||
The order offered two candidate mechanisms and said to choose by measurement,
|
||||
not taste: a broadcast TTL, or a bulk-ack for pure notices. The measurement
|
||||
chose neither. One of them is already built and has never been run; the other
|
||||
addresses the class that converges on its own.
|
||||
|
||||
## Denominators
|
||||
|
||||
| population | count |
|
||||
|---|---|
|
||||
| mailbox directories under the coord root | 55 |
|
||||
| of those, holding an `inbox/` | 52 |
|
||||
| pending directed messages across all inboxes | 27 |
|
||||
| broadcasts in `_broadcast/inbox/` | 17 |
|
||||
| (mailbox x broadcast) delivery pairs | 884 |
|
||||
|
||||
The three directories with no `inbox/` are named rather than silently dropped:
|
||||
`jobbsok`, `mediemon`, `medieovervaaking`. 55 - 3 = 52 is the reconciliation,
|
||||
stated because an unreconciled pair of denominators in one report is the same
|
||||
positive-looking null this engine refuses everywhere else.
|
||||
|
||||
## The two classes behave in opposite directions
|
||||
|
||||
**Directed messages do not converge.** They are re-injected at every session
|
||||
start until a session closes them by hand. Of the 27 pending, 4 owe a reply and
|
||||
23 are pure notices (`reply-expected: no`). By age:
|
||||
|
||||
| class | <7d | 7-13d | 14-29d | >=30d |
|
||||
|---|---|---|---|---|
|
||||
| owes a reply | 0 | 3 | 1 | 0 |
|
||||
| pure notice | 10 | 6 | 7 | 0 |
|
||||
|
||||
**Broadcasts converge on their own.** Reading one records it as seen, so a
|
||||
mailbox clears its whole backlog on its next session. 263 of the 884 pairs are
|
||||
unread (29.8%), and the distribution shows the self-clearing: 10 mailboxes are
|
||||
fully current, 26 sit at exactly 6 unread (the newest announcements), and the
|
||||
tail is short.
|
||||
|
||||
## The floor under the broadcast number, which strengthens the case
|
||||
|
||||
Two mailboxes hold all 17 broadcasts unread, and neither can ever read them:
|
||||
|
||||
- `ktg-plugin-marketplace` is a **retired `--to` address**. It is a polyrepo
|
||||
directory, not a git repo, so `basename(git toplevel)` can never resolve to
|
||||
it and no session can hold that identity. This is already documented as
|
||||
engine behaviour; the 17 unread are its permanent consequence.
|
||||
- `llm-ingestion-guard` has no checkout anywhere under `/Users/ktg`
|
||||
(`find -maxdepth 4`, with `llm-ingestion-okf` as the known-positive control
|
||||
proving the query finds a real one) and no `.origin`. It also holds the
|
||||
single oldest pending notice, 24 days.
|
||||
|
||||
So **34 of 263 unread pairs (12.9%) are a permanent floor no TTL would reduce
|
||||
to zero** - it would close them, but it would be closing announcements for
|
||||
mailboxes that were never going to read anything. `.origin` absence alone is
|
||||
NOT a proxy for unholdable: `repos` also lacks one, yet sits at 2 unread of 17,
|
||||
which is only possible if something reads it.
|
||||
|
||||
## Why no second mechanism was built
|
||||
|
||||
`coord-sweep.sh` already **is** the bulk-ack for pure notices: machine-wide,
|
||||
one mechanically decidable class (`reply-expected: no`, older than a grace
|
||||
window), dry-run by default, closing through `coord-done.sh`, logging sender
|
||||
and subject for every closure. Building the order's second candidate would have
|
||||
been a second copy of a shipped policy - the defect class this repo names
|
||||
repeatedly.
|
||||
|
||||
Its own dry-run reports what it would close today, and the figures agree
|
||||
exactly with the independent classification above:
|
||||
|
||||
--days 30 -> 0 messages
|
||||
--days 14 -> 7 messages
|
||||
--days 7 -> 13 messages
|
||||
|
||||
**The gap is invocation, not mechanism.** No `_sweep.log` exists at the default
|
||||
path, and no sweep log exists anywhere under `~/.claude` (the one `*sweep*` hit
|
||||
is an unrelated plugin file) - stated that way because `--log` can override the
|
||||
default, so an absent default-path log alone would not prove it never ran.
|
||||
|
||||
## What remains, and why it is not this repo's call
|
||||
|
||||
Making the notice class converge without opening each repo means running
|
||||
`coord-sweep.sh --write` unattended. That closes mail in 51 other repos'
|
||||
inboxes, unread, and the script's own design says so in as many words: a notice
|
||||
to a repo left unopened for the whole window is closed unread, and the log is
|
||||
the only thing standing between that and a silent disappearance.
|
||||
|
||||
Deciding that on another repo's behalf is the one anti-pattern with no
|
||||
exception clause, and the grace window is a policy constant of the same class
|
||||
as the STATE.md line limit, which was an operator decision both times it moved.
|
||||
Dry-run is the default precisely because this is the script that destroys
|
||||
pending state; flipping that to a schedule is the operator's act, not a
|
||||
plugin's.
|
||||
|
||||
The messages that owe a reply are untouched by any of this, at any age, with
|
||||
any flag. That is not a gap to close later - it is the rule that keeps a
|
||||
procedural duty from becoming a substantive one.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: the before-state, captured 2026-09-04 08:0x UTC
|
||||
|
||||
The operator authorized the sweep on 2026-09-03 (14-day window, scheduled via
|
||||
launchd). This is `coord-count.sh` immediately before the authorized
|
||||
`--write --days 14` run, recorded here because a before-state stops existing
|
||||
the moment the write happens, and the order asks for debt before/after with a
|
||||
denominator.
|
||||
|
||||
mailbox pending owed origin-age
|
||||
app-creator 1 1 -
|
||||
claude-playlist-corpus 1 0 -
|
||||
graceful-handoff 3 0 -
|
||||
human-friendly-style 1 0 -
|
||||
ki-produktivitetsmodell 1 0 -
|
||||
llm-ingestion-guard 1 0 25
|
||||
llm-security 2 0 -
|
||||
mcp-servere 2 0 -
|
||||
okr 1 0 -
|
||||
org-ops 5 2 -
|
||||
portfolio-optimiser-commons 3 0 -
|
||||
repo-standard 1 1 -
|
||||
wiki-advise 2 0 -
|
||||
.claude 1 0 -
|
||||
.profile 1 0 -
|
||||
------------------------------------------------------
|
||||
15 mailboxes with pending mail 26 4
|
||||
|
||||
`$CLAUDE_COORD_DIR/_sweep.log` did not exist: the sweep had still never run.
|
||||
|
||||
**The control this file exists to make runnable:** after the write, the `owed`
|
||||
column must be BYTE-IDENTICAL (total 4), because the sweep spares every message
|
||||
that owes a reply at any age. A changed `owed` figure means the sweep closed
|
||||
something it must never touch, and is a defect, not a result.
|
||||
|
||||
**The dry-run said 11, not the 7 this document measured a day earlier, and the
|
||||
gap is entirely the moving cutoff.** Verified two ways rather than assumed.
|
||||
Yesterday's 14-day cutoff was ~20260820T184902; today's is 20260821T055442.
|
||||
Four notices timestamped 2026-08-20 between those two instants
|
||||
(`human-friendly-style` T210113Z, `ki-produktivitetsmodell` T205611Z, `org-ops`
|
||||
T210955Z and T211358Z) crossed the boundary in one calendar day: 7 + 4 = 11.
|
||||
Independently, `--days 15` today yields 6, and those 6 plus
|
||||
`mcp-servere/20260820T104644Z` - which sits between the 15-day cutoff and
|
||||
yesterday's 14-day one - reconstruct yesterday's 7 exactly. The engine is
|
||||
consistent; the 7 was a measurement of a moment, never a constant, and reading
|
||||
it as one would have been face 3 of the verification law pointed at our own
|
||||
report.
|
||||
|
|
@ -5,10 +5,6 @@
|
|||
// ~120-line convention (global CLAUDE.md's Kontinuitets-system section;
|
||||
// raised from ~60 by operator decision 2026-08-14).
|
||||
//
|
||||
// Env: CLAUDE_STATE_MAX_LINES overrides that limit (positive integer). An
|
||||
// UNUSABLE value is refused by name, never silently ignored - see
|
||||
// resolveMaxLines() below for why that direction is the safe one.
|
||||
//
|
||||
// PreToolUse, not PostToolUse: org-ops' work order (20260814T144553Z) asked
|
||||
// for a PostToolUse hook, but PostToolUse fires AFTER the tool already ran
|
||||
// and cannot undo the write (confirmed against the official hooks docs,
|
||||
|
|
@ -116,34 +112,7 @@ import { readFileSync } from 'node:fs';
|
|||
import { basename, dirname } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const DEFAULT_MAX_LINES = 120;
|
||||
|
||||
// F13: the limit was a bare constant, so a selftest of the BOUNDARY had to
|
||||
// hardcode the same number the code carries - two copies of one policy, and
|
||||
// every fixture had to be rewritten by hand the last time the operator moved
|
||||
// it (60 -> 120, 2026-08-14). CLAUDE_STATE_MAX_LINES is the same kind of knob
|
||||
// CLAUDE_COORD_DIR is for the mailbox root: it lets a test pin the boundary at
|
||||
// a cheap value, and it lets the operator move the limit without a release.
|
||||
//
|
||||
// It is not a bypass claim. This guard has always been escapable by writing
|
||||
// the file another way (Bash, an editor), exactly as the sibling pathguard is.
|
||||
// The one thing it must never do is silently fail to take effect, which is why
|
||||
// an UNUSABLE value returns null and is refused by name below rather than
|
||||
// falling back to the default: a caller who set the variable and got 120
|
||||
// anyway would be reading a limit that was never in force - the same
|
||||
// positive-looking null this whole class of fix exists to close.
|
||||
//
|
||||
// Refused: "" (a variable expanded from something unset - a value was meant),
|
||||
// "0" and negatives (a limit no write can satisfy), and anything not made of
|
||||
// digits ("abc", "12.5", "1e3"). Unset is NOT unusable; it is the normal case.
|
||||
function resolveMaxLines() {
|
||||
const raw = process.env.CLAUDE_STATE_MAX_LINES;
|
||||
if (raw === undefined) return DEFAULT_MAX_LINES;
|
||||
if (!/^[0-9]+$/.test(raw)) return null;
|
||||
const n = Number(raw);
|
||||
if (!Number.isSafeInteger(n) || n < 1) return null;
|
||||
return n;
|
||||
}
|
||||
const MAX_LINES = 120;
|
||||
|
||||
function allow() {
|
||||
process.exit(0);
|
||||
|
|
@ -181,22 +150,6 @@ if (
|
|||
allow();
|
||||
}
|
||||
|
||||
// Resolved here, AFTER the STATE.md gate above: an unusable override must not
|
||||
// block a Write this guard would never have judged in the first place.
|
||||
const MAX_LINES = resolveMaxLines();
|
||||
if (MAX_LINES === null) {
|
||||
process.stderr.write(
|
||||
`\n[repo-mailbox] STATE LINE GUARD: ${toolName} blocked\n` +
|
||||
` File: ${filePath}\n` +
|
||||
` CLAUDE_STATE_MAX_LINES is set to ${JSON.stringify(process.env.CLAUDE_STATE_MAX_LINES)}, ` +
|
||||
`which is not a positive whole number of lines.\n\n` +
|
||||
`The limit was NOT applied and the write was NOT judged. Set ` +
|
||||
`CLAUDE_STATE_MAX_LINES to a positive integer, or unset it to use the ` +
|
||||
`default of ${DEFAULT_MAX_LINES}.\n`
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let projected;
|
||||
let currentLines;
|
||||
if (toolName === 'Write') {
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!--
|
||||
Daily FYI sweep. Closes directed messages whose sender declared
|
||||
reply-expected: no and whose filename timestamp is older than 14 days, across
|
||||
every mailbox on this machine, through coord-done.sh.
|
||||
|
||||
INVOCATION WAS THE GAP, NOT THE MECHANISM. coord-sweep.sh shipped in 0.10.0
|
||||
and had never run once against the real mailbox - measured 2026-09-03: 27
|
||||
pending directed messages, 23 of them pure notices nobody was ever going to
|
||||
act on, re-injected at every session start in repos nobody had opened. The
|
||||
script was correct and unreachable. This file is the whole fix; no new
|
||||
mechanism was built, and building a second one would have been two copies of
|
||||
a policy that already existed.
|
||||
|
||||
THE 14-DAY WINDOW IS THE OPERATOR'S CONSTANT, NOT A DEFAULT WEARING A
|
||||
SCHEDULE (decided 2026-09-03, on a measured distribution: 30d -> 0 messages,
|
||||
14d -> 7, 7d -> 13). It is written out explicitly here rather than left to
|
||||
coord-sweep.sh's own default, so that changing the script's default can never
|
||||
silently change what this agent closes every night.
|
||||
|
||||
WHAT IT CAN NEVER DO. A message that owes a reply is untouched at any age -
|
||||
the script's own rule, not this file's. This agent only supplies the
|
||||
invocation; every bound on what gets closed lives in coord-sweep.sh, and the
|
||||
closure log ($CLAUDE_COORD_DIR/_sweep.log, NOT the launchd log below) is the
|
||||
only record that a notice closed unread ever existed.
|
||||
|
||||
THE TWO AGENTS MUST NEVER SHARE AN HOUR. com.ktg.repo-mailbox-brief renders
|
||||
at 06:00 from a scan of the same mailbox this mutates, so a briefing rendered
|
||||
mid-sweep reads a mailbox changing underneath it. This runs at 05:30, clear of
|
||||
it; coord-selftest.sh section 38 asserts the two hours differ.
|
||||
|
||||
What it does NOT change is the briefing's DEBT figure. Since 0.22.0 that is
|
||||
computed from coord-count.sh's `owed` column, and this sweep closes only
|
||||
messages that owe nothing - so the debt listing is identical before and after.
|
||||
What moves is the raw pending count (the table's INN column, the FYI-only
|
||||
naming in --brief, and the volume every repo gets injected at session start).
|
||||
Claiming the briefing reports "the debt that remains" because of this agent
|
||||
would be an overclaim; the ordering exists for the read/write overlap alone.
|
||||
|
||||
ZERO MODEL CALLS, same as the briefing and for the same reason: the operator
|
||||
authenticates by subscription, so a headless `claude -p` job would draw from
|
||||
the same quota pool as interactive work. This runs one shell script.
|
||||
|
||||
PATH: every binary this touches (bash, date, grep, sed, basename, tr, cut)
|
||||
lives in /usr/bin or /bin, so launchd's minimal default PATH is sufficient
|
||||
and no EnvironmentVariables block is needed.
|
||||
|
||||
The program path points at the SOURCE REPO, deliberately. The alternative is
|
||||
version-pinned (~/.claude/plugins/cache/.../repo-mailbox/<version>/...), so
|
||||
an agent pointing there would break silently on the next version bump - and a
|
||||
second copy of these scripts on disk is the exact defect class that produced
|
||||
the 0.12.1 stale-fallback bug.
|
||||
|
||||
This file is a TEMPLATE. It carries no absolute home path on purpose: the
|
||||
repo is mirrored publicly, and a plist is the one file here that would need
|
||||
one. Substitute both placeholders at install time.
|
||||
|
||||
Install: sed -e "s|__CHECKOUT__|$PWD|g" -e "s|__HOME__|$HOME|g" \
|
||||
launchd/com.ktg.repo-mailbox-sweep.plist \
|
||||
> ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
Verify: launchctl list | grep com.ktg.repo-mailbox-sweep # loaded only
|
||||
launchctl start com.ktg.repo-mailbox-sweep # proves the path
|
||||
tail ~/Library/Logs/repo-mailbox-sweep.log # the actual proof
|
||||
Remove: launchctl unload ~/Library/LaunchAgents/com.ktg.repo-mailbox-sweep.plist
|
||||
|
||||
`launchctl list` proves the agent is LOADED, never that it does anything
|
||||
right: a wrong program path produces a loaded agent that silently never runs.
|
||||
Only `launchctl start` plus a line in the log below proves the path resolves.
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.ktg.repo-mailbox-sweep</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>__CHECKOUT__/scripts/coord-sweep.sh</string>
|
||||
<string>--write</string>
|
||||
<string>--days</string>
|
||||
<string>14</string>
|
||||
</array>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>__HOME__/Library/Logs/repo-mailbox-sweep.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>__HOME__/Library/Logs/repo-mailbox-sweep.log</string>
|
||||
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>5</integer>
|
||||
<key>Minute</key>
|
||||
<integer>30</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-mailbox",
|
||||
"version": "0.34.0",
|
||||
"version": "0.27.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
1590
scripts/board-selftest.sh
Normal file → Executable file
1590
scripts/board-selftest.sh
Normal file → Executable file
File diff suppressed because it is too large
Load diff
1033
scripts/board.sh
1033
scripts/board.sh
File diff suppressed because it is too large
Load diff
|
|
@ -35,20 +35,7 @@
|
|||
# --exclude <mailbox> omit one mailbox (the caller's own, whose inbox is
|
||||
# already injected in full).
|
||||
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
|
||||
# Exit: 0 = counted (zero or more mailboxes have pending mail)
|
||||
# 2 = usage error, nothing counted
|
||||
# 3 = mailbox root does not exist, nothing counted
|
||||
#
|
||||
# The header used to promise exit 0 unconditionally, on the grounds that this
|
||||
# runs at session start and must never fail one - and that was false in both
|
||||
# directions (F14). It exited 2 on a usage error already, and - worse - it exited 0 with zero lines when the mailbox root
|
||||
# was ABSENT, which is byte-identical to "no mailbox has pending mail" on every
|
||||
# channel a consumer can read (F5). board.sh consumes this TSV. That is
|
||||
# Verifiseringsloven ansikt 4: a broken query returning a positive-looking null.
|
||||
# What the old claim was protecting is kept and made precise: no state OF THE
|
||||
# MAILBOX can ever produce a nonzero exit - not an empty root, not a malformed
|
||||
# message, not an unreadable date. Only the caller (2) or a missing root (3)
|
||||
# can, and both print nothing on stdout, so neither can be mistaken for a count.
|
||||
# Exit: always 0 - this runs at session start and must never fail one.
|
||||
# ASCII only, bash 3.2 safe.
|
||||
set -u
|
||||
export LC_ALL=C
|
||||
|
|
@ -69,15 +56,7 @@ while [ $# -gt 0 ]; do
|
|||
esac
|
||||
done
|
||||
|
||||
# Not `|| exit 0`: see the F5 paragraph in the header. Status 3 rather than 2
|
||||
# because 2 is already "you called me wrong" and this is "the world you named
|
||||
# is not there" - two different repairs, and a consumer that only ever sees one
|
||||
# integer cannot tell them apart. stdout stays empty on purpose: 3 is not a
|
||||
# count of zero, it is the absence of a count.
|
||||
if [ ! -d "$COORD" ]; then
|
||||
echo "coord-count: mailbox root does not exist: $COORD (not counted, not zero)" >&2
|
||||
exit 3
|
||||
fi
|
||||
[ -d "$COORD" ] || exit 0
|
||||
|
||||
# GNU/BSD date flavor, detected once per run (not per mailbox): BSD date
|
||||
# rejects --version outright (exit nonzero, "illegal option" - measured on
|
||||
|
|
|
|||
|
|
@ -22,14 +22,6 @@
|
|||
set -u
|
||||
export LC_ALL=C
|
||||
|
||||
# ORDRE 65 follow-on (.claude, 2026-08-17): the WHEN DONE / IF YOU CANNOT
|
||||
# lines below are handed directly to the CLAIMING session as its own
|
||||
# next-step instruction - the same bare-verb-name defect the order named in
|
||||
# board.sh's dispatch starter. SELFDIR mirrors that fix: derived from where
|
||||
# THIS script is running FROM ($0's directory), correct at the moment it
|
||||
# prints, for whichever install location is live then.
|
||||
SELFDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
|
||||
|
||||
REPO=""; NEXT=0; ORDER_ID=""
|
||||
|
|
@ -106,8 +98,8 @@ echo "coord-order-claim: CLAIMED $ORDER_ID for $REPO. This order is yours until
|
|||
echo "BEFORE YOU START: read this repo's STATE.md NESTE block and compare it with the order below."
|
||||
echo "If they are different tasks, say so in your FIRST reply, in one line:"
|
||||
echo " \"order $ORDER_ID displaces NESTE <what NESTE says>; <that> stands as next after\"."
|
||||
echo "WHEN DONE: bash $SELFDIR/coord-order-done.sh $ORDER_ID --commit <hash> (or --no-commit --reason \"<why>\")"
|
||||
echo "IF YOU CANNOT: bash $SELFDIR/coord-order-done.sh $ORDER_ID --return --reason \"<why>\" - it goes back to the queue."
|
||||
echo "WHEN DONE: coord-order-done $ORDER_ID --commit <hash> (or --no-commit --reason \"<why>\")"
|
||||
echo "IF YOU CANNOT: coord-order-done $ORDER_ID --return --reason \"<why>\" - it goes back to the queue."
|
||||
echo "--- order $ORDER_ID ---"
|
||||
cat "$CLAIMED/$ORDER_ID.md"
|
||||
echo "--- end of order $ORDER_ID ---"
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@
|
|||
set -u
|
||||
export LC_ALL=C
|
||||
|
||||
# ORDRE 65 (.claude, 2026-08-17): `coord-order-claim`/`coord-order-done` are
|
||||
# not on PATH. This block is injected verbatim at SessionStart as
|
||||
# additionalContext - a session reading it may run the shown command via its
|
||||
# own Bash tool, so a bare verb name is command-not-found on the very first
|
||||
# try, misreadable as "the order does not exist" (Verifiseringsloven ansikt
|
||||
# 4). SELFDIR is derived from where THIS script is actually running FROM
|
||||
# ($0's directory), the same technique board.sh uses for its dispatch
|
||||
# starter - correct at the moment this text is generated, for whichever
|
||||
# install location is live then.
|
||||
SELFDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
|
||||
|
||||
REPO=""
|
||||
|
|
@ -73,31 +62,6 @@ age_of() {
|
|||
if [ -n "$ao_m" ]; then echo $(( (NOW - ao_m) / 86400 )); else echo "?"; fi
|
||||
}
|
||||
|
||||
# Delivery age in whole days, read from the FILENAME's timestamp and never from
|
||||
# the mtime. ORDRE 20260903T185736Z-1290610855: `--return` rewrites the order
|
||||
# file's mtime, so an order returned three times reported as brand new - the
|
||||
# reading that exists to say "this has sat here a long time" was reset by the
|
||||
# act of putting it back. The filename is written once, at delivery, and nothing
|
||||
# rewrites it, which is exactly the fact a PENDING age is asking about.
|
||||
#
|
||||
# This is NOT the claimed case. A claim's age is "how long has it been in
|
||||
# flight", which is the claim marker's mtime - a different question with a
|
||||
# different right answer, so age_of stays and stays used there.
|
||||
#
|
||||
# A name the grammar does not produce has no readable delivery time and yields
|
||||
# "?" - the same fail-safe age_of already used for an unreadable mtime, never a
|
||||
# fabricated 0, which would make an unmeasured order look new.
|
||||
pending_age_of() {
|
||||
pao_ts="$(basename "$1")"; pao_ts="${pao_ts%%-*}"
|
||||
case "$pao_ts" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z) ;;
|
||||
*) echo "?"; return 0 ;;
|
||||
esac
|
||||
pao_n="$(printf '%s' "$pao_ts" | tr -dc '0-9')"
|
||||
pao_e="$(date -u -j -f %Y%m%d%H%M%S "$pao_n" +%s 2>/dev/null)"
|
||||
if [ -n "$pao_e" ]; then echo $(( (NOW - pao_e) / 86400 )); else echo "?"; fi
|
||||
}
|
||||
|
||||
field_of() {
|
||||
# Bounded to the frontmatter block: a body line must never be able to forge a
|
||||
# header field the reader is told to trust.
|
||||
|
|
@ -117,12 +81,12 @@ for f in "$ORDERS"/*.md; do
|
|||
# next session the same dead premise with no warning that it is dead.
|
||||
ret="$(grep -m1 '^<!-- order-returned:' "$f" 2>/dev/null | sed -e 's/^<!-- order-returned:[[:space:]]*//' -e 's/[[:space:]]*-->$//')"
|
||||
OUT="${OUT}
|
||||
--- order: ${id} (from ${from}, pending, $(pending_age_of "$f")d old) ---
|
||||
--- order: ${id} (from ${from}, pending, $(age_of "$f")d old) ---
|
||||
subject: ${subj}"
|
||||
[ -n "$ret" ] && OUT="${OUT}
|
||||
returned earlier: ${ret}"
|
||||
OUT="${OUT}
|
||||
-> claim: bash $SELFDIR/coord-order-claim.sh ${id} | leave it: say to the operator why
|
||||
-> claim: coord-order-claim ${id} | leave it: say to the operator why
|
||||
"
|
||||
PENDING=$((PENDING + 1))
|
||||
done
|
||||
|
|
@ -141,7 +105,7 @@ if [ -d "$CLAIMED" ]; then
|
|||
OUT="${OUT}
|
||||
--- order: ${id} (from ${from}, CLAIMED ${cage}d ago) ---
|
||||
subject: ${subj}
|
||||
-> in flight. If no session is working it, put it back: bash $SELFDIR/coord-order-done.sh ${id} --return --reason \"<why>\"
|
||||
-> in flight. If no session is working it, put it back: coord-order-done ${id} --return --reason \"<why>\"
|
||||
"
|
||||
CLAIMED_N=$((CLAIMED_N + 1))
|
||||
done
|
||||
|
|
|
|||
|
|
@ -76,19 +76,6 @@ SUBJECT="$(sanitize_field "$SUBJECT")"
|
|||
case "$TO" in
|
||||
*/*|.|..|_*) echo "coord-order-send: invalid target repo name: $TO" >&2; exit 2 ;;
|
||||
esac
|
||||
# Refused, never sanitized - the same rule and the same reason as
|
||||
# coord-send.sh: --to is also the queue DIRECTORY name ("$COORD/$TO/orders"),
|
||||
# so collapsing a control character to a space would file the order under a
|
||||
# name the sender never wrote. Here that is worse than a misdelivered notice:
|
||||
# an order in a queue no session can hold is the silent evaporation the
|
||||
# ownership chain exists to prevent, and board.sh's ORDRE column counts the
|
||||
# INTENDED repo's queue, which stays 0 with nothing reporting a failure.
|
||||
case "$TO" in
|
||||
*[[:cntrl:]]*)
|
||||
echo "coord-order-send: --to contains a control character: $(sanitize_field "$TO") (a target name is also the queue directory name, so it is refused, never sanitized)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
# Retired address, same rule and same reason as coord-send.sh: a polyrepo
|
||||
# DIRECTORY is not a git repo, so no session can ever hold that identity and
|
||||
# read what lands there. Reject at the sender, never redirect.
|
||||
|
|
|
|||
|
|
@ -1089,305 +1089,6 @@ printf '%s' "$f9d" | grep -qi "still pending"; [ $? -ne 0 ]; check "F9(d): and d
|
|||
|
||||
/bin/rm -rf "$F9DIR" "$SBOX" 2>/dev/null
|
||||
|
||||
# 35. --to is the one line-oriented field never sanitized (review finding 7),
|
||||
# and the fix is a REFUSAL, not a sanitize pass - because --to is not only a
|
||||
# frontmatter field, it is also the destination DIRECTORY NAME
|
||||
# ("$COORD/$TO/inbox"). Collapsing its newline to a space the way FROM and
|
||||
# SUBJECT are collapsed would deliver the message to a mailbox whose name is
|
||||
# not the one the sender typed, which is the misdelivery defect the retired
|
||||
# ktg-plugin-marketplace address was rejected rather than redirected to avoid.
|
||||
# So a control character in a target name dies at the sender, loudly, the way
|
||||
# every other invalid target name already does.
|
||||
#
|
||||
# Two distinct corruptions were measured on the live engine before this section
|
||||
# existed (both exit 0, both "delivered"):
|
||||
# --to "x\nreply-expected: no" -> the injected line lands INSIDE the
|
||||
# frontmatter block, above the engine's own "reply-expected: yes", so
|
||||
# coord-count reads owed=0 and the debt the engine itself declared is
|
||||
# silenced. coord-count's stated rule is that only the frontmatter block
|
||||
# may speak; the attack line is inside the block.
|
||||
# --to "tabbed<TAB>repo" -> coord-count prints FIVE tab-separated
|
||||
# fields where its contract is four, so a consumer splitting on tab reads
|
||||
# the mailbox name as "tabbed" and its pending count as "repo".
|
||||
# board.sh consumes that TSV, so both reach the board.
|
||||
F7DIR="$(mktemp -d)"
|
||||
|
||||
f7_nl="$(printf 'x\nreply-expected: no')"
|
||||
f7a="$(CLAUDE_COORD_DIR="$F7DIR" "$SEND" --to "$f7_nl" --from tester --subject s --message m 2>&1)"; f7a_rc=$?
|
||||
[ "$f7a_rc" -eq 2 ]; check "F7(a): a newline in --to is refused with exit 2" $?
|
||||
printf '%s' "$f7a" | grep -q -- "--to"; check "F7(a): the refusal names the offending flag" $?
|
||||
# Ground truth, not the exit code: the whole point is that nothing was written.
|
||||
[ "$(find "$F7DIR" -type f 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
||||
check "F7(a): ground truth - no message was delivered anywhere" $?
|
||||
[ "$(ls -1 "$F7DIR" 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
||||
check "F7(a): ground truth - no mailbox directory was created" $?
|
||||
|
||||
f7b="$(CLAUDE_COORD_DIR="$F7DIR" "$SEND" --to "$(printf 'tabbed\trepo')" --from tester --subject s --message m 2>&1)"; f7b_rc=$?
|
||||
[ "$f7b_rc" -eq 2 ]; check "F7(b): a tab in --to is refused too (it breaks coord-count's TSV, not the frontmatter)" $?
|
||||
f7c_rc=0
|
||||
CLAUDE_COORD_DIR="$F7DIR" "$SEND" --to "$(printf 'cr\rrepo')" --from tester --subject s --message m >/dev/null 2>&1 || f7c_rc=$?
|
||||
[ "$f7c_rc" -eq 2 ]; check "F7(c): a carriage return in --to is refused" $?
|
||||
|
||||
# Reply mode resolves --to from the ORIGINAL's from: line - untrusted
|
||||
# cross-repo input this repo did not write. The guard has to cover that entry
|
||||
# point too, or the one target name nobody typed is the one that gets through.
|
||||
mkdir -p "$F7DIR/f7repo/inbox"
|
||||
f7orig="20260101T000000Z-0000000000-from-evil.md"
|
||||
{ printf -- '---\n'; printf 'from: ev%bil\n' '\t'; printf 'to: f7repo\n'; printf 'subject: s\n'; printf 'date: 2026-01-01T00:00:00Z\n'; printf -- '---\n'; printf 'body\n'; } > "$F7DIR/f7repo/inbox/$f7orig"
|
||||
f7d_rc=0
|
||||
CLAUDE_COORD_DIR="$F7DIR" "$SEND" --from f7repo --reply-to "$f7orig" --message reply >/dev/null 2>&1 || f7d_rc=$?
|
||||
[ "$f7d_rc" -eq 2 ]; check "F7(d): a control character in a reply-derived target is refused as well" $?
|
||||
# The predicate is "no mailbox was created at all", not "no mailbox named ev":
|
||||
# pre-fix the delivery lands in a directory whose name IS the control sequence,
|
||||
# so a check for the truncated name passes against the defect and proves
|
||||
# nothing. f7repo (created above as the reply source) is the only entry allowed.
|
||||
[ "$(ls -1 "$F7DIR" 2>/dev/null | wc -l | tr -d ' ')" = "1" ]
|
||||
check "F7(d): ground truth - the reply created no new mailbox" $?
|
||||
|
||||
# Known-positive controls. A guard that refuses everything proves nothing, and
|
||||
# the dot-prefixed name matters specifically: coord-send's existing comment
|
||||
# says a dot name is a REAL repo (basename of a git toplevel under a hidden
|
||||
# directory), so the new refusal must not widen into that class.
|
||||
f7e_rc=0
|
||||
CLAUDE_COORD_DIR="$F7DIR" "$SEND" --to f7plain --from tester --subject s --message m >/dev/null 2>&1 || f7e_rc=$?
|
||||
[ "$f7e_rc" -eq 0 ] && [ "$(ls -1 "$F7DIR/f7plain/inbox" 2>/dev/null | wc -l | tr -d ' ')" = "1" ]
|
||||
check "F7(e): control - an ordinary target name still delivers" $?
|
||||
f7f_rc=0
|
||||
CLAUDE_COORD_DIR="$F7DIR" "$SEND" --to .profile --from tester --subject s --message m >/dev/null 2>&1 || f7f_rc=$?
|
||||
[ "$f7f_rc" -eq 0 ] && [ "$(ls -1 "$F7DIR/.profile/inbox" 2>/dev/null | wc -l | tr -d ' ')" = "1" ]
|
||||
check "F7(f): control - a dot-prefixed target name still delivers" $?
|
||||
|
||||
/bin/rm -rf "$F7DIR" 2>/dev/null
|
||||
|
||||
# 36. F5 - a missing mailbox ROOT and an empty one are two different facts, and
|
||||
# coord-count.sh used to report them identically: `[ -d "$COORD" ] || exit 0`,
|
||||
# zero lines on stdout, exit 0, nothing on stderr. A consumer reading that TSV
|
||||
# (board.sh does) cannot tell "no mailbox has pending mail" from "the root I
|
||||
# was pointed at is not there" - Verifiseringsloven ansikt 4 exactly: a broken
|
||||
# query returning a positive-looking null, consumed as a fact about the world.
|
||||
# The two are now distinguishable by EXIT STATUS and a stderr line. Status 3 is
|
||||
# new and deliberately not 2: 2 already means "you called me wrong" (a usage
|
||||
# error, nothing counted), 3 means "the world you named is not there". Both
|
||||
# print nothing on stdout, so no consumer can mistake either for a count.
|
||||
#
|
||||
# The three cases below are the whole point - they must not collapse into one.
|
||||
F5DIR="$(mktemp -d)"
|
||||
|
||||
# (a) The known-positive control FIRST: an EXISTING root holding real mail must
|
||||
# still count it. A guard that exits 3 on everything would pass every negative
|
||||
# check below while having destroyed the script, and this is what proves the
|
||||
# query can still find.
|
||||
CLAUDE_COORD_DIR="$F5DIR" "$SEND" --to f5box --from tester --subject s --message m >/dev/null 2>&1
|
||||
f5a_err="$F5DIR/../f5a.err"
|
||||
f5a_out="$(CLAUDE_COORD_DIR="$F5DIR" "$COUNT" 2>"$f5a_err")"; f5a_rc=$?
|
||||
[ "$f5a_rc" -eq 0 ] && printf '%s' "$f5a_out" | grep -q '^f5box 1 1 '
|
||||
check "F5(a): control - an existing root with mail still counts it and exits 0" $?
|
||||
|
||||
# (b) An EXISTING but EMPTY root: the genuine "nobody has pending mail" answer.
|
||||
# Exit 0, no stdout, and NOTHING on stderr - a warning here would make the
|
||||
# ordinary case noisy and train every reader to ignore the channel that case
|
||||
# (c) needs.
|
||||
F5EMPTY="$(mktemp -d)"
|
||||
f5b_err="$F5DIR/../f5b.err"
|
||||
f5b_out="$(CLAUDE_COORD_DIR="$F5EMPTY" "$COUNT" 2>"$f5b_err")"; f5b_rc=$?
|
||||
[ "$f5b_rc" -eq 0 ] && [ -z "$f5b_out" ] && [ ! -s "$f5b_err" ]
|
||||
check "F5(b): an existing but empty root is a silent, clean zero (exit 0)" $?
|
||||
|
||||
# (c) The defect: a root that does not exist. Pre-fix this was byte-identical
|
||||
# to (b) on every channel a consumer can read.
|
||||
f5c_err="$F5DIR/../f5c.err"
|
||||
f5c_out="$(CLAUDE_COORD_DIR="$F5DIR/no/such/root" "$COUNT" 2>"$f5c_err")"; f5c_rc=$?
|
||||
[ "$f5c_rc" -eq 3 ]
|
||||
check "F5(c): a missing mailbox root exits 3, not 0" $?
|
||||
[ -z "$f5c_out" ]
|
||||
check "F5(c): a missing root still prints NO count line (3 is not a count)" $?
|
||||
[ -s "$f5c_err" ] && grep -q 'coord-count' "$f5c_err" && grep -q 'not counted' "$f5c_err"
|
||||
check "F5(c): a missing root says so on stderr, naming what was not measured" $?
|
||||
|
||||
# (d) Ground truth that (b) and (c) really are distinguishable now. This is the
|
||||
# defect stated as one predicate rather than as three separate assertions: it
|
||||
# fails if any future change collapses the two readings again, including one
|
||||
# that keeps both exit codes but drops the stderr line.
|
||||
[ "$f5b_rc" -ne "$f5c_rc" ]
|
||||
check "F5(d): empty root and missing root no longer report the same status" $?
|
||||
|
||||
/bin/rm -rf "$F5EMPTY" "$F5DIR/../f5a.err" "$F5DIR/../f5b.err" "$F5DIR/../f5c.err" 2>/dev/null
|
||||
|
||||
# 37. F14 - the header's exit contract said "always 0" while the script exited
|
||||
# 2 on `--exclude` with no value, and now exits 3 on a missing root (36 above).
|
||||
# The header IS the contract: it is what `-h` prints, so a consumer that reads
|
||||
# it and trusts it is reading a false claim. Pinned as a check on the HELP TEXT
|
||||
# for the same reason board-selftest pins the FLY legend wording - the text is
|
||||
# engine behavior, not prose, and a doc line nothing tests is a doc line that
|
||||
# drifts.
|
||||
f14_help="$(CLAUDE_COORD_DIR="$F5DIR" "$COUNT" --help 2>/dev/null)"; f14_rc=$?
|
||||
[ "$f14_rc" -eq 0 ] && [ -n "$f14_help" ] && printf '%s' "$f14_help" | grep -q '^Exit:'
|
||||
check "F14: control - --help still exits 0 and prints an Exit: contract" $?
|
||||
|
||||
# Ground truth for the claim the old header contradicted.
|
||||
f14b_rc=0
|
||||
CLAUDE_COORD_DIR="$F5DIR" "$COUNT" --exclude >/dev/null 2>&1 || f14b_rc=$?
|
||||
[ "$f14b_rc" -eq 2 ]
|
||||
check "F14: ground truth - --exclude with no value really does exit 2" $?
|
||||
|
||||
printf '%s' "$f14_help" | grep -q 'always 0'
|
||||
[ $? -ne 0 ]
|
||||
check "F14: the header no longer claims the script always exits 0" $?
|
||||
|
||||
printf '%s' "$f14_help" | grep -q '2 *= *usage error'
|
||||
check "F14: the header documents exit 2 (usage error, nothing counted)" $?
|
||||
|
||||
printf '%s' "$f14_help" | grep -q '3 *= *mailbox root'
|
||||
check "F14: the header documents exit 3 (missing root, nothing counted)" $?
|
||||
|
||||
# The reason the "always 0" claim existed at all must survive its removal: the
|
||||
# session-start path must still not be failable by mailbox STATE. Every exit
|
||||
# above 0 is a caller/world error, never "this mailbox has awkward contents".
|
||||
f14c_rc=0
|
||||
CLAUDE_COORD_DIR="$F5DIR" "$COUNT" --exclude f5box >/dev/null 2>&1 || f14c_rc=$?
|
||||
[ "$f14c_rc" -eq 0 ]
|
||||
check "F14: control - a correct call over a real root still exits 0" $?
|
||||
|
||||
/bin/rm -rf "$F5DIR" 2>/dev/null
|
||||
|
||||
# 38. The launchd templates. A wrong program path in a plist is the one defect
|
||||
# in this repo that NOTHING catches at runtime: the agent simply never runs, in
|
||||
# silence, and `launchctl list` confirms only that it is LOADED, never that it
|
||||
# does anything right. There is no output to be wrong, no exit status to read -
|
||||
# the failure looks exactly like a quiet machine. So the path is asserted here,
|
||||
# statically, against the file it actually names.
|
||||
#
|
||||
# This section covers EVERY plist in launchd/, not only the sweep agent that
|
||||
# 0.33.0 adds, and that is deliberate: the plist grammar has one reader here
|
||||
# rather than one per agent. Two half-checks in two suites would drift, which is
|
||||
# the two-copies-of-one-policy defect this repo names repeatedly. board-selftest
|
||||
# still owns brief-nightly.sh's BEHAVIOUR (section 9); this owns the templates.
|
||||
#
|
||||
# Deliberately NOT checked here: XML well-formedness. `plutil` is not coreutils,
|
||||
# and malformed XML is the one plist defect that already fails LOUDLY - launchctl
|
||||
# load rejects it on the spot. This section is for the defect that does not: a
|
||||
# path that is merely wrong. Both files were linted by hand at 0.33.0.
|
||||
LAUNCHD="$DIR/../launchd"
|
||||
REPOROOT="$(cd "$DIR/.." && pwd)"
|
||||
|
||||
# One reader, shared by the real files below AND by the control at the end. A
|
||||
# control that runs different code from the case it certifies proves nothing
|
||||
# about it. The program path is the <string> carrying the checkout placeholder;
|
||||
# the install instructions in the header comment name __CHECKOUT__ too, which is
|
||||
# why <string> has to match first.
|
||||
plist_program_path() {
|
||||
grep '<string>' "$1" 2>/dev/null | grep '__CHECKOUT__' | head -1 \
|
||||
| sed -e 's/.*<string>//' -e 's|</string>.*||'
|
||||
}
|
||||
plist_label() {
|
||||
grep -A1 '<key>Label</key>' "$1" 2>/dev/null | grep '<string>' | head -1 \
|
||||
| sed -e 's/.*<string>//' -e 's|</string>.*||'
|
||||
}
|
||||
plist_hour() {
|
||||
grep -A1 '<key>Hour</key>' "$1" 2>/dev/null | grep '<integer>' | head -1 \
|
||||
| sed -e 's/.*<integer>//' -e 's|</integer>.*||'
|
||||
}
|
||||
|
||||
plist_n=0
|
||||
for p in "$LAUNCHD"/*.plist; do
|
||||
[ -e "$p" ] || continue
|
||||
plist_n=$((plist_n + 1))
|
||||
pb="$(basename "$p")"
|
||||
|
||||
# launchctl addresses an agent by Label, the operator by filename. When they
|
||||
# disagree, load/start/unload silently act on a different agent than the one
|
||||
# being edited.
|
||||
lbl="$(plist_label "$p")"
|
||||
[ -n "$lbl" ] && [ "$lbl" = "${pb%.plist}" ]
|
||||
check "launchd $pb: Label matches the filename" $?
|
||||
|
||||
# The check this section exists for.
|
||||
prog="$(plist_program_path "$p")"
|
||||
[ -n "$prog" ] && [ -s "$REPOROOT/${prog#__CHECKOUT__/}" ]
|
||||
check "launchd $pb: ProgramArguments names a script that exists here" $?
|
||||
|
||||
# The repo is mirrored publicly and a plist is the one file that would
|
||||
# otherwise carry an absolute home path. It stays a TEMPLATE.
|
||||
grep -q '__HOME__' "$p"
|
||||
check "launchd $pb: log paths stay a __HOME__ placeholder (public mirror)" $?
|
||||
|
||||
# The cache path is version-pinned, so an agent pointing there breaks silently
|
||||
# on the next bump - and a second copy of these scripts on disk is the exact
|
||||
# defect class that produced the 0.12.1 stale-fallback bug. Asserted on the
|
||||
# EXTRACTED PATH, never on the whole file: the brief plist's header explains in
|
||||
# prose why it does not point at the cache, and a file-wide grep read that
|
||||
# explanation as the defect it warns about. Same shape as the board line, where
|
||||
# prose saying status=done must never trigger the done-guard.
|
||||
case "$prog" in *plugins/cache*) false ;; *) true ;; esac
|
||||
check "launchd $pb: the program path is not the version-pinned plugin cache" $?
|
||||
done
|
||||
|
||||
[ "$plist_n" -ge 2 ]
|
||||
check "launchd: both agent templates are present (brief + sweep)" $?
|
||||
|
||||
# The grace window is the OPERATOR's policy constant (14 days, decided
|
||||
# 2026-09-03), not the script's default wearing a schedule. An agent quietly
|
||||
# running a different window would close a different population every night with
|
||||
# nothing reporting the change.
|
||||
SWEEPPL="$LAUNCHD/com.ktg.repo-mailbox-sweep.plist"
|
||||
grep -q '<string>--write</string>' "$SWEEPPL" 2>/dev/null \
|
||||
&& grep -q '<string>--days</string>' "$SWEEPPL" 2>/dev/null \
|
||||
&& grep -q '<string>14</string>' "$SWEEPPL" 2>/dev/null
|
||||
check "launchd sweep: the agent runs --write --days 14, the authorized window" $?
|
||||
|
||||
# The briefing READS the mailbox the sweep MUTATES, so the two must not fire in
|
||||
# the same minute: a briefing rendered mid-sweep counts messages that are being
|
||||
# closed underneath it.
|
||||
hb="$(plist_hour "$LAUNCHD/com.ktg.repo-mailbox-brief.plist")"
|
||||
hs="$(plist_hour "$SWEEPPL")"
|
||||
[ -n "$hb" ] && [ -n "$hs" ] && [ "$hb" != "$hs" ]
|
||||
check "launchd: the two agents run at different hours (the brief reads what the sweep mutates)" $?
|
||||
|
||||
# Mandatory controls. A path check with no negative case is a check that cannot
|
||||
# go red, which this repo has shipped once already (section 11's vacuous first
|
||||
# cut) and will not ship again.
|
||||
BADPL="$CLAUDE_COORD_DIR/bad.plist"
|
||||
{
|
||||
echo '<plist version="1.0"><dict>'
|
||||
echo '<key>Label</key>'
|
||||
echo '<string>com.ktg.repo-mailbox-bad</string>'
|
||||
echo '<key>ProgramArguments</key>'
|
||||
echo '<array>'
|
||||
echo '<string>/bin/bash</string>'
|
||||
echo '<string>__CHECKOUT__/scripts/no-such-script.sh</string>'
|
||||
echo '</array>'
|
||||
echo '</dict></plist>'
|
||||
} > "$BADPL"
|
||||
|
||||
[ "$(plist_program_path "$BADPL")" = "__CHECKOUT__/scripts/no-such-script.sh" ]
|
||||
check "launchd control: the extraction really does read a program path" $?
|
||||
|
||||
badprog="$(plist_program_path "$BADPL")"
|
||||
[ -s "$REPOROOT/${badprog#__CHECKOUT__/}" ]; [ $? -ne 0 ]
|
||||
check "launchd control: a plist naming a missing script is judged missing" $?
|
||||
|
||||
[ "$(plist_label "$BADPL")" = "bad" ]; [ $? -ne 0 ]
|
||||
check "launchd control: a Label disagreeing with the filename is caught" $?
|
||||
|
||||
# The cache check needs its own control, because narrowing it from the whole file
|
||||
# to the extracted path is exactly the kind of narrowing that can quietly stop
|
||||
# catching anything.
|
||||
CACHEPL="$CLAUDE_COORD_DIR/cache.plist"
|
||||
{
|
||||
echo '<plist version="1.0"><dict>'
|
||||
echo '<key>ProgramArguments</key>'
|
||||
echo '<array>'
|
||||
echo '<string>/bin/bash</string>'
|
||||
echo '<string>__CHECKOUT__/.claude/plugins/cache/repo-mailbox/0.33.0/scripts/coord-sweep.sh</string>'
|
||||
echo '</array>'
|
||||
echo '</dict></plist>'
|
||||
} > "$CACHEPL"
|
||||
cprog="$(plist_program_path "$CACHEPL")"
|
||||
case "$cprog" in *plugins/cache*) true ;; *) false ;; esac
|
||||
check "launchd control: a program path INSIDE the plugin cache is caught" $?
|
||||
|
||||
/bin/rm -f "$BADPL" "$CACHEPL" 2>/dev/null
|
||||
|
||||
echo "----"
|
||||
echo "PASS=$PASS FAIL=$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -158,26 +158,6 @@ if [ "$BROADCAST" -eq 0 ]; then
|
|||
case "$TO" in
|
||||
*/*|.|..|_*) echo "coord-send: invalid target repo name: $TO" >&2; exit 2 ;;
|
||||
esac
|
||||
# --to is the one line-oriented field that is REFUSED rather than sanitized,
|
||||
# and the asymmetry with FROM/SUBJECT above is deliberate: --to is also the
|
||||
# destination DIRECTORY name ("$COORD/$TO/inbox"). Collapsing a newline to a
|
||||
# space would deliver the message to a mailbox the sender never named, which
|
||||
# is the same misdelivery the retired ktg-plugin-marketplace address is
|
||||
# rejected rather than redirected to avoid. Measured before this guard
|
||||
# existed, both with exit 0 and a "delivered" line: a newline injects its
|
||||
# payload INSIDE the frontmatter block (silencing the reply-expected: yes the
|
||||
# engine itself wrote, since coord-count reads the first match), and a tab
|
||||
# gives coord-count five tab-separated fields where its contract is four, so
|
||||
# a consumer reads the mailbox name and the pending count off by one column.
|
||||
# board.sh consumes that TSV. Reply mode resolves TO from the original's
|
||||
# from: line - untrusted cross-repo input - so this must sit AFTER that
|
||||
# resolution, covering the one target name nobody typed.
|
||||
case "$TO" in
|
||||
*[[:cntrl:]]*)
|
||||
echo "coord-send: --to contains a control character: $(sanitize_field "$TO") (a target name is also the mailbox directory name, so it is refused, never sanitized)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
# Retired address (operator decision 2026-08-15, catalog's H4 reply
|
||||
# archived 2026-08-15T16:27:51Z): ktg-plugin-marketplace is a polyrepo
|
||||
# DIRECTORY, not a git repo, so basename(git toplevel) can never resolve to
|
||||
|
|
|
|||
|
|
@ -66,13 +66,7 @@ r2="$("$READ" --repo fake-repo)"; rc=$?
|
|||
printf '%s' "$r2" | grep -q "2 pending"; check "read reports the pending count" $?
|
||||
printf '%s' "$r2" | grep -q "order one"; check "read shows the subject" $?
|
||||
printf '%s' "$r2" | grep -q "dispatcher"; check "read shows the sender" $?
|
||||
# ORDRE 65 follow-on (2026-08-17): this is the SessionStart injection - the
|
||||
# same bare-name defect the order named in board.sh's dispatch starter also
|
||||
# lived here, and arguably worse: it fires on every session with a pending
|
||||
# order, not only a dispatched one (this is the literal text this session saw
|
||||
# at its own start). Pinned by absolute path, exactly like board.sh's fix.
|
||||
printf '%s' "$r2" | grep -qF "bash $DIR/coord-order-claim.sh $oid1"; check "read gives a per-order claim hint by an ABSOLUTE script path, not a bare PATH name" $?
|
||||
printf '%s' "$r2" | grep -Eq "(^|[^./])coord-order-claim $oid1"; [ $? -ne 0 ]; check "read: no bare, un-pathed coord-order-claim invocation survives the injection" $?
|
||||
printf '%s' "$r2" | grep -q "coord-order-claim $oid1"; check "read gives a per-order claim hint" $?
|
||||
# The order body is deliberately NOT injected: an order can be a full session
|
||||
# prompt, and the queue view has to stay readable at session start. The text
|
||||
# arrives at claim time, from the one place it lives.
|
||||
|
|
@ -100,12 +94,6 @@ c3="$("$CLAIM" --repo fake-repo "$oid1" 2>&1)"; rc=$?
|
|||
[ "$rc" -eq 0 ]; check "claim exits 0" $?
|
||||
printf '%s' "$c3" | grep -q "do the thing"; check "claim prints the full order body" $?
|
||||
printf '%s' "$c3" | grep -q "NESTE"; check "claim instructs the D-check against STATE's NESTE" $?
|
||||
# ORDRE 65 follow-on: the claim's own "WHEN DONE"/"IF YOU CANNOT" lines are the
|
||||
# THIRD live emitter of the same bare-name defect - the text handed directly
|
||||
# to the claiming session as its own next-step instruction.
|
||||
printf '%s' "$c3" | grep -qF "WHEN DONE: bash $DIR/coord-order-done.sh $oid1 --commit"; check "claim's WHEN DONE line calls coord-order-done.sh by an ABSOLUTE script path" $?
|
||||
printf '%s' "$c3" | grep -qF "IF YOU CANNOT: bash $DIR/coord-order-done.sh $oid1 --return"; check "claim's IF YOU CANNOT line calls coord-order-done.sh by an ABSOLUTE script path" $?
|
||||
printf '%s' "$c3" | grep -Eq "(^|[^./])coord-order-done $oid1"; [ $? -ne 0 ]; check "claim output: no bare, un-pathed coord-order-done invocation" $?
|
||||
[ ! -e "$of1" ]; check "claimed order leaves the pending queue" $?
|
||||
[ -f "$CLAUDE_COORD_DIR/fake-repo/orders/claimed/$oid1.md" ]; check "claimed order lands in orders/claimed" $?
|
||||
|
||||
|
|
@ -121,8 +109,7 @@ check "re-claiming an already claimed order exits 1" $?
|
|||
r3="$("$READ" --repo fake-repo)"
|
||||
printf '%s' "$r3" | grep -q "1 claimed"; check "read reports the claimed count" $?
|
||||
printf '%s' "$r3" | grep -q "CLAIMED"; check "read shows a claimed order as in flight" $?
|
||||
printf '%s' "$r3" | grep -qF "bash $DIR/coord-order-done.sh $oid1 --return"; check "read gives the return hint for a claimed order by an ABSOLUTE script path" $?
|
||||
printf '%s' "$r3" | grep -Eq "(^|[^./])coord-order-done $oid1 --return"; [ $? -ne 0 ]; check "read: no bare, un-pathed coord-order-done invocation survives the in-flight hint" $?
|
||||
printf '%s' "$r3" | grep -q "coord-order-done $oid1 --return"; check "read gives the return hint for a claimed order" $?
|
||||
|
||||
# --next takes the oldest pending order, so a session never has to parse the
|
||||
# queue to obey it.
|
||||
|
|
@ -336,7 +323,7 @@ printf '%s' "$bout" | grep -q 'ordrepo'; check "board table lists the fixture re
|
|||
# Matched on the rendered row rather than by awk field number: KOST is
|
||||
# "Sonnet 5/high", which contains a space, so a field index would be counting
|
||||
# the wrong columns and would keep "passing" if the layout shifted.
|
||||
printf '%s' "$bout" | grep -qE '^ordrepo[[:space:]]+planned[[:space:]]+Sonnet 5/high[[:space:]]+1[[:space:]]+2:0d[[:space:]]'
|
||||
printf '%s' "$bout" | grep -qE '^ordrepo[[:space:]]+planned[[:space:]]+Sonnet 5/high[[:space:]]+1[[:space:]]+2[[:space:]]'
|
||||
check "board prints INN 1 and ORDRE 2 side by side, never summed" $?
|
||||
|
||||
# --dispatch --order-id: the thin starter form. The order text lives in the
|
||||
|
|
@ -377,100 +364,6 @@ else
|
|||
skip "morning not installed - plan-file starter render NOT measured"
|
||||
fi
|
||||
|
||||
# --- 10. --to is refused, not sanitized (the coord-send finding 7 class) ----
|
||||
# Same defect, same fix, measured separately here because this channel is the
|
||||
# one where it costs the most: an order delivered to a name no session can
|
||||
# hold is the silent evaporation the queue's ownership chain exists to
|
||||
# prevent, and board.sh's ORDRE column counts "$COORD/<name>/orders/*.md", so
|
||||
# the count for the repo that was meant to get the work stays 0 with nothing
|
||||
# anywhere reporting a failure. Measured before the guard: exit 0, an
|
||||
# "order delivered" line, and a queue directory whose name carries the newline.
|
||||
O10DIR="$(mktemp -d)"
|
||||
o10a_rc=0
|
||||
CLAUDE_COORD_DIR="$O10DIR" "$SEND" --to "$(printf 'q\nreply-expected: no')" --from tester --subject s --message m >/dev/null 2>&1 || o10a_rc=$?
|
||||
[ "$o10a_rc" -eq 2 ]; check "10a: a newline in --to is refused with exit 2" $?
|
||||
[ "$(find "$O10DIR" -type f 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
||||
check "10a: ground truth - no order was written anywhere" $?
|
||||
[ "$(ls -1 "$O10DIR" 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
||||
check "10a: ground truth - no queue directory was created" $?
|
||||
o10b_rc=0
|
||||
CLAUDE_COORD_DIR="$O10DIR" "$SEND" --to "$(printf 'tab\tq')" --from tester --subject s --message m >/dev/null 2>&1 || o10b_rc=$?
|
||||
[ "$o10b_rc" -eq 2 ]; check "10b: a tab in --to is refused too" $?
|
||||
# Known-positive controls: the guard must not refuse the ordinary case, nor
|
||||
# the dot-prefixed name that coord-send's own comment protects as a real repo.
|
||||
o10c_rc=0
|
||||
CLAUDE_COORD_DIR="$O10DIR" "$SEND" --to o10plain --from tester --subject s --message m >/dev/null 2>&1 || o10c_rc=$?
|
||||
[ "$o10c_rc" -eq 0 ] && [ "$(ls -1 "$O10DIR/o10plain/orders" 2>/dev/null | grep -c '\.md$' | tr -d ' ')" = "1" ]
|
||||
check "10c: control - an ordinary target name still receives its order" $?
|
||||
o10d_rc=0
|
||||
CLAUDE_COORD_DIR="$O10DIR" "$SEND" --to .profile --from tester --subject s --message m >/dev/null 2>&1 || o10d_rc=$?
|
||||
[ "$o10d_rc" -eq 0 ] && [ "$(ls -1 "$O10DIR/.profile/orders" 2>/dev/null | grep -c '\.md$' | tr -d ' ')" = "1" ]
|
||||
check "10d: control - a dot-prefixed target name still receives its order" $?
|
||||
/bin/rm -rf "$O10DIR" 2>/dev/null
|
||||
|
||||
# --- 11. Pending age comes from the FILENAME, never the mtime ---------------
|
||||
# ORDRE 20260903T185736Z-1290610855: `--return` rewrites the order file's
|
||||
# mtime, and both age surfaces read mtime, so an order returned three times can
|
||||
# never read as old. Measured on the live queue before the order was written: a
|
||||
# file whose name says 2026-09-02 reported "0d old" minutes after a return.
|
||||
#
|
||||
# PM decision, and it is two questions with two answers: a PENDING order's age
|
||||
# is "how long has this sat with no owner" = now - the DELIVERY time, which only
|
||||
# the filename carries; a CLAIMED order's age is "how long has it been in
|
||||
# flight" = the claim marker's mtime, which is already right and stays.
|
||||
O11DIR="$WORK/o11"; mkdir -p "$O11DIR"
|
||||
o11_old_ts="$(date -u -v-2d +%Y%m%dT%H%M%SZ 2>/dev/null)"
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$SEND" --to o11repo --from dispatcher \
|
||||
--subject "aged order" --message "body" >/dev/null 2>&1
|
||||
o11_q="$O11DIR/o11repo/orders"
|
||||
o11_orig="$(ls -1 "$o11_q"/*.md 2>/dev/null | head -1)"
|
||||
o11_id="$(basename "$o11_orig" .md)"
|
||||
o11_aged_id="${o11_old_ts}-${o11_id#*-}"
|
||||
mv "$o11_orig" "$o11_q/$o11_aged_id.md" 2>/dev/null
|
||||
|
||||
# Ground truth FIRST, so a broken `date -v` fails here instead of turning the
|
||||
# whole section into a test of nothing (the F13 section-11 lesson).
|
||||
[ -n "$o11_old_ts" ] && [ -f "$o11_q/$o11_aged_id.md" ]
|
||||
check "11a: ground truth - the fixture order's filename timestamp is 2 days old" $?
|
||||
|
||||
# Drive the REAL defect: claim it, then return it. The return is what rewrites
|
||||
# the mtime, so this is the path that produced the live 0d reading.
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$CLAIM" --repo o11repo "$o11_aged_id" >/dev/null 2>&1
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$ODONE" --repo o11repo "$o11_aged_id" --return --reason "test" >/dev/null 2>&1
|
||||
o11_mtime="$(stat -f %m "$o11_q/$o11_aged_id.md" 2>/dev/null)"
|
||||
o11_now="$(date +%s)"
|
||||
[ -n "$o11_mtime" ] && [ $(( o11_now - o11_mtime )) -lt 300 ]
|
||||
check "11b: ground truth - the return really did rewrite the file's mtime to now" $?
|
||||
|
||||
o11_out="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11repo 2>/dev/null)"
|
||||
printf '%s' "$o11_out" | grep -q 'pending, 2d old'
|
||||
check "11c: a returned order reports its DELIVERY age (2d), not 0d" $?
|
||||
|
||||
# Known-positive control: the reading must still be able to say 0d, or 11c
|
||||
# would pass just as well against a function that always prints 2.
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$SEND" --to o11fresh --from dispatcher \
|
||||
--subject "fresh order" --message "body" >/dev/null 2>&1
|
||||
o11_fresh="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11fresh 2>/dev/null)"
|
||||
printf '%s' "$o11_fresh" | grep -q 'pending, 0d old'
|
||||
check "11d: control - a freshly delivered order still reports 0d" $?
|
||||
|
||||
# FLY is the OTHER question and must not move: the claim marker's mtime is when
|
||||
# the claim happened, and an order with an ancient filename claimed just now has
|
||||
# been in flight for 0 days.
|
||||
CLAUDE_COORD_DIR="$O11DIR" "$CLAIM" --repo o11repo "$o11_aged_id" >/dev/null 2>&1
|
||||
o11_fly="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11repo 2>/dev/null)"
|
||||
printf '%s' "$o11_fly" | grep -q 'CLAIMED 0d ago'
|
||||
check "11e: FLY age still comes from the claim marker's mtime, not the filename" $?
|
||||
|
||||
# A name the grammar does not produce has no readable delivery time. It must
|
||||
# read "?" - the same fail-safe the mtime path already used, never a fabricated
|
||||
# 0, which would make an unmeasured order look brand new.
|
||||
mkdir -p "$O11DIR/o11bad/orders"
|
||||
printf -- '---\nfrom: x\nsubject: s\n---\nbody\n' > "$O11DIR/o11bad/orders/not-a-timestamp.md"
|
||||
o11_bad="$(CLAUDE_COORD_DIR="$O11DIR" "$READ" --repo o11bad 2>/dev/null)"
|
||||
printf '%s' "$o11_bad" | grep -q 'pending, ?d old'
|
||||
check "11f: an unparseable filename timestamp reads ?, never 0" $?
|
||||
|
||||
echo
|
||||
echo "orders-selftest: $PASS passed, $FAIL failed, $SKIP skipped (of $((PASS+FAIL+SKIP)) checks)"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
|
|
@ -141,8 +141,7 @@ 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)"
|
||||
RL_ROOT=""
|
||||
cleanup() { /bin/rm -rf "$ROOT" "$MBOX" ${RL_ROOT:+"$RL_ROOT"} 2>/dev/null; }
|
||||
cleanup() { /bin/rm -rf "$ROOT" "$MBOX" 2>/dev/null; }
|
||||
trap cleanup EXIT
|
||||
|
||||
rt_bad=0
|
||||
|
|
@ -170,16 +169,6 @@ done
|
|||
# rather than produced by "$R".
|
||||
rt_case "rt-5" "Fable 5/high"
|
||||
rt_case "rt-6" "Fable 5/xhigh"
|
||||
# A hand-written Fable 5.1 board line. next-cost extraction is free text, so
|
||||
# board.sh parses the point release back unchanged - pinned here so a later
|
||||
# narrowing of that extraction fails in this suite rather than in the
|
||||
# operator's eye. MEASURED GAP, stated rather than closed: at 15 characters it
|
||||
# overflows the %-14s KOST column and shifts the rest of that row one column
|
||||
# right. That is a board.sh rendering change nobody ordered in this session, so
|
||||
# it is reported to .claude, not fixed here - which is also why "Fable
|
||||
# 5.1/xhigh" is deliberately absent from the widest-value loop below. Adding it
|
||||
# there would go red, and the red would be the unfixed gap, not a broken test.
|
||||
rt_case "rt-51" "Fable 5.1/xhigh"
|
||||
|
||||
OUT="$(CLAUDE_COORD_DIR="$MBOX" "$BOARD" --roots "$ROOT" 2>/dev/null)"
|
||||
for want in "Sonnet 5/high" "Sonnet 5/xhigh" "Opus 5/high" "Opus 5/xhigh" \
|
||||
|
|
@ -187,8 +176,6 @@ for want in "Sonnet 5/high" "Sonnet 5/xhigh" "Opus 5/high" "Opus 5/xhigh" \
|
|||
printf '%s' "$OUT" | grep -q "$want" || { rt_bad=$((rt_bad+1)); echo " board lost: [$want]"; }
|
||||
done
|
||||
[ "$rt_bad" -eq 0 ]; check "round trip: board.sh parses back all 6 emitted values" $?
|
||||
printf '%s' "$OUT" | grep -q 'Fable 5\.1/xhigh'
|
||||
check "board parses back a hand-written Fable 5.1 next-cost" $?
|
||||
|
||||
# board.sh renders KOST with %-14s; a longer value shoves the whole row right
|
||||
# even though it parsed fine. Measure the widest string the table can emit -
|
||||
|
|
@ -285,43 +272,6 @@ check "no route-last line when the record is omitted" "$rc"
|
|||
--last-completed yes --last-corrections 0 >/dev/null 2>&1
|
||||
[ $? -eq 0 ]; check "--last-model/-effort accept every legal value" $?
|
||||
|
||||
# Fable 5.1 shipped 2026-09-01 and the closed set refused it, so a session that
|
||||
# actually ran it could not record what it ran: the record was either omitted
|
||||
# or LIED, and a lied record reads back months later as a measurement. The set
|
||||
# is WIDENED, never replaced by form validation - the check below is what makes
|
||||
# that choice machine-verified instead of prose. "Fable 5" stays legal for a
|
||||
# reason stronger than the one STATE.md on this machine that still carries it:
|
||||
# route.sh's OWN row table spells rows 5-6 "Fable 5/high" and "Fable 5/xhigh",
|
||||
# so dropping it would make the script refuse to record a value its own spec
|
||||
# names. The check above this one is what goes red if anyone drops it.
|
||||
"$R" --path known --verification strong --reversibility cheap --scope local \
|
||||
--rationale x --last-model "Fable 5.1" --last-effort xhigh \
|
||||
--last-completed yes --last-corrections 0 >/dev/null 2>&1
|
||||
[ $? -eq 0 ]; check "--last-model accepts the Fable 5.1 point release" $?
|
||||
|
||||
# The set is still CLOSED after being widened, and this is the whole cost of
|
||||
# NOT switching to form validation. A pattern like "<family> <digits>[.<digits>]"
|
||||
# would accept every line below, and would stop catching a version that does
|
||||
# not exist - which reads back later as evidence that a model ran when it never
|
||||
# shipped. That is the positive-looking null this repo refuses everywhere else.
|
||||
fable_bad=0
|
||||
for bad in "Fable 5.2" "Fable 6" "Fable 5.10"; do
|
||||
"$R" --path known --verification strong --reversibility cheap --scope local \
|
||||
--rationale x --last-model "$bad" --last-effort xhigh \
|
||||
--last-completed yes --last-corrections 0 >/dev/null 2>&1
|
||||
[ $? -eq 2 ] || { fable_bad=$((fable_bad+1)); echo " accepted a model that does not exist: [$bad]"; }
|
||||
done
|
||||
[ "$fable_bad" -eq 0 ]; check "the model set stays CLOSED after Fable 5.1 (no form validation)" $?
|
||||
|
||||
# Accepting the value is not the same as RECORDING it. The record is what the
|
||||
# next session reads back, so the emitted line must carry the point release
|
||||
# verbatim rather than collapsing it to the family name.
|
||||
LAST51="$("$R" --path known --verification strong --reversibility cheap --scope local \
|
||||
--rationale x --last-model "Fable 5.1" --last-effort xhigh \
|
||||
--last-completed yes --last-corrections 0 2>/dev/null | sed -n 's/^route-last=//p')"
|
||||
printf '%s' "$LAST51" | grep -q '^<!-- route-last: model=Fable 5.1; effort=xhigh; completed=yes; corrections=0 -->$'
|
||||
check "route-last carries Fable 5.1 verbatim into the emitted line" $?
|
||||
|
||||
# The record is telemetry and must NOT silently change what the calculator
|
||||
# outputs - a "completed=no" record describes what happened, and covers
|
||||
# context exhaustion, an operator interrupt and a block on another repo just
|
||||
|
|
@ -360,14 +310,13 @@ 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, 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.
|
||||
# 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.
|
||||
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" ]; check "command mirrors the row (sonnet/high)" $?
|
||||
[ "$cmd" = "claude --model sonnet --effort high --advisor opus" ]; 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)"
|
||||
|
|
@ -379,7 +328,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" ]; check "fallback ships its own command" $?
|
||||
[ "$fbc" = "claude --model sonnet --effort xhigh --advisor opus" ]; 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
|
||||
|
|
@ -395,9 +344,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. 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.
|
||||
# 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.
|
||||
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"
|
||||
|
|
@ -469,129 +418,117 @@ 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 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.
|
||||
# --- 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.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
|
||||
adv="$(field known strong cheap local command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 1 (Sonnet/high) carries no advisor" "$rc"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "row 1 (Sonnet/high) always carries an advisor" $?
|
||||
|
||||
adv="$(field known weak cheap local command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "row 2 (Sonnet/xhigh) carries no advisor" "$rc"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "row 2 (Sonnet/xhigh) always carries an advisor" $?
|
||||
|
||||
adv="$(field known strong costly local command)"
|
||||
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"
|
||||
printf '%s' "$adv" | grep -q -- '--advisor opus'
|
||||
check "reversibility=costly carries an advisor onto an Opus row" $?
|
||||
|
||||
adv="$(field known strong one-way local command)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "reversibility=one-way no longer pulls an advisor onto an Opus row" "$rc"
|
||||
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.
|
||||
|
||||
# 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)"
|
||||
if printf '%s' "$adv" | grep -q -- '--advisor'; then rc=1; else rc=0; fi
|
||||
check "the row-4 fallback carries no advisor either" "$rc"
|
||||
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 "an Opus row falling back to a Sonnet row carries no advisor" "$rc"
|
||||
check "row 4 falling back to row 3 at cheap stakes stays unadvised" "$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"
|
||||
# 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}" $?
|
||||
|
||||
# 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"
|
||||
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 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"
|
||||
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"
|
||||
|
||||
# row_advisor() never emits '--advisor fable' - it depends on the opposite
|
||||
# direction: whether opus can advise a FABLE main model. That is the fact
|
||||
# rows 5-6's suppression rests on, so that is what must be gated. (Measured
|
||||
# at CC 2.1.226: fable is now itself accepted as an advisor for opus/sonnet
|
||||
# mains - a capability-ordering change worth flagging to the operator, but
|
||||
# not what this check exists to pin.)
|
||||
FABOUT="$(claude --model fable --advisor opus -p "" 2>&1 | head -3)"
|
||||
printf '%s' "$FABOUT" | grep -q 'cannot advise'
|
||||
check "installed claude still rejects opus advising a fable main" $?
|
||||
else
|
||||
echo " skip - claude not on PATH, advisor flag gate not run"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "route-selftest: $PASS passed, $FAIL failed"
|
||||
|
|
|
|||
110
scripts/route.sh
110
scripts/route.sh
|
|
@ -29,7 +29,10 @@
|
|||
# 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).
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
|
@ -63,14 +66,32 @@
|
|||
# overkill costs quota every session - but a wrong architecture decision in a
|
||||
# published plugin costs more than either.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
|
|
@ -92,7 +113,7 @@
|
|||
# route.sh --path <v> --verification <v> --reversibility <v> --scope <v>
|
||||
# --rationale <text>
|
||||
#
|
||||
# route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5|Fable 5.1>
|
||||
# route.sh ... --last-model <Sonnet 5|Opus 5|Fable 5>
|
||||
# --last-effort <low|medium|high|xhigh|max>
|
||||
# --last-completed <yes|no> --last-corrections <n>
|
||||
#
|
||||
|
|
@ -187,31 +208,9 @@ if [ "$L_SET" -eq 1 ]; then
|
|||
# this record back as evidence months from now, so a drifted spelling
|
||||
# ("opus 5" for "Opus 5") rebuilds the reader-versus-writer drift this whole
|
||||
# script exists to remove, one field over.
|
||||
# THE SET IS CLOSED, AND STAYS CLOSED - decided 2026-09-01 when Fable 5.1
|
||||
# shipped and was refused here. Both boundary questions were live:
|
||||
#
|
||||
# (a) "Fable 5" is KEPT alongside the point release. The reason is not
|
||||
# backward compatibility with the one STATE.md on this machine that still
|
||||
# carries it (measured: 1 of 45 route-last lines) - it is that the row
|
||||
# table above spells rows 5-6 "Fable 5/high" and "Fable 5/xhigh".
|
||||
# Dropping the value would make this script refuse to record a name its
|
||||
# own spec writes.
|
||||
#
|
||||
# (b) The set was WIDENED rather than replaced by form validation. A pattern
|
||||
# like "<family> <digits>[.<digits>]" would still catch a misspelled
|
||||
# family and a drifted case, and would stop catching A VERSION THAT DOES
|
||||
# NOT EXIST: "Fable 5.2" and "Opus 7" would both pass and read back
|
||||
# months later as evidence that a model ran when it never shipped. This
|
||||
# field is telemetry read as evidence, so a silently-accepted lie is
|
||||
# worse than a loud refusal.
|
||||
#
|
||||
# The cost of that choice is real and was paid before it was made: a session
|
||||
# that genuinely ran Fable 5.1 could not record it, so its record was omitted
|
||||
# or lied. The list must therefore be extended the day a model ships, and the
|
||||
# die message says so rather than leaving the caller to guess.
|
||||
case "$L_MODEL" in
|
||||
"Sonnet 5"|"Opus 5"|"Fable 5"|"Fable 5.1") ;;
|
||||
*) die "--last-model: '$L_MODEL' is not a row-table model (Sonnet 5|Opus 5|Fable 5|Fable 5.1) - a newly shipped model must be added to this list in route.sh, never approximated to a name that is already in it" ;;
|
||||
"Sonnet 5"|"Opus 5"|"Fable 5") ;;
|
||||
*) die "--last-model: '$L_MODEL' is not a row-table model (Sonnet 5|Opus 5|Fable 5)" ;;
|
||||
esac
|
||||
case "$L_EFFORT" in
|
||||
low|medium|high|xhigh|max) ;;
|
||||
|
|
@ -267,14 +266,41 @@ row_base_cmd() {
|
|||
esac
|
||||
}
|
||||
|
||||
# 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"; }
|
||||
# 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")"; }
|
||||
|
||||
FB=$((ROW - 1)); [ "$FB" -ge 1 ] || FB=1
|
||||
|
||||
|
|
|
|||
|
|
@ -565,114 +565,6 @@ run_hook "$P"
|
|||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: correcting done -> blocked is allowed with unpushed commits" $?
|
||||
unset SG_PATH SG_CONTENT
|
||||
|
||||
# --- 11. F13: the line limit is overridable, and an unusable override is loud
|
||||
# The limit was a bare `const MAX_LINES = 120`, so a test of the BOUNDARY had
|
||||
# no choice but to hardcode 120 in every fixture - which means the tests and
|
||||
# the code encoded the same number twice, and section 1's boundary fixtures
|
||||
# would have to be rewritten by hand the next time the operator moves it (they
|
||||
# already were once, 60 -> 120). CLAUDE_STATE_MAX_LINES makes the boundary
|
||||
# testable at a cheap value AND gives the operator the same knob
|
||||
# CLAUDE_COORD_DIR gives them over the mailbox root.
|
||||
#
|
||||
# The override is not a bypass claim: this guard has always been escapable by
|
||||
# writing the file some other way (Bash, an editor), exactly as the sibling
|
||||
# pathguard is. What it must never do is silently NOT take effect.
|
||||
|
||||
# Control first, at the DEFAULT: with no override set, the shipped limit still
|
||||
# governs. This is what proves the denials below come from the override rather
|
||||
# than from the guard having become stricter for everyone.
|
||||
unset CLAUDE_STATE_MAX_LINES
|
||||
P="$(payload '
|
||||
const content = "x\n".repeat(6);
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: "/tmp/f13/STATE.md", content }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]
|
||||
check "F13: control - a 6-line new STATE.md is allowed at the default limit" $?
|
||||
|
||||
# The override takes effect, in BOTH directions. One assertion alone would not
|
||||
# do: a broken parse that clamped everything to 0 would deny the 6-line file
|
||||
# and look like a working override.
|
||||
CLAUDE_STATE_MAX_LINES=5
|
||||
export CLAUDE_STATE_MAX_LINES
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 2 ]
|
||||
check "F13: an override of 5 denies a 6-line new STATE.md" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q 'max 5'
|
||||
check "F13: the denial message quotes the OVERRIDDEN limit, not the default" $?
|
||||
|
||||
P5="$(payload '
|
||||
const content = "x\n".repeat(5);
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: "/tmp/f13/STATE.md", content }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P5"
|
||||
[ "$HOOK_EXIT" -eq 0 ]
|
||||
check "F13: exactly-at-the-override is still allowed (boundary, not off by one)" $?
|
||||
|
||||
# The ratchet is a property of the guard, not of the constant, so it must
|
||||
# survive the override: an already-oversized file can still be edited toward
|
||||
# compliance. Same rule section 8 pins at the default.
|
||||
# The file must be named exactly STATE.md and the variable must be exported
|
||||
# BEFORE node reads it. Both were wrong in this section's first cut, and the
|
||||
# check went GREEN anyway - the basename gate let the write through without
|
||||
# measuring a thing, and the fixture file was never created. A vacuous pass
|
||||
# is the very defect this order is closing, so the fixture asserts its own
|
||||
# ground truth before the check that depends on it.
|
||||
mkdir -p "$TMPDIR/f13-ratchet"
|
||||
export SG_BIG="$TMPDIR/f13-ratchet/STATE.md"
|
||||
node -e 'require("fs").writeFileSync(process.env.SG_BIG, "y\n".repeat(40))'
|
||||
[ "$(wc -l < "$SG_BIG" | tr -d ' ')" = "40" ]
|
||||
check "F13: fixture ground truth - the oversized STATE.md really is 40 lines" $?
|
||||
P="$(payload '
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: process.env.SG_BIG, content: "y\n".repeat(20) }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]
|
||||
check "F13: the ratchet survives the override (40 -> 20, still over 5, allowed)" $?
|
||||
|
||||
# The inverse, so the check above cannot pass by the guard simply never firing
|
||||
# on this path: GROWING the same oversized file is still denied at 5.
|
||||
P="$(payload '
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: process.env.SG_BIG, content: "y\n".repeat(60) }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 2 ]
|
||||
check "F13: growing that same oversized file is still denied under the override" $?
|
||||
|
||||
# An override that cannot be used is DENIED, never silently ignored. A silent
|
||||
# fallback to 120 is the exact defect class this whole order is closing: the
|
||||
# caller would believe a limit was in force that never was, and a selftest
|
||||
# would go green having measured the default while claiming to measure 5.
|
||||
# Failing here is recoverable in one action (unset the variable) and the
|
||||
# message says which one.
|
||||
for bad in "0" "-3" "abc" "" "12.5" "1e3"; do
|
||||
CLAUDE_STATE_MAX_LINES="$bad"
|
||||
export CLAUDE_STATE_MAX_LINES
|
||||
run_hook "$P5"
|
||||
[ "$HOOK_EXIT" -eq 2 ] && printf '%s' "$HOOK_STDERR" | grep -q 'CLAUDE_STATE_MAX_LINES'
|
||||
check "F13: an unusable override ('$bad') is refused by name, not ignored" $?
|
||||
done
|
||||
|
||||
# ...and an UNSET variable is not an unusable one. Without this the check above
|
||||
# would pass against a guard that refused every write on the planet.
|
||||
unset CLAUDE_STATE_MAX_LINES
|
||||
run_hook "$P5"
|
||||
[ "$HOOK_EXIT" -eq 0 ]
|
||||
check "F13: control - an UNSET override is the normal case, not a refusal" $?
|
||||
unset SG_BIG
|
||||
|
||||
echo ""
|
||||
echo "state-line-guard-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
|
|
@ -16,14 +16,11 @@ description: >-
|
|||
"hva er billigst å flytte", "hvor bør jeg begynne", "status på tvers av repo",
|
||||
"hva er blokkert", "lag en dagsplan", "planlegg dagen", "hvilke repo skal jeg
|
||||
åpne i dag", "fokusdag på X", "i dag jobber jeg bare med X", "hvilke repo
|
||||
gjelder X". Also covers Voyage briefs in flight across repos: "which briefs are
|
||||
running", "what phase is that brief in", "which brief is blocked on a decision",
|
||||
"show the voyage board", "hvilke briefer er i gang", "hvilken fase ligger den i",
|
||||
"hvilke briefer venter paa en beslutning", "vis Voyage-oversikten". Trigger even when the
|
||||
gjelder X". Trigger even when the
|
||||
user names no repo and no tool — choosing *between* repos is this skill. Not for
|
||||
"where were we" inside the current repo: that is this repo's own STATE.md,
|
||||
already injected at session start.
|
||||
version: "0.34.0"
|
||||
version: "0.27.0"
|
||||
---
|
||||
|
||||
# board — which repo deserves the next session
|
||||
|
|
@ -58,9 +55,7 @@ malformed argument — read stderr and fix it rather than retrying.
|
|||
in the session, from a number quoted in a document, or from memory. Inbox counts,
|
||||
uncommitted files and board lines all change between turns, and a recommendation
|
||||
built on a stale count is the exact defect the operator's premise-verification
|
||||
rule exists to stop. The run costs about eight seconds over ~50 repos (measured 2026-08-31; the
|
||||
older "about three seconds" figure predates both the current tree size and
|
||||
the `VOY` column).
|
||||
rule exists to stop. The run costs about three seconds.
|
||||
|
||||
## What the columns mean
|
||||
|
||||
|
|
@ -69,57 +64,11 @@ the `VOY` column).
|
|||
| `STATUS` | `planned` / `in-progress` / `blocked` / `deferred` / `done`, or `blocked>X` naming the repo it waits on. `?` means the STATE.md has no board line. |
|
||||
| `KOST` | Model/effort for the next step, from the rubric row table in `route.sh` (the `route` skill writes it; this one only reads it). |
|
||||
| `INN` | Unhandled inbox: **other repos are waiting on THIS one**. An obligation it owes outward. |
|
||||
| `ORDRE` | Pending orders: **authorized work is waiting on this repo**, unclaimed and pickable. |
|
||||
| `FLY` | Orders in flight (claimed). Someone TOOK the order — never proof a session is still alive. |
|
||||
| `VOY` | Voyage projects (briefs in flight). `N:Md` = how many, and how long since the **stalest** one's newest artifact. A bare `0` means none. Per-project detail lives in `--voyage`. |
|
||||
| `DRT` | Uncommitted files. |
|
||||
| `ALDER` | Days since STATE.md last changed — the age of the *plan*. `-` where the repo has none. |
|
||||
| `SISTE` | Days since the last commit — the age of the *work*. `-` where the repo has no commits yet. |
|
||||
| `NESTE` | First line of the STATE.md next-step block, truncated. |
|
||||
|
||||
## Briefs in flight — `--voyage`
|
||||
|
||||
"$BOARD" --voyage
|
||||
|
||||
Answers what no single `STATE.md` can: **which Voyage briefs are running, in what
|
||||
phase, and who is waiting on whom.** One `key=value` block per project.
|
||||
|
||||
`fase` is the only field here that cannot be derived from a STATE.md at all, and
|
||||
it is why the view exists: `brief-draft` (the `/trekbrief` review gate has not
|
||||
cleared) → `brief` → `research` → `plan` → `execute` → `review`.
|
||||
|
||||
Read these three the way the engine means them, and never soften them:
|
||||
|
||||
- **`fase` measures ARTIFACTS, not sessions.** A plan executed in one session
|
||||
leaves no file, so `plan` is the last thing the filesystem can prove. Nothing
|
||||
here says a session is alive — the same refusal `FLY` carries.
|
||||
- **`kvalitet=-` means the `brief_quality` field is ABSENT, never that the brief
|
||||
is complete.** Only 8 of ~40 briefs on the real tree carry it. `partial` vs
|
||||
`complete` is exactly what three presence-greps cannot tell apart.
|
||||
- **`research=-` and `research=0` are different facts.** `-` is no research
|
||||
directory (never started); `0` is a directory that exists and holds nothing —
|
||||
a research step with null output.
|
||||
|
||||
`venter=operatoerbeslutning` means the brief declares an open
|
||||
`[BLOCKING DECISION, before S<n>]`, and `blokkerende_gate` names the step it
|
||||
gates. That is the one form of "who waits on whom" the files can prove; the
|
||||
`ordre_id=` lines beside it carry the repo's pending orders, which is what lets a
|
||||
reader go from "this brief is standing still" to "this order is pending".
|
||||
|
||||
Report what the blocks say. Do **not** infer that a brief is abandoned, that a
|
||||
decision has since been resolved, or that a session is running — the view
|
||||
reports and refuses the inference, and so should you.
|
||||
|
||||
**`ORDRE` and `FLY` are never summed, and `FLY` is never read as "busy."** They
|
||||
are the same queue in two states. Before `FLY` existed, a repo with one order in
|
||||
flight and a repo with no orders at all both printed `ORDRE 0` — the same digit
|
||||
for two opposite facts, which is how two tabs sat idle for 45 hours holding
|
||||
finished orders with nothing on the board reporting it. What `FLY` still cannot
|
||||
tell you is whether a session is *running*: nothing un-claims an order when the
|
||||
session that claimed it dies (one order on the live mailbox had been claimed for
|
||||
117 hours). Say "an order is claimed here", never "a session is working here".
|
||||
The board inspects no processes and will not start.
|
||||
|
||||
**`INN` never means "this repo is waiting on someone."** It means the opposite:
|
||||
messages arrived and were not handled. The mailbox format carries no reply-to or
|
||||
thread field, so outbound waiting is not derivable from it at all — `blocked>X` is
|
||||
|
|
@ -229,18 +178,6 @@ Two things to say out loud when you hand it over:
|
|||
command to paste. Name those repos rather than letting the operator discover it
|
||||
per tab. Fixing them is the `route` skill's job, in *that* repo — never a side
|
||||
quest here.
|
||||
- **`ledig_antall=N` and the `ledig=` lines are free capacity, not tabs.** They
|
||||
name the repos that can take NEW work: nothing owed, nothing queued, nothing in
|
||||
flight, clean tree, at `done` or `deferred`. They carry no `tab=` and no
|
||||
command on purpose — there is no next step to start, so the operator decides
|
||||
what to send there. Read them out when the ask is about capacity ("hvem kan ta
|
||||
mer arbeid", "hvor har jeg ledig kapasitet") and whenever the plan is short.
|
||||
`status=done` alone is **not** the same set: on the real tree 4 of 17
|
||||
done/deferred repos were not free. Never derive this list yourself from the
|
||||
table — the engine joins four fields you would have to join by hand.
|
||||
- **`fly=N` on a tab block means that repo already holds a claimed order.** Say
|
||||
so before the operator opens the pane. It is not proof a session is live, and
|
||||
it is not a reason to drop the tab — it is a reason to look first.
|
||||
|
||||
### When the day has a subject ("fokusdag")
|
||||
|
||||
|
|
|
|||
|
|
@ -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.34.0"
|
||||
version: "0.27.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.34.0"
|
||||
version: "0.27.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 and effort 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, effort and advisor flag 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,18 +140,13 @@ 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`. 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.
|
||||
`--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.
|
||||
|
||||
### 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.34.0"
|
||||
version: "0.27.0"
|
||||
---
|
||||
|
||||
# route — what the next session should run with
|
||||
|
|
@ -104,7 +104,8 @@ 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.
|
||||
than scoring the four traits to land there. One fact worth carrying into that
|
||||
override: a Fable session runs without an advisor.
|
||||
|
||||
## The last-session record
|
||||
|
||||
|
|
@ -185,26 +186,24 @@ 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.** 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.
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
**`--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.
|
||||
|
||||
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, readFileSync, existsSync } from 'node:fs';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
|
@ -12,31 +12,8 @@ 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', () => {
|
||||
runSuite('coord');
|
||||
execFileSync('bash', [join(root, 'scripts', 'coord-selftest.sh')], { encoding: 'utf8' });
|
||||
});
|
||||
|
||||
// board.sh reads this plugin's mailbox for its INN column, so the board ships
|
||||
|
|
@ -45,7 +22,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', () => {
|
||||
runSuite('board');
|
||||
execFileSync('bash', [join(root, 'scripts', 'board-selftest.sh')], { encoding: 'utf8' });
|
||||
});
|
||||
|
||||
// route.sh is the WRITER for the next-cost field board.sh already reads, so its
|
||||
|
|
@ -54,7 +31,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', () => {
|
||||
runSuite('route');
|
||||
execFileSync('bash', [join(root, 'scripts', 'route-selftest.sh')], { encoding: 'utf8' });
|
||||
});
|
||||
|
||||
// pre-state-line-guard.mjs is a PreToolUse hook, so like session-start.mjs it
|
||||
|
|
@ -62,7 +39,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', () => {
|
||||
runSuite('state-line-guard');
|
||||
execFileSync('bash', [join(root, 'scripts', 'state-line-guard-selftest.sh')], { encoding: 'utf8' });
|
||||
});
|
||||
|
||||
// The order queue is the second channel beside the mailbox, with the opposite
|
||||
|
|
@ -71,7 +48,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', () => {
|
||||
runSuite('orders');
|
||||
execFileSync('bash', [join(root, 'scripts', 'orders-selftest.sh')], { encoding: 'utf8' });
|
||||
});
|
||||
|
||||
// The engine refuses to invent an identity from the cwd, but the hook is the
|
||||
|
|
@ -211,69 +188,3 @@ 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