Skip to content

I1: replace the dispatcher's bespoke repairs with a manifest, gated against the canonical field text - #97

Merged
bdelanghe merged 1 commit into
mainfrom
claude/dispatcher-repair-manifest-gate-usikb5
Aug 3, 2026
Merged

I1: replace the dispatcher's bespoke repairs with a manifest, gated against the canonical field text#97
bdelanghe merged 1 commit into
mainfrom
claude/dispatcher-repair-manifest-gate-usikb5

Conversation

@bdelanghe

Copy link
Copy Markdown
Contributor

Closes #91. Implements I1 of docs/session-capability-invariants.md, which moves from Partial to Enforced.

The gate, not the refactor

ensureMcpRegistered (#84) and syncStopHook (#88) are now entries in one MANIFEST the dispatcher drives with a single loop. That half fixes no live bug.

The half that pays: parseSteps in gen-bootstrap-pin.mjs — which already parsed the canonical setup-script block for PIN and the SUM_* lines, verified before designing around it — now also enumerates that field's steps, and .claude/bootstrap-steps.test.mjs asserts each maps to a manifest entry or an IRREDUCIBLE declaration 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:

step covered by
settings.json heredoc IRREDUCIBLE — writes the pointer that invokes the dispatcher; a fallback would run before itself
CLAUDE_SESSION_ROOT= prefix IRREDUCIBLE — being wrong about the session root is the state it corrects
node register-mcp.mjs MANIFEST — predicate over JSON, repair by merging into ~/.claude.json
cp stop-hook-git-check.sh MANIFESTbyte compare, repair by copying

What was preserved deliberately

  • The asymmetry. The comparison is per-entry. The Stop hook compares bytes because its failure was a wrong file (3262 stock against 5458), so a presence check reports health on a broken hook; MCP compares a predicate over JSON because ~/.claude.json holds much more than any repo declares. Forcing one comparison on both reinstates a failure that has already happened here.
  • The comments. This file's value is disproportionately in its scar tissue — 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.
  • Irreducible is declared, not inferred from absence. An omission and a decision both present as silence, and the test requires a reason, not a bare flag.
  • The parse refuses what it cannot classify. An unknown verb throws rather than dropping out of the enumeration — a step silently missing from the gate is the exact invisibility this removes.
  • The idiom. bootstrap-steps.test.mjs asserts on parseSteps rather than reimplementing it, the same reason bootstrap-pin.test.mjs asserts on its generator.

Verification

  • All existing suites pass unchanged (109 pass across .claude/, up from 99).
  • The negative test, which is the honest one: adding a cp step 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 (needsRepair ignoring repairable: false; repairing unconditionally) are both caught.
  • Ran end to end against a fake two-repo session root: replaced a stock Stop hook byte-for-byte, registered an empty MCP config, and emitted the drift context block only when registration could not be repaired — a repaired session still pays nothing in context.

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 is node --test '.claude/*.test.mjs'. CI enumerates the files individually and now includes the new one.

Note on the pin

session-start-dispatch.mjs is a fetched file, so FRESHNESS is red on this branch by construction — PIN cannot name a merge commit that does not exist. It is reported as a diagnostic on pull_request and org-defaults.yml opens the bump PR on merge, so the pin is deliberately not hand-bumped here. INTEGRITY is green.


Generated by Claude Code

…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
bdelanghe marked this pull request as ready for review August 3, 2026 19:00
@bdelanghe
bdelanghe requested a review from a team as a code owner August 3, 2026 19:00
@bdelanghe
bdelanghe merged commit 9536835 into main Aug 3, 2026
9 checks passed
@bounded-systems-front-desk bounded-systems-front-desk Bot moved this from Todo to Done in Front Desk Aug 3, 2026
@bdelanghe
bdelanghe deleted the claude/dispatcher-repair-manifest-gate-usikb5 branch August 3, 2026 19:03
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

I1: replace the dispatcher's bespoke repair functions with a manifest, and gate it against the canonical field text

2 participants