I1: replace the dispatcher's bespoke repairs with a manifest, gated against the canonical field text - #97
Merged
Conversation
…the field Implements I1 of docs/session-capability-invariants.md. ensureMcpRegistered (#84) and syncStopHook (#88) were two bespoke implementations of one idea, written a few hours apart, and a third would have been a third. They are now entries in one MANIFEST that the dispatcher interprets with a single loop. The gate is the point, not the refactor. Nothing related the canonical setup-script field's contents to the dispatcher's coverage of it — that mapping lived only as prose — so a step added to the field with no fallback stayed invisible until it broke in production, which is how #85 happened. parseSteps in gen-bootstrap-pin.mjs, which already parsed that same block for PIN and the SUM_* lines, now also enumerates its steps, and bootstrap-steps.test.mjs asserts each maps to a manifest entry or an IRREDUCIBLE declaration carrying a reason. Three things held onto deliberately: - The comparison stays per-entry. The Stop hook compares BYTES (its failure was a wrong file — 3262 stock against 5458 — so presence reports health); MCP compares a predicate over JSON. Forcing one comparison on both reinstates a failure that has already happened. - The parse REFUSES an unclassifiable verb rather than skipping it. A step silently dropping out of the enumeration is the exact invisibility this gate exists to remove. - settings.json and the CLAUDE_SESSION_ROOT prefix are declared irreducible with reasons, not merely absent — an omission and a decision both present as silence otherwise. The scar-tissue comments move with their repairs onto the entries they explain, since they are the reason the two are asymmetric. Verified: both existing suites pass unchanged; the dispatcher was run end to end against a fake two-repo session root, repairing a stock Stop hook byte-for-byte and an empty MCP config, and emitting the drift context block only when registration could not be repaired. The negative test is the load-bearing one — a cp step added to the canonical text with no manifest entry takes the suite from 99 pass to 98 pass / 1 fail, naming the artifact and the line. Refs #91
bdelanghe
marked this pull request as ready for review
August 3, 2026 19:00
bdelanghe
added a commit
that referenced
this pull request
Aug 3, 2026
By hand again, for #97, because the `pin` job still cannot push its bump branch (#87 — the App installation lacks `contents: write` on this repo). Verified on that merge: the job computed the identical bump, then died with "Permission to bounded-systems/.github.git denied to bounded-systems-front-desk[bot]", exit 128. #97 changed session-start-dispatch.mjs, a fetched file, so main was serving older bytes than the tree contains to any session without `.github` attached, and FRESHNESS is asserted on push — `schema` was red on main on exactly one test ("the pin is not stale") until this landed. Only the dispatcher's digest moved; register-mcp.mjs and stop-hook-git-check.sh are byte-identical at the new pin.
bdelanghe
added a commit
that referenced
this pull request
Aug 3, 2026
Two changes, one cause (#87). 1. Point at `front-desk-pin`, not `front-desk`. The fan-in entry carries no `contents`, so it could never push the bump branch, and it cannot be given any: it is deliberately unpinned and `contents` is privileged, so the broker would refuse the entry outright and take every other consumer with it. The separate pinned entry is bounded-systems/infra#172. 2. Do not use the token when the mint step FAILED. `require: contents, pull_requests` (#93) worked exactly as designed on the merge of #97 — it reported `contents(granted: absent)` and named both places the gap could live. Then `continue-on-error: true` swallowed the verdict, the job used the token anyway, and died on the very push the assertion had just said would fail. An assertion whose verdict nothing consumes is decoration. The fallback is not a downgrade: github.token holds contents:write here and pushed this branch fine before the broker was wired in (#79). So on a scope gap the branch now LANDS with the correct pin and only opening the PR is lost — a click, versus the full hand-regenerate it costs today (#79, #86, #89, #98). A third annotation separates "broker reachable, scopes insufficient" from "broker unreachable", since the two are fixed in different systems.
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #91. Implements I1 of
docs/session-capability-invariants.md, which moves from Partial to Enforced.The gate, not the refactor
ensureMcpRegistered(#84) andsyncStopHook(#88) are now entries in oneMANIFESTthe dispatcher drives with a single loop. That half fixes no live bug.The half that pays:
parseStepsingen-bootstrap-pin.mjs— which already parsed the canonical setup-script block forPINand theSUM_*lines, verified before designing around it — now also enumerates that field's steps, and.claude/bootstrap-steps.test.mjsasserts each maps to a manifest entry or anIRREDUCIBLEdeclaration with a reason. A step added to the field with no fallback now fails CI instead of going unnoticed until it breaks in production, which is how #85 happened.The parse finds exactly the four steps I1 names:
settings.jsonheredocIRREDUCIBLE— writes the pointer that invokes the dispatcher; a fallback would run before itselfCLAUDE_SESSION_ROOT=prefixIRREDUCIBLE— being wrong about the session root is the state it correctsnode register-mcp.mjsMANIFEST— predicate over JSON, repair by merging into~/.claude.jsoncp stop-hook-git-check.shMANIFEST— byte compare, repair by copyingWhat was preserved deliberately
~/.claude.jsonholds much more than any repo declares. Forcing one comparison on both reinstates a failure that has already happened here.homedir()being right for the settings dir and wrong for the session root, the stock hook crying wolf after every squash merge,"absent"being a real case because the bootstrap verifies each fetched file independently. Those moved onto the entries they explain rather than being deleted; they are why the two entries differ.bootstrap-steps.test.mjsasserts onparseStepsrather than reimplementing it, the same reasonbootstrap-pin.test.mjsasserts on its generator.Verification
.claude/, up from 99).cpstep to the canonical text with no manifest entry takes the suite to 98 pass / 1 fail, naming the artifact and quoting the offending line. Two mutants of the loop (needsRepairignoringrepairable: false; repairing unconditionally) are both caught.One precision on the issue's wording:
node --test .claude/does not resolve a dot-prefixed directory on Node 22 (Cannot find module), so the suite form isnode --test '.claude/*.test.mjs'. CI enumerates the files individually and now includes the new one.Note on the pin
session-start-dispatch.mjsis a fetched file, so FRESHNESS is red on this branch by construction —PINcannot name a merge commit that does not exist. It is reported as a diagnostic onpull_requestandorg-defaults.ymlopens the bump PR on merge, so the pin is deliberately not hand-bumped here. INTEGRITY is green.Generated by Claude Code