Skip to content

session-start: register MCP servers from the dispatcher when the setup script did not - #84

Merged
bdelanghe merged 2 commits into
mainfrom
claude/front-desk-not-working-6993hp
Aug 1, 2026
Merged

session-start: register MCP servers from the dispatcher when the setup script did not#84
bdelanghe merged 2 commits into
mainfrom
claude/front-desk-not-working-6993hp

Conversation

@bdelanghe

Copy link
Copy Markdown
Contributor

What was wrong

A session 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, so the answer was indistinguishable from Front Desk's.

~/.claude.json read mcpServers: null. The environment setup script had been reduced
to just the settings.json heredoc — 264 bytes, per Running initialization script in
the environment log — so its register-mcp.mjs call was simply gone.

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
when invoked by hand. One line was missing from a field no reviewer and no gate can
read — the infra#122 failure mode, one layer up from where this directory usually
argues about it.

What changed

The dispatcher now calls register-mcp.mjs itself before fanning out, and puts anything
still 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: null gained the server's five tools within the same session, seconds
after 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.mjs has probably stopped doing the rest of its job, so
stepping in is logged as a bug report against that field, not as a healthy session.

Files

  • register-mcp.mjs — extract register() from main so it is callable
    in-process; add registrationStatus(), the read-only report. unregistered() is now
    the 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 missing
    register-mcp.mjs (the bootstrap verifies each fetched file independently, so a
    refused 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, and
    fix 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

  • 79 tests pass across the four .claude suites, including the bootstrap-pin gate.
  • 11 new tests, plus end-to-end runs of the dispatcher against a scratch session root:
    nothing registered → self-heals and warns on stderr; re-run → no-op; unreadable
    ~/.claude.json → refused, file untouched, warning reaches session context; and
    register-mcp.mjs absent → degrades cleanly.
  • The pin is untouched: FRESHNESS is not asserted on a PR, and org-defaults.yml
    opens 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:

if [ -f "$BOOT/register-mcp.mjs" ]; then
  CLAUDE_SESSION_ROOT="$ROOT" node "$BOOT/register-mcp.mjs" || true
fi

This PR means a session survives without it; it does not make it unnecessary.


Generated by Claude Code

…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
bdelanghe marked this pull request as ready for review August 1, 2026 14:29
@bdelanghe
bdelanghe requested a review from a team as a code owner August 1, 2026 14:29
@bdelanghe
bdelanghe merged commit a7cb962 into main Aug 1, 2026
8 checks passed
@bdelanghe
bdelanghe deleted the claude/front-desk-not-working-6993hp branch August 1, 2026 14:30
@bounded-systems-front-desk bounded-systems-front-desk Bot moved this from Todo to Done in Front Desk Aug 1, 2026
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>
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.
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.

2 participants