fix(heal-board): the board self-heal could not see the drift that mattered - #2014
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 3 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Multi-agent review roll call (CodeRabbit and Claude review automatically. Reviewers: post substantive findings only. Authors/agents: address every thread, push fixes to this branch, reply and resolve, then re-request review.) |
|
To use Codex here, create a Codex account and connect to github. |
…atch receipt
Every serial dispatch receipt has 409'd at broker registration since 2026-07-19:
ConductError: conduct broker rejected request (409):
{"detail": "session_id is already registered to another identity"}
`register()` binds agent/surface/session_id from the authenticated principal and then rejects
any re-registration whose WHOLE identity object differs. Post-binding those three are forced
equal, so the only fields that can still differ are `provider_identity` and `native_run_id` —
both client-declared. That check is therefore structurally incapable of catching an authority
mismatch (`session_principals`, two lines later, is what guards authority); it can only ever
fire on cosmetic provider drift. Since every relay call site passes a FIXED session_id literal,
a literal is claimable exactly once and the loser is refused forever.
This is the #1408 relay freeze, recurring on the fields the #1408 fix did not normalize —
`_bind_conductor_identity` carries the comment describing exactly this failure mode. Reproduced
against the real broker with ONE principal throughout:
#1 provider_identity='limen-cli' : OK
#2 same again (idempotent re-register) : OK
#3 provider_identity='limen-cli-2' : CONFLICT
#4 'limen-cli-2' again (permanent?) : CONFLICT
#5 native_run_id set, provider unchanged : CONFLICT
#6 original identity still works : OK
The freeze is per-literal, which is why the symptom looked so specific: writers using their own
literals (`harvest`, `dispatch-async/reserve`, `heal-board/lifecycle-repair`) kept landing
receipts all month — the published projection holds 177 August receipts — while the one path
that records a *dispatch* was silently dead. `logs/throughput-governor.jsonl` then read
`dispatched: 0` on every pass and pinned the jules lane in `bootstrap` at 25/day, making the
requested 100/day structurally unreachable (#1995).
Fix: a refused literal falls back once to an id keyed on exactly the identity fields the keeper
compares but does not normalize.
A FALLBACK, NOT ALWAYS-KEYED — and the first cut of this fix got that wrong. The relay session id
is not internal plumbing: the keeper stamps it into `session_id` on every projection event, so it
is part of the recorded receipt that harvest and 14 tests read. Keying it unconditionally renames
an observable identifier estate-wide to fix a condition that only arises once a literal is already
poisoned (14 tests failed on exactly that). So the healthy path keeps the stable literal and only a
refused one falls back — which also makes the fallback legible: a keyed `session_id` in a
dispatch_log entry IS the signal that its literal is frozen.
Classified on the STATUS, never the prose — ConductError's own documented contract, since three
keepers word this refusal three ways. Both 409 shapes (frozen identity, bound principal) mean one
thing here: that literal is not mine to use. Registering a different id cannot touch the session
the keeper is protecting, and every authority check still applies to whatever this client does
register. A non-conflict failure is re-raised untouched.
Verified live against the poisoned keeper: a real `limen dispatch --agent jules --limit 1 --live`
now gets PAST registration and reaches `submit()`, where it stops on a different and legitimate
409 — `task ORG-artist-organ-face-0705 exact revision moved` — because the local projection has
been frozen at `track.date: 2026-07-26` since the board-publication PR rung was retired (#2001
carries that backlog, #2014 the drift that blocks it). Stacked defects; this is one of four.
4 regression tests: the healthy path keeps the literal; a frozen literal falls back and still
relays; a non-conflict failure is never retried; and the real ConductBroker is driven through the
freeze and then through the keyed id. cli/tests/test_tabularius.py: 34 passed.
Refs #1995
There was a problem hiding this comment.
Pull request overview
Adds a “canonical” mode to the board self-heal so it can detect and reconcile lifecycle drift that exists on the keeper’s published projection (tabularius publication ref) even when the local tasks.yaml mirror is stale, and wires that reconcile into the heartbeat loop behind new parameter-panel flags.
Changes:
- Add
scripts/heal-board.py --canonicalto readorigin/tabularius/board-projection:tasks.yaml, run the existing lifecycle repairs against that canonical snapshot, and submit deltas via the authenticated relay using the canonical snapshot as the CAS base. - Wire the canonical reconcile into
scripts/heartbeat-loop.shgated byLIMEN_BOARD_CANONICAL_HEAL. - Add parameter-panel entries and CLI tests covering unreadable-ref no-op and the “local healthy vs canonical drift” structural case.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| scripts/heartbeat-loop.sh | Runs heal-board.py --canonical each beat (gated) to reconcile keeper-published drift. |
| scripts/heal-board.py | Implements canonical projection loading from a git ref and a --canonical repair path that relays against the keeper. |
| institutio/governance/parameters.yaml | Declares LIMEN_BOARD_CANONICAL_HEAL and LIMEN_BOARD_CANONICAL_REF in the parameter panel. |
| cli/tests/test_heal_board.py | Adds tests for unreadable publication ref (fail-open) and canonical-only drift detection without local writes. |
| try: | ||
| raw = subprocess.run( | ||
| ["git", "-C", str(ROOT), "show", f"{CANONICAL_REF}:tasks.yaml"], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=120, | ||
| check=False, | ||
| ) |
| try: | ||
| return LimenFile.model_validate(yaml.safe_load(raw.stdout)) | ||
| except Exception: | ||
| return None |
…tered
Every repair in this organ reads `tasks.yaml` — the LOCAL projection, which mirrors the keeper
only after a board-publication PR merges to `main`. When that merge stalls, canonical state
drifts somewhere no self-heal rung can see, and each one reports a healthy board while looking
at a stale copy of it.
Measured 2026-08-07. Twelve `needs-human`-labelled `ASK-quicken-*` tasks — login, credential,
d2l, delete, send, and seven escalations — sat at `open` on the keeper's published projection
while `main` had them correctly at `needs_human`. So this organ's own needs-human reconcile,
which exists precisely to stop the fleet re-picking a human-gated lever, read the local board,
found nothing wrong, and did nothing — while the canonical board was the one offering
delete/send atoms to agents.
The loop closes on itself: that same drift held `main` red on `validate-task-board.py`
(`needs-human-in-open`), which is what blocked the publication merge that would have refreshed
the local mirror. The drift was protecting itself from the repair.
`--canonical` runs the SAME repairs against the keeper's published projection and submits them
through the authenticated relay with that published board as the explicit compare-and-swap base,
so each ticket's precondition is computed from canonical state instead of the local mirror.
Nothing is written locally; the keeper stays the only board writer.
Read from the publication ref rather than over HTTP because that ref IS the projection contract
(the keeper publishes only through it) and a several-MB fetch has no place on a beat rung. An
unreadable ref — shallow clone, unfetched remote — makes the rung a no-op rather than a guess,
and a keeper that has published past this head simply fails the compare-and-swap so the next
pass re-derives. Fail-open like its siblings.
Dry-run against the live estate reports exactly the 12, and nothing else:
heal-board: WOULD reconcile canonical drift at origin/tabularius/board-projection:
0 reopened-done, 12 needs-human, 0 log-mismatch — needs-human: ASK-quicken-login,
ASK-quicken-credential, ASK-quicken-d2l, ASK-quicken-delete, ASK-quicken-send, ...
Wired into heartbeat-loop.sh beside the TABVLARIVS relay (the other network rung), gated on
LIMEN_BOARD_CANONICAL_HEAL. Both new parameters are declared in the panel — check-params caught
their absence, which is the registry discipline working.
NOTE: this is a loop-body edit, so the running daemon needs a kickstart to load it.
2 tests: the unreadable-ref no-op, and the structural case — a HEALTHY local board beside a
canonical board carrying the regression, where the default pass must report healthy and
`--canonical` must report the drift. cli/tests/test_heal_board.py: 6 passed.
Refs #1995
db5a54a to
2d792e9
Compare
|
Rebased onto Resolution keeps both, and the order is deliberate: Canonical heal runs first, because the drift it repairs is exactly what holds Verified post-rebase: |
…atch receipt (#2005) Every serial dispatch receipt has 409'd at broker registration since 2026-07-19: ConductError: conduct broker rejected request (409): {"detail": "session_id is already registered to another identity"} `register()` binds agent/surface/session_id from the authenticated principal and then rejects any re-registration whose WHOLE identity object differs. Post-binding those three are forced equal, so the only fields that can still differ are `provider_identity` and `native_run_id` — both client-declared. That check is therefore structurally incapable of catching an authority mismatch (`session_principals`, two lines later, is what guards authority); it can only ever fire on cosmetic provider drift. Since every relay call site passes a FIXED session_id literal, a literal is claimable exactly once and the loser is refused forever. This is the #1408 relay freeze, recurring on the fields the #1408 fix did not normalize — `_bind_conductor_identity` carries the comment describing exactly this failure mode. Reproduced against the real broker with ONE principal throughout: #1 provider_identity='limen-cli' : OK #2 same again (idempotent re-register) : OK #3 provider_identity='limen-cli-2' : CONFLICT #4 'limen-cli-2' again (permanent?) : CONFLICT #5 native_run_id set, provider unchanged : CONFLICT #6 original identity still works : OK The freeze is per-literal, which is why the symptom looked so specific: writers using their own literals (`harvest`, `dispatch-async/reserve`, `heal-board/lifecycle-repair`) kept landing receipts all month — the published projection holds 177 August receipts — while the one path that records a *dispatch* was silently dead. `logs/throughput-governor.jsonl` then read `dispatched: 0` on every pass and pinned the jules lane in `bootstrap` at 25/day, making the requested 100/day structurally unreachable (#1995). Fix: a refused literal falls back once to an id keyed on exactly the identity fields the keeper compares but does not normalize. A FALLBACK, NOT ALWAYS-KEYED — and the first cut of this fix got that wrong. The relay session id is not internal plumbing: the keeper stamps it into `session_id` on every projection event, so it is part of the recorded receipt that harvest and 14 tests read. Keying it unconditionally renames an observable identifier estate-wide to fix a condition that only arises once a literal is already poisoned (14 tests failed on exactly that). So the healthy path keeps the stable literal and only a refused one falls back — which also makes the fallback legible: a keyed `session_id` in a dispatch_log entry IS the signal that its literal is frozen. Classified on the STATUS, never the prose — ConductError's own documented contract, since three keepers word this refusal three ways. Both 409 shapes (frozen identity, bound principal) mean one thing here: that literal is not mine to use. Registering a different id cannot touch the session the keeper is protecting, and every authority check still applies to whatever this client does register. A non-conflict failure is re-raised untouched. Verified live against the poisoned keeper: a real `limen dispatch --agent jules --limit 1 --live` now gets PAST registration and reaches `submit()`, where it stops on a different and legitimate 409 — `task ORG-artist-organ-face-0705 exact revision moved` — because the local projection has been frozen at `track.date: 2026-07-26` since the board-publication PR rung was retired (#2001 carries that backlog, #2014 the drift that blocks it). Stacked defects; this is one of four. 4 regression tests: the healthy path keeps the literal; a frozen literal falls back and still relays; a non-conflict failure is never retried; and the real ConductBroker is driven through the freeze and then through the keyed id. cli/tests/test_tabularius.py: 34 passed. Refs #1995
…— `tail -1` was eating the reason (#2050) The measured defect: #2014's canonical-heal rung has been failing on EVERY beat with limen.conduct.broker.ConductError: conduct broker rejected request (500): { "detail": "Exceeded allowed rows written in Durable Objects free tier." } 61 diagnostic lines. The beat log received a bare `}`. Neither "Durable Objects" nor "rejected request (500)" appeared in ANY log file estate-wide, so twelve regressed needs-human board atoms stayed regressed behind a rung that looked like it was working. `<cmd> 2>&1 | tail -1 || true` destroys three things at once: 1. the exit status — a pipeline's `$?` is TAIL's, ~always 0, so the `|| true` is decorative and nothing can distinguish a hard failure from a clean run; 2. the diagnostic — `tail -1` of a traceback is the last line of the exception's own repr, which for an HTTP error carrying a JSON body is the closing brace; 3. the record — nothing is written down, so the failure has no reader at all. That is the "signal with no effector" class #2023 exists to close, reappearing one level up: here the SIGNAL was destroyed before any effector could exist. All 65 heartbeat-loop.sh rungs now route through the loop's own inline `beat_run` helper. Happy path is byte-identical (one line, the organ's own last line — log volume unchanged); a failure prints a `── RUNG FAIL [label] exit=N ──` banner plus the real tail; either way the outcome is appended to logs/beat-rungs.jsonl as {ts,rung,exit}. That ledger is the reader the idiom never had — labels are stable per rung (heal-board vs heal-board-canonical) because they are the streak buckets a later efficacy rung folds. `beat_run` stays INLINE so the daemon takes on no new file dependency; a missing sourced helper would kill the beat. Gate `check-beat-diagnostics` (GATES registry, pr-gate) has two rungs: · a shrink-only per-file ratchet — heartbeat-loop.sh at 0; the 8 sibling beat-script sites are a declared position in institutio/governance/beat-diagnostics-baseline.txt, and the ceiling moves up only by a reviewable hand edit, never by a flag; · helper integrity, which EXECUTES the shipped helper against a fabricated failing rung. A grep here would be as blind as the idiom it polices: removing the opening banner leaves "RUNG FAIL" intact in the closing one, so a string check passes on a helper that has already been gutted. Caught during development, which is why it runs the bytes. Verified at runtime, not by tests alone: drove the real heartbeat-loop.sh in a sandbox LIMEN_ROOT whose heal-board.py succeeds plainly and, under --canonical, fails with a traceback whose final line is `}`. The beat log now carries the quota sentence where it used to carry `}`, the succeeding rung above it is still exactly one line, and the ledger records exit=1 for heal-board-canonical across consecutive beats. Two pre-existing self-load tests broke and were fixed rather than worked around: the rung now calls beat_run, so their extraction carries the helper too — a test of the real composition instead of one where the runner does not exist. Gates: check-beat-diagnostics 0 · check-gates 0 (108 gates) · check-sensors 0 (88 sensors) · check-params 0 (the 4 new LIMEN_* vars are declared in the panel) · ruff 0 · verify-scoped.sh 0 (5511 cli + 48 api). Claude-Session: https://claude.ai/code/session_01E3fxQ3ez5DtSsEpzbScKLA Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…a registry owner, not a traceback (#2057) THE LIVE BLOCKER ON 100 TASKS/DAY. Measured 2026-08-07: POST /api/conduct/sessions answers 500 {"detail": "Exceeded allowed rows written in Durable Objects free tier."} `_register_relay_session` is on EVERY relay write path, so this single wall blocks the #2014 canonical-heal rung, dispatch receipts, and board publication at once. The keeper is the only sanctioned board writer, so while its writes are refused the board cannot advance at all: twelve needs-human `ASK-quicken-*` atoms stay regressed on origin/tabularius/board-projection and the throughput governor stays at `dispatched: 0`, bootstrap cap 25. The last successful keeper write was 2026-08-07T12:55:47-04:00 — immediately after #2016's own merge, so unblocking the write path is what spent the day's remaining allowance. Resolving it is a recurring SPEND, which is a human-gated lever, not agent work. What the fleet owes is that the condition be legible and HOMED rather than recited at the operator: · `BrokerQuotaExhausted(ConductError)` — its own type, because this is neither a bug nor a transient fault: no retry, no backoff, and no correct client code makes the next write land. A subclass, so every existing `except ConductError` keeps working unchanged. · `heal-board.py --canonical` catches it and reports ONE legible line naming its registry owner, exiting 75 (EX_TEMPFAIL). Deliberately non-zero: a tidy exit 0 here would restore exactly the "everything looks healthy" blindness that let this sit unseen, and the beat's rung ledger needs a real failure to record. · lever `L-CLOUDFLARE-DO-QUOTA` in his-hand-levers.json (issue #2054) — the durable home, with both answers spelled out: raise the plan, or decide explicitly not to and accept a board that advances once per daily reset. Either is legitimate; being the default nobody noticed is not. ONE DELIBERATE RULE EXEMPTION, stated because it contradicts a rule this repo enforces: `ConductError`'s own docstring says callers classify on `status`, never on rejection prose, because three keepers word the same condition differently. A Cloudflare storage refusal arrives as an undifferentiated 500 with no machine-readable field, so prose is the ONLY available signal. The match is kept narrow (three explicit substrings, and a marker is REQUIRED — status alone never suffices) precisely so real 500s stay real bugs; that negative direction is the more load-bearing half of the tests. The durable fix is for the keeper to answer a structured code, which is recorded on the lever as fleet-owned follow-on — it needs a wrangler deploy, which does NOT happen on merge, so it is not smuggled in here. Verified at runtime by A/B against a loopback keeper returning the exact 500 body, with the baseline extracted from origin/main and asserted NOT to contain the fix's markers first: BEFORE 62 lines, an uncaught traceback whose final line is `}` exit 1 AFTER 6 lines naming the condition, the lever, all 12 blocked atoms exit 75 Gates: ruff 0 · verify-scoped.sh 0 (5519 cli + 48 api) · no-tasks-on-me registry rung green (80 levers, all owned + traceable, no PII shapes). Claude-Session: https://claude.ai/code/session_01E3fxQ3ez5DtSsEpzbScKLA Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fail on every single beat (#2059) * feat(enactment): a third audit axis — a rung can be wired, live, and fail on every single beat The audit had two rungs. WIRING proves a declared-ON flag is actually set in the beat's wiring. LIVENESS proves the running daemon is not older than that wiring. Neither can see a rung that is wired, live, and fails every time it runs — and that is not hypothetical. Measured 2026-08-07: `heal-board.py --canonical` (#2014) failed on EVERY beat with `conduct broker rejected request (500): Exceeded allowed rows written in Durable Objects free tier` while THIS AUDIT printed "3 rung(s) green/skip". Twelve regressed needs-human atoms stayed regressed for hours behind a fully-enacted, completely ineffective rung. The first diagnosis of that was wrong and is worth recording: it looked like `LIMEN_BOARD_CANONICAL_HEAL` had simply been left out of enactment-audit.py. It had not — this audit is registry-derived, so a flag earns a row by declaring `fleet_runtime` in parameters.yaml, and that flag WAS declared and WAS resolving to 1. Adding it to a list would have proven the switch was on, which was never in doubt. What was missing was an AXIS: is the rung LANDING, not merely enabled. EFFICACY reads `logs/beat-rungs.jsonl` — the per-rung `{ts,rung,exit}` ledger that heartbeat-loop.sh's `beat_run` helper writes — and goes RED when a rung has failed LIMEN_RUNG_FAIL_STREAK_RED (3) consecutive beats. This is the reader the old `2>&1 | tail -1` idiom made impossible: it reported tail's exit status, so no record of a rung's real outcome existed for anything to consume. Three deliberate calls, each of which could have gone the other way: · Streaks are TRAILING, not total. A rung that failed twice last week and has succeeded since is healthy. Counting history would make it permanently red, and a permanently-red rung is an ignored rung. · A single failing beat is INFO, not RED. One timeout is noise; at the 120s tempo floor a real defect still trips within ~6 minutes. · Exit 75 (EX_TEMPFAIL) is reported but NEVER red. It is how a rung says "blocked on a condition already filed with a human owner" — heal-board --canonical raises it for the keeper's spent storage plan, citing lever L-CLOUDFLARE-DO-QUOTA. Making a correctly homed human-gated blocker fail this gate would hold it red until the operator acts, training everyone to ignore it, and would violate the charter's "never re-surface a filed gate". So it is named and visible without failing the audit; a failure for any other reason does. Also `--efficacy-only`, so the rung is testable against a fixture ledger without the host-dependent liveness rung flapping between CI and the live host. Verified by driving the real script across six ledger shapes — healthy (GREEN), a 3-beat streak (RED, naming the rung), a single blip (INFO), a 5-beat EX_TEMPFAIL run (INFO, not RED), failed-then-recovered (GREEN, proving the streak is trailing), and a ledger with torn and malformed lines (skipped, never fatal). All nine new cases live in the registered gate's own suite (`enactment-test`), and its case count is now an ASSERTION rather than a label — a case that stops running used to leave a cheerful summary behind covering nothing, which is the same silence this rung exists to catch one layer down. Gates: enactment-audit.test.sh 12/12 · check-params 0 (963 declared) · check-gates 0 (108 gates) · verify-scoped.sh 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3fxQ3ez5DtSsEpzbScKLA * fix(enactment): resolve the rung ledger against the DAEMON's checkout, not this script's Adopts #2053's fix rather than reintroducing the bug it removed. The efficacy rung read LIVE_ROOT, which defaults to the checkout the script lives in — per-worktree — while the daemon it reports on is host-global. Run from a session worktree (which is where the charter puts sessions, and where .claude/skills/verify tells them to run this organ), that worktree's logs/ holds no ledger, so the rung would SKIP while the real evidence sat in the live checkout. A silent SKIP where the evidence exists is the same 'I found nothing' versus 'I read nothing' confusion this lineage is about. live_checkout() already resolves it correctly and honours an explicit LIMEN_ROOT untouched; LIMEN_BEAT_RUNG_LOG still overrides everything so the fixture-driven tests are unaffected. Verified from this worktree: the rung now names /Users/4jp/Workspace/limen/logs/beat-rungs.jsonl rather than the worktree's own logs/. Suite 12/12, check-params 0, check-gates 0, ruff 0. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
check-operator-gates read BOARD = ROOT/"tasks.yaml" — the LOCAL projection, which refreshes only when a board-publication PR merges to main. heal-board.py carried the identical hole and it hid twelve regressed needs-human atoms (#2014). Measured 2026-08-09 against origin/tabularius/board-projection: tasks 3299 keeper vs 3111 mirror needs_human 126 keeper vs 109 mirror HEAL open/blk 533 keeper vs 492 mirror (162 stale / 369 live) The verdicts happened to agree — zero violations on both boards, and still ZERO of 126 needs_human tasks set by a human — but the census this gate publishes as a receipt was understated by 17. A wrong number in the artifact whose entire job is to be the number. - --canonical reads the keeper offline from its publication ref, the same idiom heal-board.py uses (LIMEN_BOARD_CANONICAL_REF). - board_tasks() returns (tasks, source) and the census carries board_source, so an unreachable keeper can never print like a clean one: it yields CANONICAL-UNREADABLE and exit 2, never [] with a green verdict. That is the same fail-open shape as the .agent-runtime bug caught pre-merge in #2143. - The default --check stays on the local mirror: hermetic, fast, no fetch dependency in CI. --canonical is the deliberate audit mode, as in heal-board. - Receipt regenerated from the keeper board. - Two regression tests: the keeper is read instead of the mirror, and an unreadable keeper surfaces as UNREADABLE rather than an empty clean board. Tests 22/22. verify-scoped.sh exit 0. ruff exit 0. Claude-Session: https://claude.ai/code/session_01E6YU33yZyHVwQp6w5TSgvv Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Layer 4 of #1995 — and the reason #2001 is correctly red.
The blind spot
Every repair in this organ reads
tasks.yaml— the local projection, which mirrors the keeperonly after a board-publication PR merges to
main. When that merge stalls, canonical state driftssomewhere no self-heal rung can see, and each one reports a healthy board while looking at a stale
copy of it.
Measured 2026-08-07. Twelve
needs-human-labelledASK-quicken-*tasks — login, credential,d2l, delete, send, and seven escalations — sat at
openon the keeper's published projection whilemainhad them correctly atneeds_human:ASK-quicken-*statusorigin/main:tasks.yamlneeds_human(12/12)origin/tabularius/board-projection:tasks.yamlopen(12/12)So this organ's own needs-human reconcile — which exists precisely to stop the fleet re-picking a
human-gated lever — read the local board, found nothing wrong, and did nothing, while the canonical
board was the one offering delete/send atoms to agents.
The loop closes on itself
That same drift held
mainred onvalidate-task-board.py(needs-human-in-open), which is whatblocked the publication merge that would have refreshed the local mirror. The drift was protecting
itself from the repair. Opening that loop is what this PR does.
(The current
reclassify-needs-human.pyclassifier is already correct — 94 KEEP / 0 FLIP / 15 REVIEW— so this is historical damage from an earlier version, not a live misclassification.)
The fix
--canonicalruns the same repairs against the keeper's published projection and submits themthrough the authenticated relay with that published board as the explicit compare-and-swap
before,so each ticket's precondition is computed from canonical state instead of the local mirror. Nothing
is written locally; the keeper stays the only board writer.
Read from the publication ref rather than over HTTP because that ref is the projection contract
(the keeper publishes only through it) and a several-MB fetch has no place on a beat rung. An
unreadable ref — shallow clone, unfetched remote — makes the rung a no-op rather than a guess, and a
keeper that has published past this head simply fails the CAS so the next pass re-derives.
Fail-open like its siblings.
Blast radius, measured against the live estate
Exactly the 12 that block #2001. Nothing else.
Wiring
Beside the TABVLARIVS relay in
heartbeat-loop.sh(the other network rung), gated onLIMEN_BOARD_CANONICAL_HEAL. Both new parameters are declared in the panel —check-paramscaughttheir absence first, which is the registry discipline working as designed.
I did not run the live
--canonicalapply from this session: the auto-mode classifier blocked thecanonical board write, correctly — a board mutation belongs to the beat's own authority, not an
interactive session. The rung is the durable form regardless.
Tests
2 added: the unreadable-ref no-op, and the structural case — a healthy local board beside a
canonical board carrying the regression, asserting the default pass reports healthy while
--canonicalreports the drift, and that the local projection is never written.cli/tests/test_heal_board.py: 6 passed.Local
pytest-clihas one unrelated environmental failure in this sandbox(
test_workstream_command.py:735— "configured remote origin is unavailable"); CI is the arbiter.Non-deploy paths.
Refs #1995