Implements I1 of docs/session-capability-invariants.md — every step of the canonical bootstrap has a fallback, or is declared irreducible. Currently graded Partial: two of four steps self-heal.
The problem
ensureMcpRegistered (#84) and syncStopHook (#88) are two bespoke implementations of one idea, written a few hours apart. A third would be a third. Worse, nothing relates the setup-script field's contents to the dispatcher's coverage — that mapping exists only as prose in .claude/README.md, so a step added to the field without a fallback is invisible until it goes missing in production, which is how #85 happened.
Shape
One manifest the dispatcher interprets. Per entry: artifact, canonical source, install target, comparison (bytes or predicate), repair action, failure wording. The dispatcher becomes a loop; mcpDriftContext generalises to "entries that could not be repaired".
The two existing repairs are deliberately not symmetric today, and the manifest has to keep both:
- MCP: compare declared-vs-registered JSON, repair by merging into
~/.claude.json.
- Stop hook: compare bytes, repair by copying. Bytes matter — the failure was a wrong file (3262 stock vs 5458), so a presence check reports health.
The gate is the point
gen-bootstrap-pin.mjs already parses the canonical setup-script text in .claude/README.md to extract PIN and the SUM_* lines. Extend that parse to enumerate the field's steps and assert each maps to a manifest entry or an explicit irreducible: declaration with a reason.
Then adding a line to the field without a fallback fails a test — which is the gap this whole thread is about.
Follow the existing idiom: bootstrap-pin.test.mjs asserts on the generator rather than reimplementing its logic, specifically so the two cannot drift. The manifest gate should do the same.
Done when
- The dispatcher contains no per-artifact repair code, only the manifest and the loop.
- Both existing repairs still pass their current tests, unchanged in behaviour.
- A step added to the canonical text with no manifest entry fails
node --test .claude/.
- The two irreducible steps (
settings.json write, CLAUDE_SESSION_ROOT prefix) are declared as such with reasons, not merely absent.
Note
This is a refactor plus a gate; it fixes no live bug. Its value is that the next #85 is caught by CI instead of by reading /tmp/env-manager.log.
Implements I1 of
docs/session-capability-invariants.md— every step of the canonical bootstrap has a fallback, or is declared irreducible. Currently graded Partial: two of four steps self-heal.The problem
ensureMcpRegistered(#84) andsyncStopHook(#88) are two bespoke implementations of one idea, written a few hours apart. A third would be a third. Worse, nothing relates the setup-script field's contents to the dispatcher's coverage — that mapping exists only as prose in.claude/README.md, so a step added to the field without a fallback is invisible until it goes missing in production, which is how #85 happened.Shape
One manifest the dispatcher interprets. Per entry: artifact, canonical source, install target, comparison (bytes or predicate), repair action, failure wording. The dispatcher becomes a loop;
mcpDriftContextgeneralises to "entries that could not be repaired".The two existing repairs are deliberately not symmetric today, and the manifest has to keep both:
~/.claude.json.The gate is the point
gen-bootstrap-pin.mjsalready parses the canonical setup-script text in.claude/README.mdto extractPINand theSUM_*lines. Extend that parse to enumerate the field's steps and assert each maps to a manifest entry or an explicitirreducible:declaration with a reason.Then adding a line to the field without a fallback fails a test — which is the gap this whole thread is about.
Follow the existing idiom:
bootstrap-pin.test.mjsasserts on the generator rather than reimplementing its logic, specifically so the two cannot drift. The manifest gate should do the same.Done when
node --test .claude/.settings.jsonwrite,CLAUDE_SESSION_ROOTprefix) are declared as such with reasons, not merely absent.Note
This is a refactor plus a gate; it fixes no live bug. Its value is that the next
#85is caught by CI instead of by reading/tmp/env-manager.log.