Commit graph

90 commits

Author SHA1 Message Date
dde392d79d fix(board): exact-match status and route-trait tokens, not a prefix
board.sh's status classifier and route_cmd_for()'s four-trait extraction
both used a [a-z-]* sed capture that stops at the first byte outside that
class instead of running to the field's real boundary. status=done2
silently classified as done (excluded from --plan like a real done repo,
shown green); status=Planned captured as empty and read as "?" (no board
line at all), feeding the MERK footer a false count. route_cmd_for() had
the same defect on all four traits: path=known2 truncated to known, which
route.sh's own exact-match validation then accepted, producing a
safely-worded but WRONG startup command instead of a refusal.

Fixed by capturing to the next ';' or the closing '-->' (the same
[^;>]* + trim shape next-cost already used), so the exact-match case
statements downstream see the real, un-truncated value.

Selftest: repo-status-prefix, repo-status-case, repo-trait-prefix and
repo-revers-prefix pin all four cases, with known-positive controls that
a genuinely absent board line still reads ? and a genuinely valid route
line still derives a real command. All four suites green: coord 220,
board 229, route 69, guard 40. Verified no MALFORMED regression against
the real ~/repos tree (empty set before and after).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxLmbNN6qswBuu6XkSYVrt
2026-08-16 22:47:07 +02:00
165385be5f chore(release): 0.26.0
The done-guard (d16a3f5) landed after 0.25.0 was already tagged and published
on 95ac710, with the catalog ref pinned to it (catalog 01161f0). Moving a
published tag would swap content under a name consumers may already have
fetched, so the guard gets its own release instead.

Version bumped in all seven places: plugin.json, package.json, the README
badge, and the coord-send / board / route / dispatch skill frontmatters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4LMWBQGmufmBU6UdvJZ2E
2026-08-16 22:19:08 +02:00
d16a3f57e7 feat(state-line-guard): deny status=done while commits are unpushed
ORDRE 42 (operator, 2026-08-16). Two sessions had their push refused by the
UFW rate limit on port 22, reported that honestly in the coord inbox, and
wrote status=done anyway: board line green, one commit unpushed, published
surface 404. `done` meant "the session finished" where every reader takes it
to mean "the work landed" -- and since `done` drops a repo from the board
plan, `morning --say <repo>` could not reach either of them.

The deny sits on the WRITE, not on session end. Measured against the official
hooks docs rather than assumed: Stop fires "once per turn" with no signal
marking the last one, and its exit 2 "prevents Claude from stopping", so a
repo that genuinely cannot push would get a session that will not end;
SessionEnd is the once-per-session event and cannot block at all.

Fails open on every git uncertainty (no upstream, detached HEAD, missing
remote-tracking ref, not a repo) -- 8 of 44 repos on the real tree have no
upstream, one already status=done. Compares against the branch's own
upstream, never a hardcoded origin/main (three repos sit on master). Selects
the board line with board.sh's own anchor, so prose saying status=done never
triggers it. status=blocked and status=in-progress stay writable in the same
single edit, so the deny can never wedge a session.

state-line-guard-selftest.sh section 10, 17 checks (23 -> 40), including the
mandatory known-positive: status=done with everything pushed still allows.
Both outcomes also verified against real repos -- app-creator (1 unpushed)
denied, repo-mailbox (clean) allowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4LMWBQGmufmBU6UdvJZ2E
2026-08-16 22:06:44 +02:00
95ac7101ea docs(security): add SECURITY.md v0.25.0
Vulnerability reporting policy for the C-axis trust program: private
disclosure address, response process, and an honest pre-1.0 supported-
versions statement (no fabricated version table, no SBOM claim).
2026-08-16 21:15:15 +02:00
05e56eb2a5 chore(release): 0.25.0
Version bumped across plugin.json, package.json, the README version badge and
all four skill frontmatters (coord-send, board, route, dispatch), plus the
README skills (3 -> 4) and selftest-checks (433 -> 529) badges. CHANGELOG
entry for --dispatch/the dispatch skill and the coord-send reply-mode fix.

NOT released: no tag, no catalog ref bump. Both require pushing to Forgejo,
and the operator has held the push window closed. The catalog gate is clear
(check-versions.mjs: 12 plugins, 0 ERROR; the single WARN is this bump
itself), so the release runs the moment the window opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu
2026-08-16 16:19:26 +02:00
1ee003328c feat(board): --dispatch, the startup command a dispatched session can act on
"Start a session in repo X, on order Y, at cost Z" was produced by hand, and
it misfired four times on 2026-08-16 across two repos. Three distinct holes,
all measured, all closed here:

1. A bare `claude --model X --effort Y` forces the operator to type Go, and
   the session then guesses its task out of STATE.md. The emitted command
   carries the prompt in argv: `... "$(cat <file>)"`. Verified directly that
   this passes the file's bytes as ONE argv element with no re-evaluation, so
   $(...), backticks, quotes and UTF-8 in the prompt BODY are inert - only the
   PATH is expanded, so it must be absolute and shell-clean.
2. --no-go stops only the follow-up Go message, never the work (morning:806).
   The plan-file form says so in its own output, not just in a comment.
3. A session dispatching its own next session gets an empty plan: morning's
   plan_drop_open (morning:1788) drops a block whose repo already has a pane,
   and --dry-run says "0 of 1", which reads as a broken plan file. --dispatch
   therefore emits two forms, chosen by --target-pane: a plan block, or a
   bare paste line for the tab that already exists (and no tab= key at all,
   so it can never be fed to morning as a plan).

Generator ownership, the question left open for two sessions: it goes in
board.sh, which already owns the block format including paste=. A second
emitter of tab=/repo=/dir=/command=/paste= would be two copies of one file
format. Read-only survives - the prompt file and the plan file are written by
the caller, the brief-nightly.sh split unchanged.

--target-pane yes|no is REQUIRED with no default, the same rule --last-effort
carries: it is a measurement (morning --probe-panes, which works without a
tty), and the dry-run cannot substitute for it - run from a Claude session
morning reports "window: unknown ... assuming an empty window" and
plan_drop_open never fires, so a dry-run gate would pass the self-dispatch
case every time.

Cost comes from route.sh's row table; --dispatch deliberately takes no
--model/--effort, because --advisor opus is a property of the ROW and a
dispatch taking the model directly has no honest source for that flag.

New skills/dispatch/SKILL.md is the front door. board-selftest 183 -> 217.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu
2026-08-16 16:05:53 +02:00
21e2873e21 fix(coord-send): reply mode claimed "marked handled" without checking
coord-send.sh ran coord-done under `>/dev/null 2>&1` and then printed the
handled claim unconditionally. Measured with a stub coord-done exiting 1:
the original stayed in the inbox, no archive/ was created, and coord-send
still exited 0 saying "marked handled" - a false success in the message
transport itself, which is why every reply had to be verified by hand.

The predicate is deliberately wider than the exit code: coord-done exits 0
when it archives nothing (an unknown name is idempotently fine by its own
contract), so an exit-code-only fix still certifies a message that never
moved. The check is exit 0 AND the original no longer being at
$COORD/$FROM/inbox/$REPLYTO - recomputed rather than reusing $REPLY_ORIG,
which resolves to the inbox OR the archive, so replying to an already
archived original moves nothing and must not warn.

Failure is exit 1, a new status: the reply WAS delivered and re-sending it
would duplicate it, so 2 stays the nothing-was-written status.

Selftest section 34 (14 checks, red first) pins all four cases: coord-done
fails outright, coord-done exits 0 without moving, the real happy path, and
an archive-path reply. coord-selftest 206 -> 220.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu
2026-08-16 15:42:13 +02:00
b5c860eb03 fix(state-line-guard): Edit path used String.replace, not a function
current.replace(oldStr, newStr) with newStr as a STRING lets JS treat
$-sequences inside it ($&, $`, $', $$, $n) as special replacement
patterns, even though oldStr (the search side) is a plain string. A
new_string documenting old backtick-substitution style ($`cmd`) - the
kind of prose a STATE.md shell-conventions section writes routinely -
triggers it. Measured against the real bug (.claude/STATE.md,
2026-08-15): a 5-line addition on a 112-line file projected to 219
lines and was wrongly denied.

Fix: current.replace(oldStr, () => newStr) - a function replacement is
never pattern-substituted, covering every $-sequence at once. The
replace_all branch (split/join) was never affected.

Direction was always fail-closed (over-blocks, never under-blocks a
real oversize), but it made exactly the STATE.md files that document
shell conventions hard to edit via Edit.

state-line-guard-selftest.sh: 23/23 (+2, section 9: $` as the repro,
$& as a second sequence proving the fix is general).

Also updates CLAUDE.md's pinned selftest counts (197/178/69/21 were
already stale before this session's own additions; now 206/183/69/23).
2026-08-15 20:37:18 +02:00
bd24b8f0e7 feat(coord-send): reject the retired ktg-plugin-marketplace address
Operator decision 2026-08-15: ktg-plugin-marketplace is a polyrepo
directory, not a git repo, so no session can ever hold that coord
identity naturally. catalog's H4 reply confirmed adoption was declined
and drained the 6 stray messages as a one-time settlement, not an
ongoing subscription. --to now fails loud with a pointer to catalog
instead of silently redirecting mail somewhere the sender doesn't
believe it landed - the same misdelivery defect this closes a second
time (2 messages sat undelivered 2 days on this exact misaddressing).
Only --to is retired; --from is untouched since the defect was mail
arriving there, not mail claiming to originate there.

coord-selftest.sh: 206/206 (+6, section 33).
2026-08-15 20:37:11 +02:00
87194fe574 fix(board): a missing coord-count.sh sibling warns instead of lying
Review finding 1 (docs/2026-08-14-confident-zero-review.md), tier 1 #2,
prioritized by .claude 2026-08-14T21:24:16Z coord message.

board.sh:381,395,409,516-517 gated four coord-count.sh invocations on
`[ -f "$SELFDIR/coord-count.sh" ]` and silently continued with empty
lookups when it failed - the 0.12.1 deployed-copy incident shape, a real
deployment state, not hypothetical. Reproduced exactly as the review
measured it (copy board.sh + route.sh to a scratch dir without
coord-count.sh, fixture mailbox with one reply-owing message): --brief
claimed "Ingen repo skylder noen et svar i dag." and then actively
mislabeled the reply-owing repo as "bare FYI-post" - a false statement,
not merely an absent one, since inbox>0/owed==0 looks identical whether
owed is genuinely 0 or simply uncomputed. --inbox-plan silently rendered
0 blocks instead of 1.

Fixed by computing HAVE_COUNT once and checking it everywhere a missing
sibling would otherwise read as "checked, found nothing": brief() no
longer enters the zero-debt/FYI-mislabel branch when the sibling is
missing, printing a warning instead; brief_orphans() and
brief_deadletters() warn instead of silently returning nothing (the
review's "dead-letter and orphan cross-checks vanish silently too");
plan() and inbox_plan() emit a key=value `advarsel=` line (not a '#'
comment - the driver consumer drops comment lines by rule, same
reasoning as the existing fokus= disclosure). plan()'s `keep` membership
logic is deliberately untouched: changing which repos appear in --plan
under this condition is a policy change to a format with a second
consumer in another repo that no test here can hold stable, and is a
proposal for the operator, not this fix.

Verified red before fixing: the reviewer's exact reproduction against
`git show HEAD:scripts/board.sh` (pre-fix) confirmed the mislabel and
the silent 0-block --inbox-plan, so the new board-selftest.sh section 17
fixture is red for the right reason, not by accident. Smoke-tested
against the real ~/repos tree (coord-count.sh present, HAVE_COUNT=1):
unchanged output, no warning.

All four selftest suites green: coord 200/200, board 183/183 (5 new
checks in section 17), route 69/69, guard 21/21.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1mzsaUmjqFHArbArG7EpF
2026-08-14 23:53:31 +02:00
db57b43f50 fix(coord-count): distinguish claimed from unmeasured, fix Linux date parse
Review finding 11 (docs/2026-08-14-confident-zero-review.md), tier 1,
prioritized by .claude 2026-08-14T21:24:16Z coord message.

Two bugs collapsed into one token. Column 4 of coord-count.sh (the WP1d
origin-age column) printed "-" for both "has .origin" (claimed, not a
dead-letter candidate) and "age could not be measured" - a consumer could
not tell "not a dead letter" from "not measured". Fixed by reserving "-"
for claimed only and introducing "?" for could-not-measure; board.sh's
sole column-4 consumer (awk '$4 != "-" && $4+0 >= 3') needed no change,
since awk's numeric coercion already treats "?" as 0 and filters it out
the same way "-" always was.

Second, the age computation itself used `date -u -j -f ...`, a BSD-only
invocation. Measured directly (Ubuntu 24.04, GNU coreutils 9.4, real
coord-count.sh, unmodified) rather than reasoned about: GNU date rejects
-j outright ("invalid option -- 'j'", exit 1), so every mailbox printed
"-" on Linux regardless of actual dead-letter status - WP1d detection was
silently inert on the one platform this public plugin cannot assume away.
Fixed with a one-time `date --version` flavor check (measured: exits 0
with a GNU banner on GNU date, exits nonzero with "illegal option" on BSD
date) branching to `date -u -d <RFC-3339-string>` on GNU, unchanged
`-j -f` on BSD. The RFC 3339 acceptance is documented (GNU Coreutils
manual, "Options for date"), not live-measured, since Colima was removed
from this machine mid-session before that specific sub-claim could be
re-verified live.

coord-selftest.sh section 31 (e) previously pinned the bug (asserted "-"
for an ungrammatical filename); updated to assert "?", plus a new
contrast check that claimed and could-not-measure are always different
tokens. Section 32 pins the GNU branch with a PATH shim for `date` that
replays only measured facts (the --version and -j behaviors above) rather
than a speculative mock.

All four selftest suites green: coord 200/200, board 178/178 (regression,
unaffected as predicted), route 69/69, guard 21/21.

Out of scope, reported not fixed: coord-sweep.sh:77 and both selftests'
fixture generators use `date -v-Nd` (BSD-only, same class) - the suite
itself cannot run on Linux yet. No CI exists in this repo to catch that
drift automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1mzsaUmjqFHArbArG7EpF
2026-08-14 23:44:01 +02:00
108a73a497 docs(review): confident-zero design review of coord-send, coord-count, board, route
14 findings, one lens: every place an unknown or an error becomes a
confident zero or a successful exit. All measured against throwaway
fixtures; no behavior changed. Operator prioritizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NYneni3VzRUMn1UFszytrf
2026-08-14 23:16:59 +02:00
03e36c9499 chore(release): 0.24.0 v0.24.0
Version bump across the six files the release gate checks (plugin.json,
package.json, README badge, three SKILL.md frontmatter fields) plus the
CHANGELOG entry, covering the three commits unreleased since v0.23.0:

- 09fd4b7 feat(board): --inbox-plan, a fourth rendering
- 44fb34a fix(hooks): state-line-guard MAX_LINES 60 -> 120
- 19c0c10 feat(coord,board): dead-letter detection (WP1d detection half)

All four selftest suites green at this commit: coord 197/197, board
178/178, route 69/69, state-line-guard 21/21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Neu6DivA9k5op2rSEvcewn
2026-08-14 22:24:28 +02:00
19c0c1010b feat(coord,board): flag mailboxes no session has ever read (WP1d detection half)
coord-count.sh gains a fourth TSV column: "-" when a mailbox has .origin
(a real session has read it via SessionStart), otherwise the age in whole
days of the oldest pending message. .origin is only written by
coord-inbox.sh's non---repo path, so its absence means the mailbox is
never reached by normal injection — a genuine dead letter, not merely
slow. board.sh's --brief surfaces mailboxes past the 3-day threshold as
a new "ALDRI LEST" section, mirroring the existing orphan-mailbox
listing. This is WP1d's detection half only (per .claude's coord
bestilling 2026-08-14); the action half (report-to-sender / retract) is
unapproved design, not built here.

coord-selftest.sh: 191 -> 197 checks. board-selftest.sh: 175 -> 178
checks (net +3; section 16 adds 3 new fixtures on top of the existing
175 baseline, some pre-existing counts shift with the trailing column).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194eV8b6BXNv6aKLovP8TP6
2026-08-14 21:52:47 +02:00
44fb34ab72 fix(hooks): raise state-line-guard MAX_LINES from 60 to 120
Operator decision 2026-08-14: the STATE.md convention's line limit moved
from ~60 to ~120 (global CLAUDE.md already updated). Re-bases every
selftest fixture and boundary value that encoded 60 as a literal,
including section 8's ratchet fixtures, so they still exercise the
ratchet rather than degenerating into a flat gate at the new threshold.
Re-verified the real-tree justification at 120: 13 files over the limit,
one at 1496 (was 23 over 60, one at 1405 — left as historical record).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194eV8b6BXNv6aKLovP8TP6
2026-08-14 21:35:31 +02:00
09fd4b74fa feat(board): add --inbox-plan, a fourth rendering independent of --plan's admission
morning-driver's own regression (0.22.0 narrowed --plan's admission gate,
and morning --innboks derives its population from --plan, so 21 mailboxes
with unhandled post produced only 3 openable tabs): --plan answers "which
repos deserve a tab today" (admission, ranking, a cap), --inbox-plan answers
"which repos have unhandled post" (population, no judgement, no cap) - a
superset of --plan's candidates, not a complement.

One block per name with pending>0 in the mailbox, classified against
board.sh's own RECORDS scan (no new discovery logic): class=repo (STATE.md
present), no-state (scanned repo, no STATE.md), orphan-mailbox (no matching
directory at all). pending=/owed= surface coord-count.sh's two counts
directly. TDD-first: board-selftest.sh section 15 (6 fixtures, isolated
root+mailbox) written and confirmed red before implementation.

Accepted work order: morning-driver 20260814T175317Z, corrected 20260814T180854Z.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mKC7v8gytdsota36tL7oP
2026-08-14 20:30:22 +02:00
bf11cbf89d docs(hooks): document the absolute-path assumption in state-line-guard v0.23.0
currentLineCountOf() swallows a read failure as current=0. A relative
file_path resolving against the wrong cwd would silently collapse the
ratchet back to a flat gate (Write) or disable enforcement entirely
(Edit). The Write/Edit tool contracts require an absolute file_path, so
this can't happen in practice -- noted advisor concern, verified against
the tool schemas, recorded in the hook's own comment so a future change
doesn't harden this without re-reading why it was never needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186kZGKddxfA9N84HqMLbb2
2026-08-14 17:12:59 +02:00
c1dabf109d fix(hooks): state-line-guard ratchets against current size, not a flat gate
Advisor review caught this before the v0.23.0 tag landed: the guard
compared the projected line count only against the fixed 60-line max,
never against the file's current size, so trimming an already-oversized
STATE.md (e.g. 156 -> 100 lines, still over 60 but smaller) was denied
exactly like growing it would be.

Verified against the real tree: 23 of the machine's STATE.md files are
already over 60 lines today, one at 1405. Shipped as a flat gate, this
hook would have made most of them un-editable except by a single write
landing at <=60 in one shot -- backwards for a guard meant to make
trimming possible.

Fixed with a ratchet: deny only when the projection is over the max AND
larger than the file's current line count (0 for a file that doesn't
exist yet), for both Write and Edit. A compliant file still cannot grow
past the limit and a new file still cannot be created oversized, but an
oversized file can now be edited toward compliance one write at a time.

state-line-guard-selftest.sh: 16 -> 21 checks (new section 8: shrink
allows, same-size allows, grow-while-oversized still denies, new-oversized
still denies). Suite total: 191 + 152 + 69 + 21 = 433.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186kZGKddxfA9N84HqMLbb2
2026-08-14 17:10:04 +02:00
f39c0df929 feat(hooks): enforce STATE.md's ~60-line convention with a PreToolUse guard
org-ops dispatched a work order (20260814T144553Z) from an /insights sweep
of 160 sessions: a real STATE.md drifted to 155-156 lines before anyone
noticed, and one trim pass on it increased the line count instead of
shrinking it. Prose alone doesn't enforce.

org-ops proposed a PostToolUse hook. Checked against the official hooks
docs first: PostToolUse fires after the tool has already written the file
and cannot block it (confirmed "Can block? No"), only nag afterward. Built
it as PreToolUse instead, the only event that can deny the call before the
file lands.

pre-state-line-guard.mjs denies (stderr + exit 2, matching llm-security's
pre-write-pathguard.mjs) a Write or Edit on any STATE.md whose projected
result exceeds 60 lines. Write projects from the call's own content; Edit
projects from the current on-disk file with old_string replaced by
new_string, honoring replace_all (every occurrence) vs the default (first
occurrence only) the same way the real Edit tool does. Anything the hook
can't project confidently (missing file, old_string not found) is left to
the real tool.

state-line-guard-selftest.sh: 16 checks, including a replace_all fixture
that a first-occurrence-only projection would wrongly allow. Wired into
hooks/hooks.json as PreToolUse on Write|Edit. Version 0.22.0 -> 0.23.0.
Suite total: 191 + 152 + 69 + 16 = 428.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186kZGKddxfA9N84HqMLbb2
2026-08-14 17:01:20 +02:00
61aebad748 fix(board): --brief's zero-debt branch no longer claims zero pending mail v0.22.0
Found in review of the previous commit, before catalog tags 0.22.0: once
n_owe counts OWED repos rather than raw pending, its ==0 branch could fire
while a repo still held FYI-only mail, making "Ingen repo har uhaandtert
innboks" false at the exact moment it printed. Fixed to state only the
debt claim, and to name any FYI-only mailboxes found instead of letting
their existence go unmentioned. No fixture in the shared test tree ever
reached n_owe==0 (it always carries a debtor), so this needed its own
isolated-root fixture to pin.

Still part of the 0.22.0 release -- amends that changelog entry rather
than bumping again, since no tag exists yet.

board-selftest.sh: 150 -> 152 checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGWMPskXBsTjMrrQ2GofFx
2026-08-13 21:07:39 +02:00
03e712a423 fix(board): plan/brief use actual debt, not raw pending mail
board.sh --plan group 2 and --brief conflated every unhandled inbox
message with an obligation to reply, including ones the sender
declared reply-expected: no (a notice, not a request). Reported by
morning-driver (2026-08-11), independently reproduced against the live
mailbox on 2026-08-13: 27 of 72 pending messages were notices. Both
paths now join against coord-count.sh's owed column instead, so a
done/deferred/blocked repo whose only mail is FYI no longer gets a
plan tab, and --brief no longer counts a notice as an obligation. The
table's raw INN column is unchanged by design.

While extending that join with a second lookup file, found and fixed
a more severe, independent defect: the existing $UNBLOCKS/$RECORDS
join used the NR==FNR awk idiom, which silently empties the entire
plan whenever the first file is empty -- i.e. whenever the repo tree
has zero blocked repos, a common, ordinary state, not an edge case.
Verified against the shipped 0.21.0 script. Fixed by matching on
FILENAME instead of cumulative line counts, for both lookup files.

Also repoints the README's governance link at repo-standard's
canonical GOVERNANCE.md (was pointing at the marketplace's copy),
per org-ops D11.

board-selftest.sh: 142 -> 150 checks. Version 0.21.0 -> 0.22.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGWMPskXBsTjMrrQ2GofFx
2026-08-13 20:54:55 +02:00
b02c2880e3 Revert "docs(readme): drop the static selftest-checks badge"
This reverts commit 78bb997.

The badge isn't decorative: it's the input to the catalog's per-axis
stat-mirror-gate (check-versions.mjs:49-59), which cross-checks the
catalog's own stat line against this badge on every release. Dropping
it silently ungates that number -- exactly the defect class v0.20.2
was released to close, and the one check-versions.mjs cites by name
as "the standing proof that ungated numbers rot" (repo-mailbox's own
selftest count drifted to 251 while badge-less).

org-ops's census 06 (BADGE-STATIC-CLAIM) reads the badge as "a claim
dressed as evidence" because nothing visibly runs behind it. That's a
false-positive premise here: the count is verified, just off-badge --
by a script in another repo, at release time, not by a live CI run.
Answering the census with that mechanism instead of complying with
"link it or drop it".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015eUDLeEf8P4dithWDRhSqH
2026-08-11 21:39:14 +02:00
78bb997884 docs(readme): drop the static selftest-checks badge
org-ops census 06 (BADGE-STATIC-CLAIM) flags it as a claim dressed as
evidence: the badge asserts a run that nothing verifies. This repo has
no CI/workflow to link it to (no .github/workflows, no Forgejo Actions),
so dropping is the only real fix -- linking to a nonexistent run would
just be a different unverified claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015eUDLeEf8P4dithWDRhSqH
2026-08-11 21:36:42 +02:00
6c144174bd docs(release): document the manual check-versions discipline until catalog's pre-flight gate ships
Catalog confirmed --write --commit does not close the ERROR window (execFileSync
gate runs before the --commit conditional regardless of the flag) and is
evaluating a pre-flight gate as a future direction, not yet implemented. Per
their explicit request, document the interim discipline instead of a gate that
doesn't exist yet: run check-versions.mjs manually and confirm 0 ERROR before
--write, even for an unrelated plugin's error.
2026-08-10 20:36:19 +02:00
dd8f3ce042 feat(route)!: Fable rows are a hand-written override, never a route.sh outcome v0.21.0
The operator removed the "Fable can only be suggested after a failed Opus
5/xhigh session" policy on 2026-08-06 - it stood in the way too often. Until
now that policy was implemented as code: --opus-xhigh-failed was the ONLY
way route.sh's SELECTION chain could reach rows 5-6 (Fable), so the rubric
still enforced a policy the operator had already dropped.

Given a choice between (A) deleting the branches and the flag outright, so
the calculator's output range closes at row 4, or (C) keeping a path that
renders a Fable row on explicit instruction with rule=operator-override, the
operator chose A (AskUserQuestion) - CLAUDE.md is explicit that the rubric
stays the only deterministic lookup and a Fable choice is now always a
deviation from it, recorded in STATE as an override rather than produced
here. board.sh is untouched and still parses "Fable 5/high"/"Fable 5/xhigh"
written by hand into a board line (route-selftest.sh section 6 now pins that
half directly, since route.sh can no longer produce the strings itself).

TDD: every affected selftest check was rewritten to fail against the
unmodified route.sh first (72->68/69, confirmed red), then route.sh was
edited to match. --opus-xhigh-failed is gone outright - passing it now exits
2 like any other unknown argument, not silently accepted as a no-op.

route-selftest.sh: 73 -> 69 checks (three checks tested command shapes
row_advisor() can no longer produce; the two row-5/row-6 reachability checks
in section 1 collapsed into one "the flag is gone" check). coord 191, board
142 unaffected. Suite total 406 -> 402.

Also folds in a standalone fix already pushed this session: section 14 was
gating the wrong CLI fact (whether "claude --advisor fable" is rejected,
which row_advisor() never depends on) instead of the one it actually rests
on (whether opus/sonnet can advise a FABLE main model). Re-pointed and
verified against the installed CC 2.1.226.

Verified before committing: grepped every repo under ~/repos for a route
line carrying --opus-xhigh-failed (none - one repo has it in prose only,
not in its <!-- route: --> comment) and ran board.sh --plan/--brief over the
real tree to confirm no repo's command line broke. Sent a follow-up
coord-send to catalog superseding an earlier now-stale "406" stat-line
correction with the current 402.

skills/route/SKILL.md: usage block, "last-session record" framing, and the
closing --opus-xhigh-failed paragraph rewritten to match. CLAUDE.md, README
and CHANGELOG updated (checks 73->69, badge 406->402, new 0.21.0 entry).
Version bumped 0.20.3 -> 0.21.0 across plugin.json, package.json and all
three skill frontmatters (breaking CLI removal at 0.x -> minor, per v0.20.0
precedent).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ap1WKHCDPcSfpjo4ds2dQX
2026-08-09 22:03:05 +02:00
4821ba8d4b fix(route-selftest): re-point section 14's CLI gate at the fact row_advisor() actually depends on
The pre-existing failure (route-selftest 72/73, verified with git stash
against clean main) was measuring the wrong direction: it checked whether
"claude --advisor fable" is rejected, but route.sh's row_advisor() never
emits --advisor fable at all - it depends on the opposite fact, whether
opus/sonnet can advise a FABLE main model. Both checks happened to agree
when written; they diverged once the CLI's capability ordering changed.

Measured against the installed CC 2.1.226: opus/sonnet advising a fable
main is still rejected ("cannot advise ... must be at least as capable"),
which is the fact rows 5-6's advisor suppression rests on and now stays
pinned. Also pinned row 1-2's check to an explicit --model sonnet rather
than the ambient default. Separately worth flagging to the operator (not
acted on here): fable itself is now accepted as an advisor for opus/sonnet
mains, which CLAUDE.md's standing note about CC 2.1.220 does not reflect.

route-selftest.sh: 73/73.
2026-08-09 21:45:12 +02:00
652dabc48e chore(release): prepare v0.20.3 - dot-prefixed repo name fix v0.20.3
Version bump for f425a11 (coord-send.sh/coord-count.sh/coord-sweep.sh
dot-prefixed repo name fix). Bumps plugin.json, package.json, README
badge, and the three skill frontmatters together; CHANGELOG entry added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHmf1VfCvaYXamJxe5y6Vt
2026-08-09 21:14:31 +02:00
f425a11311 fix(coord-send): stop rejecting dot-prefixed repo names
coord-send.sh:154 guarded the --to target with a `.*` case arm, which
rejects every leading-dot name instead of just `.` and `..`. A
dot-prefixed name is a real repo identity (basename of a git toplevel
under a hidden directory, e.g. ~/.claude) and was reported unreachable
by morning-driver via coord message 20260809T103138Z. Narrowed the
guard to reject exactly `.` and `..`, matching the equivalent guards
already used elsewhere in this file and in coord-done.sh.

coord-count.sh and coord-sweep.sh both enumerate the mailbox root with
a bare "$COORD"/* glob, which by construction never matches a
dot-prefixed directory - confirmed empirically before this change.
Fixing only the send-side guard would have let a dot-prefixed mailbox
receive mail that neither script could ever report or close on its
grace window. Both now also glob "$COORD"/.[!.]* to reach hidden
mailboxes without matching "." or "..".

coord-selftest.sh: 183 -> 191 checks, section 30 covers the fix and
both enumeration paths. Doc counts (README badge, CLAUDE.md) updated
to match; the catalog's mirrored badge is untouched pending release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHmf1VfCvaYXamJxe5y6Vt
2026-08-09 21:09:23 +02:00
ddc978d4c1 docs(readme): trim the badge row to five, recolor the selftest count
repo-standard's presentation check flagged two WARNs. Both land on the
badge row, and one of them collides with a decision made yesterday.

BADGE-STATIC-CLAIM: green is CI idiom for "passing", and this repo has
no CI by design (a stated non-goal — the forge has no runner). Linking
the badge to a real run is therefore impossible, but DROPPING it is not
free either: the catalog's stat-mirror gate is per-axis and badge-gated,
so an unbadged axis is skipped silently and the number it protects can
rot again (measured 6 and 251 against a true 8 and 398). The badge stays
and turns blue, which reads as a count rather than a pass state. The
catalog parser splits label/value/color and ignores the color, so the
gate is unaffected.

BADGE-COUNT: seven badges, past the point a row reads as clutter. Five
of them are load-bearing — version is ERROR-gated by the catalog, and
hooks/skills/CLI scripts/selftest checks are the four axes it mirrors.
Platform and License are not: the first restates the opening line, the
second restates the License section that already links LICENSE. Dropping
those two lands exactly on five without ungating anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bWgoaf46fu6DagYQyDVR5
2026-08-04 11:49:29 +02:00
cbcd236050 docs(readme): meet the repo-standard gate — matching opening line, non-goals, changelog link
The gate (repo-standard v0.1.1) reported 3 ERROR against this repo:

- README-DESC: the first line after the H1 was the `coord` rename note, not
  the forge description. That line is the only place a machine can check
  description == catalog == README from inside the repo, so it now carries the
  forge description verbatim (179 codepoints, unchanged upstream) and the
  rename note moves one paragraph down.
- HEADING-MISSING `## Non-goals`: added. It answers "is this for me?" — not a
  state store, not a network protocol, not an issue tracker, not a
  conversation, the board never writes, and no CI.
- HEADING-MISSING `## Changelog`: added as a link to CHANGELOG.md.

The one remaining finding is a WARN on the `Selftest checks` badge and it
stands deliberately: the badge counts the checks in the suite, it does not
assert a passing run, and the gate's own escape hatch is to link the badge to
a real run — there is no runner to link to. The honest substitute the standard
asks for is now stated in Non-goals and in Development.

README only; no code path touched. 398/398 selftest checks still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bWgoaf46fu6DagYQyDVR5
2026-08-03 21:55:20 +02:00
5c182578e6 chore(release): prepare v0.20.2 - README badges for CLI scripts and selftest checks v0.20.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7V99BXv1v5pWHUwY63it
2026-08-03 10:16:26 +02:00
dbc68fbc73 docs(readme): badge the CLI scripts and selftest checks axes
The catalog's stat-mirror gate is per-axis and badge-gated: an axis
the plugin does not badge is skipped silently, and these two were the
ones measured stale (6 and 251 against a true 8 and 398). Adding the
badges here lets the gate start enforcing them once the next release
tags this README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7V99BXv1v5pWHUwY63it
2026-08-03 10:13:54 +02:00
123d40e4b4 fix(board): track an open HTML comment through to its closer v0.20.1
Skipping lines that START with the comment opener is not the same as
skipping a comment. Every continuation line of a wrapped comment under
the NESTE heading still looked like prose, so the excerpt became comment
internals instead of the next step. Both the board line and the route
line wrap easily; measured, 1 of 28 repos was affected, with the symptom
held down only by the convention of keeping those comments one-line.

An open comment is now tracked to its closer, and a NESTE block that is
nothing but a comment reports an empty block rather than promoting the
comment's own text.

Two limits are the format, not leftovers: an HTML comment body may not
contain the closer at all, so a rationale quoting it still ends its own
comment early.

The fixture is the real shape - one-line, wrapped, one-line, prose -
because a lone wrapped comment passes even with a flag that never
resets, and the continuation marker sits at the start of its line
because the table's 38-char cut would otherwise truncate it away and
the check would pass against the broken code too.

board-selftest 138 -> 142.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014K262DRWBJzEpWoMsBjB8J
2026-08-03 09:53:25 +02:00
d0a5ffe515 feat(board)!: rank --plan on five ordered groups, planned above in-progress v0.20.0
Replaces the weighted score shipped in 0.19.0 with five lookups: chain-root
credit, unhandled inbox, planned, in-progress, undeclared status. Within a
group: that group's own quantity, then a Sonnet next-cost, then oldest plan.

The score's objection is accepted, not forgotten, and is written into board.sh
and CLAUDE.md so a later session reads it as decided rather than as an unfixed
defect: a group order cannot express "owes one message AND releases two others"
as one quantity. What the score could not do was hold still for the format's
second consumer - re-tuning one weight against another silently reorders a
parser in another repo, and no test here can catch that.

planned now ranks above in-progress, inverted by the same decision: converting a
decision into motion is the slow step; live work is already moving.

Debt stays uncapped and never excluded. One group below chain-root credit is not
the cap declined at 0.19.0 - the debtor keeps its tab, its most-owed-first
position, and its why=inbox:N. Pinned by a discriminating fixture the score
would fail: a root releasing one repo outranks a repo owing four.

board-selftest 134 -> 138. Suite 183 + 138 + 73 = 394.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6ULuFCPMNYAPNN3pAjsXQ
2026-08-03 06:55:21 +02:00
e5e9cbe847 feat(board): rank --plan on one score, with transitive chain-root credit v0.19.0
blocked-on was scanned and then used for a single display string; it never
reached the ordering. A blocked chain's root could therefore rank below the
repos waiting on it - measured on the real tree, the root opened at tab 14
while the two repos it held up opened at tabs 5 and 8.

--plan now ranks on one score instead of four hard buckets, because four
groups cannot express "this repo owes one message and releases two others":

    40 x repos released transitively + 15 x inbox
    +10 in-progress / +5 planned / +2 undeclared, +3 for a Sonnet row

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 - its own next step is by definition waiting. A cycle, a
blocked-on naming an unscanned repo, and a blocked repo with no target all
credit NOBODY: inventing a root there yields a plan that looks correct and
sends the operator to the wrong repo.

Debt is deliberately uncapped. A cap was proposed alongside the credit and
declined: owing a reply is the other axis from a repo's own next step, and
answering is often what unblocks a chain. The weight carries that decision -
15 per message exceeds the sum of every other bonus (13), so one message
more still outranks any combination of status and cost. At the originally
proposed 12 a tiebreak could flip the debt order, which is the cap arriving
through the back door.

blocked-on becomes a record field, inserted BEFORE neste so no existing field
index moves: neste is free prose lifted from a STATE.md and may carry a
literal '|' from a markdown table, so it stays last.

why= now names the dominant term rather than the first rule that matched, so
a block says what opening it would release instead of repeating inbox:N.

Format is otherwise unchanged for both declared consumers: tab=, repo=, dir=,
why=, status=, neste=, command=/command_missing=, paste= and the fokus_*
header all keep their names and positions.

board-selftest 118 -> 134 (chain, cycle, dangling-edge and format fixtures
written first, all failing before the change).
2026-08-02 22:38:40 +02:00
917a902172 fix(board): anchor the NESTE marker to the heading form
The awk reader matched /NESTE/ unanchored, so any prose line that merely
mentioned the word (not just the real ## NESTE heading) set the flag and the
excerpt became whatever line followed the false match, not the real next
step.

Closed by operator decision 2026-08-02: one anchor form for all repos,
^#+[[:space:]].*NESTE. Measured 26/27 repos already use the heading form;
the one exception now renders "(tom NESTE-blokk)" instead of a misleading
fragment, which is the intended, honest failure mode until it switches
over - already flagged directly and on the same shape as the existing
"MERK: N repo mangler board-linje" line.

board-selftest 116 -> 118 (new fixture proves the false-match failure before
the fix, both checks green after).
2026-08-02 20:52:17 +02:00
402597b1e5 fix(board): read emphasis on the status token, not only on the slug
The marker reader accepted **slug**: done and missed slug: **done**. That was
never a rule - it was whichever example happened to be in front of us when the
regex was written. The second form is in live use, and it makes a repo that HAS
declared look silent to --focus, which is the exact failure the held-back
report exists to surface.

Found by acting on the report's own output instead of reading it: enumerating
the held-back population turned up a repo whose declaration we were dropping
ourselves. The report blamed the repo; the defect was here.

The evidence field reports the status unwrapped - the emphasis is markdown the
operator typed, not part of the token, and "**done**" in a key=value field
reads as a value. board-selftest 114 -> 116.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:34:53 +02:00
ecbb34ae48 chore(release): prepare v0.18.0 - version bump, and the entry point --focus was missing v0.18.0
release-plugin.mjs does not bump these; it REFUSES to release until they
already match the target (release-plugin.mjs:64). The bump belongs in this
repo, not in the catalog session, so the catalog step is now unblocked:
plugin.json, package.json, README badge and all three SKILL.md frontmatters
move to 0.18.0 together, and CHANGELOG's [Unreleased] becomes [0.18.0].

More important than the numbers: --focus was reachable by nobody. The board
SKILL.md is the operator's only entry point and it documented only --plan, so
"fokusdag pa X" would have routed to an unfocused plan on a released 0.18.0.
The skill now passes the user's phrasing through VERBATIM - resolving prose to
a slug in the skill would put a guess in front of a deterministic lookup, and
fokus= already reports what the phrase resolved to. It also relays the three
disclosure lines rather than trimming them as noise, since this is the one
rendering that hides repos.

check-versions: 0 ERROR, 1 WARN (catalog ref lags plugin.json - the documented
transient state before the ref bump).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:15:52 +02:00
dd04356920 docs(changelog): correct the check count to what the suite actually reports
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:09:15 +02:00
040480890d fix(board): an empty held-back set renders a value, not a dangling colon
With nobody held back, fokus_utenfor= ended at a bare colon. sed could not
supply the placeholder it looked like it was supplying: given an empty string
there is no input line for the substitution to run on, so it silently did
nothing. That is the shape command_missing= exists to keep out of this format
- a field whose value carries nothing.

Zero repos mentioning a slug without declaring it is a real answer and now
looks like one. board-selftest 113 -> 114.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:08:32 +02:00
3fe159e75d docs(board): scrub a real topic slug out of a public-mirror comment
The worked example in the focus comment was a live slug from another repo's
private topic register. This repo mirrors publicly, and the decision file
deliberately avoids naming slugs for that reason - the code has to hold the
same line. Replaced with a shaped placeholder; behaviour untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:04:35 +02:00
9bbfe9facd docs(focus): record what shipped against the condition it was permitted under
The decision file was written before any code existed and said so in its
opening line. That line is now false, and the fix is not to delete the tense
but to name it: everything above "What shipped" is the record of what the
feature was ALLOWED to be, and the closing section reports what was built
against it.

Also records the one thing the decision could not have known, because it was
found by reading the consumer's parser rather than assuming it: paragraph mode
discards any block without tab=, so neither a '#' comment nor a bare key=value
in the header ever reaches the driver's records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 20:02:44 +02:00
f9d2c927ab feat(board): give --plan a focus cutoff that reports what it held back
--focus "<prose>" narrows the day plan to repos whose STATE.md DECLARES a
matching topic marker. Measured on the real tree: 26 blocks to 6.

The filter alone was never the feature. --plan documents that it takes one
position (the order), hides nothing, and labels what it cannot rank rather
than dropping it, so a silent cutoff would break a property the format had
already written down. The same run prints the slugs the prose resolved to,
how many blocks it removed, the repos that MENTION a resolved slug with no
marker line - named, not counted - and how many STATE.md it searched. Each
surviving block carries the declaration it survived on.

Enumerated rather than counted because the decisive find behind this feature
was invisible to every string measurement until the held-back population was
listed. "nevner", never "dekker": this reports text found, and board.sh has
no grounds for a claim about relevance. The slug vocabulary comes from the
scanned STATE.md themselves, so the "no other file" invariant survives.

Emitted as key=value, not '#' comments: the consumer's parser runs in
paragraph mode and drops any block without tab=, so a comment would reach the
operator on the terminal path and vanish on the driver path - the one case
where the cutoff is applied unseen.

board-selftest 89 -> 113. The narrowing check is comparative, since every
presence-only assertion passes against a no-op filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 19:58:25 +02:00
cb77f2889d docs(focus): record the condition --focus rests on, before it is built
A cross-repo exchange settled that --plan may grow a --focus filter only if
the same run reports the repos it held back. The decision constrains a future
feature, but it lived only in a private mailbox and a local-only STATE.md, so
nothing durable pointed at it.

The argument is an invariant, not a preference: --plan documents that it has
no cutoff and labels rather than drops, and --focus IS a cutoff. Measuring the
held-back population also turned up the find that carries the decision - a
heavy consumer of the subject, invisible to every strict-marker measurement in
the exchange, recovered only by enumerating what the filter would have hidden.

Other repos are unnamed by design: this mirror is public, and which repo
carries which malformed marker line is that repo's business.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USD9q3Fr2unp58fe8YvapW
2026-08-02 18:42:45 +02:00
50f6420da5 docs(changelog): reconstruct the missing 0.15.0 entry
The log jumped straight from 0.16.0 to 0.14.0 even though 0.15.0 shipped
and is tagged - a log gap, not a code gap, but this repo mirrors publicly
and nothing machine-checks changelog prose.

Reconstructed from v0.15.0 alone: the tag points exactly at 9dd24c3
(--brief + brief-nightly.sh). STATE.md's pointer to also use 6de2408
(the malformed-route-line test) was a stale premise - `git merge-base
--is-ancestor 6de2408 v0.15.0` says no, it landed 6 minutes after the tag
and only entered history via v0.16.0. Checked out both tags into scratch
trees and ran board-selftest.sh live to get 36 -> 49 from PASS=, not from
commit-message prose (this repo's README has printed a wrong count
before). Grepped the launchd plist for leaked absolute paths before
repeating the "placeholders only" claim: clean.

Left the published [0.16.0] entry's "51 -> 86" as-is per operator
call - the 49 -> 51 gap (6de2408's two checks, landed untagged between
the tags) is explained in the new entry instead of rewriting an
already-tagged entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017orCFDkmp88fLnqDR3chdJ
2026-08-02 16:05:53 +02:00
4080e22abd docs(ghostty): a ghost surface is detectable in one property read
The measurement identified realized surfaces by counting shell processes, which
is what was available while probing. morning-driver, building against this
document, found a cheaper and stronger test in the sdef: `working directory` is
readable on a terminal, and a surface with no shell process reports it EMPTY -
exactly the ghost signature the broken creation API produces.

That turns "has this surface been realized" from an inference into a
machine-checkable question, which is the guard anything driving Ghostty needs
before it types into a pane.

The same property answers one of the document's own open questions: it goes
empty when a shell exits and returns when the prompt is back, so "has this pane
returned to a prompt" can be polled rather than guessed at with a delay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 15:57:27 +02:00
5bea1f8651 feat(board): give the plan a paste= line the operator can actually use v0.17.0
--plan shipped with dir= and command= as separate fields. That is right for a
driver, which moves the pane itself and then types the command, and unusable
for a person: the first operator to read a block could not act on it at all.

Joining an absolute path from one field with a command from another is not a
saved line of output. It is the step where a session gets started in the wrong
repository - the same failure the absolute dir= exists to prevent, moved from
the machine to the human.

paste= is the two already joined, cd <dir> && <command>, one thing to select.
Both forms stay: the two consumers want the same fact shaped differently, and
neither shape serves both.

Emitted only alongside command=, never once per block. A `paste=cd X && ` with
nothing after it would run the cd and then a bare newline, which fails
SILENTLY - the operator ends up in the right directory with no session started
and no error to notice.

board-selftest: 86 -> 89 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 07:34:30 +02:00
6d0fe14d0f feat(board): render the day plan as a third rendering of the same scan v0.16.0
The table says what the state of every repo is; the briefing says who is
waiting. Neither says which repos to open a tab for today, in what order,
and with which command - so a day plan was assembled by hand every morning
from a table that deliberately takes no position.

--plan is built on exactly the argument --brief was: a lookup over data the
scan already holds, zero model calls, and route.sh already derives the
per-repo command. The order IS the position it takes, and the only one -
there is no cutoff, so nothing is hidden.

The rule most likely to be "fixed" into a defect: repos owing mail rank
first regardless of status. Excluding blocked or done is a claim about a
repo's OWN next step, which by definition cannot be moved, while owing an
answer is the other axis entirely - and answering is often what unblocks
it. Measured on the real tree, two of 26 planned repos were done with an
unhandled inbox, so the fixture tree pins done-with-debt and
blocked-with-debt rather than the general rule alone. Repos with no board
line rank last and are LABELLED, because the table already prints a note
about them and dropping them silently would repeat that defect.

key=value rather than prose, because the plan has two consumers: the
operator, and a separate repo driving a terminal from it. Prose would make
the rendered format an API no test here could hold stable for a consumer
living in another repo. dir= is absolute because a new pane inherits its
anchor's directory; command_missing= carries both no-command causes,
because a driver reading ^command= would type a blank line into a live pane.

route_cmd_for() becomes the single reader of the route-line grammar, shared
with --brief, distinguishing the two failure causes by exit code rather
than by an empty string. --brief output is unchanged.

Driving a terminal from the plan stays OUT of this repo, on the measurement
in docs/ghostty-orchestration-measurement.md rather than on taste.

board-selftest: 51 -> 86 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 06:40:22 +02:00
5d9c76763d docs(board): measure what Ghostty 1.3.1 can actually be driven to do
The gate question before any day-plan design: can Ghostty be scripted to open
a tab with two vertical panes, each in its own directory, with a command
running in one? Measured against the installed 1.3.1 rather than reasoned
about, because this is undocumented platform behavior.

Yes, but not through the documented API. new window / new tab / split return
correctly shaped objects that never spawn a shell - nine probe windows, shell
count flat at 29 - and every call that needs the process fails with "Terminal
surface model is not available". That is issue #12730, a 1.3.1 regression
closed as not planned, and 1.3.1 is the newest release. surface configuration
is collateral: all four of its useful properties ride that path.

What does work is perform action on an ALREADY REALIZED surface. new_tab and
new_split produce live shells (29 -> 31), and input text + send key drives
them - two panes cd'd to different repos reported distinct directories and
distinct PIDs. The working directory becomes a typed cd, so the broken path is
not needed at all.

Three constraints fell out of the measurement and are recorded because each
was found by breaking something: pane targeting must diff tab ids, since
last tab of w is not the new tab and a probe that trusted position typed into
a tab it did not create; new tabs inherit the anchor's directory; and
count of terminals is not a pane count.

Recommends splitting the work - the plan here as a third rendering of the
board scan, the Ghostty driver in a separate repo. The argument is the
measurement: a preview API that breaks in 1.4, whose documented path is
already broken, with a blast radius reaching into other repos' live sessions,
does not belong behind the mailbox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfDGWyyhnM26J4p93GSk2L
2026-08-02 06:15:47 +02:00
8e207c6c49 test(sweep): stop the --days 0 check from racing a one-second cutoff
Measured, not guessed: the check failed 7 runs in 20, not once. The cause is
a same-second collision, reproduced deterministically - a notice minted at
20260801205457 against a cutoff of 20260801205457 survives, the same notice
60s older is closed.

coord-sweep.sh is right and is left alone. Its cutoff is second-granular and
it closes strictly older messages, which spares rather than closes at the
boundary; at any real --days value one second is unobservable. Relaxing that
guard to <= would make a destructive script more aggressive to satisfy a test.

So the test was claiming what the code does not promise: that a notice minted
earlier in the same run is necessarily older at second granularity. Under a
second of work separates the two, so it was a coin flip. Aged by 5 seconds
through the existing age_it, which keeps it well inside the default 14-day
window and clear of the boundary. No sleep: that would have hidden the answer
rather than fixed it.

Selftest 182 -> 183, 20/20 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfDGWyyhnM26J4p93GSk2L
2026-08-01 23:01:32 +02:00