session-start: register MCP servers from the dispatcher when the setup script did not - #84
Merged
Merged
Conversation
…p script did not The environment setup script had been reduced to just the settings.json heredoc — 264 bytes, per the environment log — so its `register-mcp.mjs` call was gone. `~/.claude.json` read `mcpServers: null`. A session then asked "what should we work on next?", found no `next` tool, and ranked the org's work by hand from list_issues/list_pull_requests — the one move front-desk-scheduler's CLAUDE.md forbids in its opening paragraph. Nothing in the session said the tool was absent, and the answer was indistinguishable from Front Desk's. Nothing was broken that anyone could see: the dispatcher ran, every repo's hook ran, deno/dolt/node_modules were all present, and the MCP server started cleanly by hand. One line was missing from a field no reviewer and no gate can read — the infra#122 failure mode, one layer up. So the dispatcher now calls `register-mcp.mjs` itself before fanning out, and puts anything still missing into the session context — worded to head off the failure that follows a missing tool, which is not silence but a hand-rolled answer delivered with the same confidence. This works because the claim that registering after launch cannot help was only half right, and the wrong half was load-bearing. Verified live on Claude Code 2.1.42: a session that started with `mcpServers: null` gained the server's five tools within the same session, seconds after the write, no relaunch. It stays a fallback — the setup script is still the right primary call site, since it is ordered before anything reads the tool list, and a field that has stopped making that call has likely stopped doing the rest of its job. - register-mcp.mjs: extract `register()` from main so it is callable in-process; add `registrationStatus()`, the read-only report; `unregistered()` is now the one definition of "not registered", shared by the writer and the report. - session-start-dispatch.mjs: register, then report; a missing register-mcp.mjs (a refused digest leaves one file present and the other not) degrades to the previous behaviour and says "could not check", not "nothing is missing". - Refusal is preserved: an unreadable ~/.claude.json is never overwritten.
bdelanghe
marked this pull request as ready for review
August 1, 2026 14:29
bdelanghe
added a commit
that referenced
this pull request
Aug 1, 2026
By hand, because the `pin` job could not do it. Its brokered App token minted
and passed the `pull_requests: write` assertion, then 403'd one step later on
the push:
remote: Permission to bounded-systems/.github.git denied to
bounded-systems-front-desk[bot]
fatal: ... The requested URL returned error: 403
So the App installation is missing `contents: write` on this repo. The job
asserts the scope it needs to OPEN the PR but not the one it needs to PUSH the
branch, which is why this surfaced as a raw 403 rather than the named error the
step was written to produce. Filed separately; this commit just gets main green.
Also corrects a comment in the canonical bootstrap text that #84 disproved: the
setup script's `register-mcp.mjs` call is no longer the only one that can work,
so "this must happen here, not in the dispatcher" is wrong. It is still the one
ordered before launch, which is the reason to keep it — so the comment now says
that instead.
Co-authored-by: Claude <noreply@anthropic.com>
bdelanghe
added a commit
that referenced
this pull request
Aug 1, 2026
…nfra#112) (#88) Row 3 of #85's table, closed the same way #84 closed row 2. The setup script's `cp` of stop-hook-git-check.sh had gone missing along with its register-mcp.mjs call; measured 2026-08-01, $HOME/.claude held the platform's stock hook at 3262 bytes against this repo's 5458. This one outlasted the MCP break because it fails quietly. The stock hook scopes its check to `origin/<branch>..HEAD`, which after a squash merge includes GitHub's own merge commit — so it warns "Unverified" after every successful merge and advises an --amend that would rewrite already-merged history. Nothing looks broken, so nothing gets investigated, and a hook that cries wolf on every merge is worse than no hook. Compares BYTES, not presence: the failure mode here is a wrong file rather than a missing one, so `[ -f ]` would have called this healthy. Only the script is replaced — launcher-settings.json is platform-managed and rewritten, so the dispatcher swaps the file it already points at. Hooks run per event, so a copy written at SessionStart is in force from that session's first Stop. Degrades the way the rest of the file does: a refused digest can leave the dispatcher present and this file absent, and that case is reported and leaves the platform's hook alone rather than overwriting it with nothing. Note homedir() is correct here and wrong for the session root — /root/.claude really is where the user settings live, while the repos are under /home/user. That asymmetry is what sessionRootFrom exists for, so the comment says not to "fix" one to match the other. Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 1, 2026
bdelanghe
added a commit
that referenced
this pull request
Aug 1, 2026
…instances of (#90) 2026-08-01: Front Desk was reported "not working" and was broken two unrelated ways at once — an MCP server declared but never registered (#84), and a read plane returning strings where the MCP output schema wanted numbers (front-desk-scheduler#101). Neither had been noticed. The fixes are merged; this is the part that generalises. One idea, applied in six places: a capability a session depends on needs a canonical definition, a detector for drift, and a repairer or a loud failure — all three in version control. Both root causes were a missing middle term. The invariants are graded Enforced / Partial / Aspirational per positioning.md's convention, because most of them are not enforced yet and a spec that reads as if they were would be the same class of error it documents. Today's grades: one Aspirational pair (the capability probe and the preflight scope assertion), four Partial, none Enforced. Two findings worth reading even if the rest is skipped: I2 — registry-graph.yml reported success weekly for over a month while the App identity it uses has never once worked. Its script early-exits on "in sync", so success meant "nothing to do". Green from a job that did nothing is the absence of evidence reported in the same colour as evidence. I4 — the most expensive wrong sentence was an undated "cannot" in a code comment, which is why nobody had considered the fallback #84 turned out to need. A stale capability claim costs a retry; a stale impossibility claim closes a design direction and nobody reopens it. Records what is NOT claimed too: the bootstrap cannot reach zero, verified against the cloud-environment docs rather than assumed. The target is one line that fails loudly, not elimination. Co-authored-by: Claude <noreply@anthropic.com>
bdelanghe
added a commit
that referenced
this pull request
Aug 2, 2026
…nly ever prose (#94) * docs: record the session-capability invariants today's five PRs were instances of 2026-08-01: Front Desk was reported "not working" and was broken two unrelated ways at once — an MCP server declared but never registered (#84), and a read plane returning strings where the MCP output schema wanted numbers (front-desk-scheduler#101). Neither had been noticed. The fixes are merged; this is the part that generalises. One idea, applied in six places: a capability a session depends on needs a canonical definition, a detector for drift, and a repairer or a loud failure — all three in version control. Both root causes were a missing middle term. The invariants are graded Enforced / Partial / Aspirational per positioning.md's convention, because most of them are not enforced yet and a spec that reads as if they were would be the same class of error it documents. Today's grades: one Aspirational pair (the capability probe and the preflight scope assertion), four Partial, none Enforced. Two findings worth reading even if the rest is skipped: I2 — registry-graph.yml reported success weekly for over a month while the App identity it uses has never once worked. Its script early-exits on "in sync", so success meant "nothing to do". Green from a job that did nothing is the absence of evidence reported in the same colour as evidence. I4 — the most expensive wrong sentence was an undated "cannot" in a code comment, which is why nobody had considered the fallback #84 turned out to need. A stale capability claim costs a retry; a stale impossibility claim closes a design direction and nobody reopens it. Records what is NOT claimed too: the bootstrap cannot reach zero, verified against the cloud-environment docs rather than assumed. The target is one line that fails loudly, not elimination. * docs: give the invariants inbound links, and land the two that were only ever prose #90 committed docs/session-capability-invariants.md and nothing linked to it. A governing doc with no inbound edge is durable but not findable, which is the weaker half of permanent. Two of the six invariants were graded "convention, enforced by a reviewer prompt in CONTRIBUTING.md" — a file that contained no such prompt. Stating where a convention lives is not the same as putting it there, and the gap is the same shape as the bug the document is about. - CONTRIBUTING.md gains I4 (date every "cannot", and say how you checked) and I5 (say when you could not use the sanctioned path), each with the concrete cost it carried on 2026-08-01, and a link to the full set. Framed as the two habits the gates cannot enforce, so it is clear why they are prose while the rest of the org's quality properties are forcing functions. - .claude/README.md points at the doc from where anyone debugging session provisioning actually lands, with a note not to add a fourth bespoke repair before reading #91. Prose only, outside the canonical bootstrap block, so the digests are unchanged and the pin stays fresh — verified with GITHUB_EVENT_NAME=push, the mode that asserts FRESHNESS. --------- Co-authored-by: Claude <noreply@anthropic.com>
bdelanghe
added a commit
that referenced
this pull request
Aug 3, 2026
…gainst the canonical field text (#97) Closes #91. Implements I1 of docs/session-capability-invariants.md, which moves from Partial to Enforced. ensureMcpRegistered (#84) and syncStopHook (#88) were two bespoke implementations of one idea, written a few hours apart. They are now entries in one MANIFEST driven by a single loop. The gate is the point, not the refactor. parseSteps in gen-bootstrap-pin.mjs, which already parsed the canonical setup-script block for PIN and the SUM_* lines, now also enumerates that field's steps, and bootstrap-steps.test.mjs asserts each maps to a manifest entry or an IRREDUCIBLE declaration carrying 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. Held onto deliberately: the comparison stays per-entry (the Stop hook compares BYTES because its failure was a wrong file; MCP compares a predicate over JSON), the parse REFUSES an unclassifiable verb rather than skipping it, and the two irreducible steps are declared with reasons rather than merely absent. The scar-tissue comments moved onto the entries they explain — they are why the two entries are asymmetric. Verified with a negative test: 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 offending line. Two mutants of the loop are both caught. Run end to end against a fake two-repo session root.
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.
What was wrong
A session asked "what should we work on next?", found no
nexttool, and ranked theorg's work by hand from
list_issues/list_pull_requests— the one movefront-desk-scheduler's
CLAUDE.mdforbids in its opening paragraph. Nothing in thesession said the tool was absent, so the answer was indistinguishable from Front Desk's.
~/.claude.jsonreadmcpServers: null. The environment setup script had been reducedto just the
settings.jsonheredoc — 264 bytes, perRunning initialization scriptinthe environment log — so its
register-mcp.mjscall was simply gone.Nothing was broken that anyone could see. The dispatcher ran, every repo's hook ran,
deno/dolt/node_moduleswere all present, and the MCP server started cleanlywhen invoked by hand. One line was missing from a field no reviewer and no gate can
read — the
infra#122failure mode, one layer up from where this directory usuallyargues about it.
What changed
The dispatcher now calls
register-mcp.mjsitself before fanning out, and puts anythingstill missing into the session context.
This works because the claim this repo carried — that registering after launch cannot
help, since servers resolve at launch — was only half right, and the wrong half was
load-bearing. Verified live on Claude Code 2.1.42: a session that started with
mcpServers: nullgained the server's five tools within the same session, secondsafter the write, with no relaunch. The config is watched. Launch-time resolution is real
and still the ordering you want; it is not the only door.
It stays a fallback, not a relocation. The setup script is still the right primary
call site — it is ordered before anything reads the tool list — and a field that has
stopped calling
register-mcp.mjshas probably stopped doing the rest of its job, sostepping in is logged as a bug report against that field, not as a healthy session.
Files
register-mcp.mjs— extractregister()frommainso it is callablein-process; add
registrationStatus(), the read-only report.unregistered()is nowthe single definition of "not registered", shared by the writer and the report, so a
drift check cannot come to disagree with what the writer would do.
session-start-dispatch.mjs— register, then report. A missingregister-mcp.mjs(the bootstrap verifies each fetched file independently, so arefused digest can leave one present and the other not) degrades to the previous
behaviour and logs "could not check" — a different claim from "nothing is missing".
README.md— record the 2026-08-01 recurrence, correct the launch-time claim, andfix two log-line examples that no longer match the code.
The warning block is deliberately repo-agnostic (which CLI substitutes for which server
is the declaring repo's knowledge) and is emitted only when registration did not
take — the healthy and self-healed paths cost zero context, since the org context file's
own header notes it is charged every session.
Verification
.claudesuites, including the bootstrap-pin gate.nothing registered → self-heals and warns on stderr; re-run → no-op; unreadable
~/.claude.json→ refused, file untouched, warning reaches session context; andregister-mcp.mjsabsent → degrades cleanly.FRESHNESSis not asserted on a PR, andorg-defaults.ymlopens the bump automatically once a merge commit exists.
Still needs a human
The setup-script field itself lives outside version control and I cannot edit it. It
should be returned to the canonical text in
.claude/README.md— the missing call is:This PR means a session survives without it; it does not make it unnecessary.
Generated by Claude Code