Skip to content

Make doctor prove a registered MCP command is a healthy CommitLore server - #636

Open
MongLong0214 wants to merge 20 commits into
mainfrom
fix-572-mcp-identity
Open

Make doctor prove a registered MCP command is a healthy CommitLore server#636
MongLong0214 wants to merge 20 commits into
mainfrom
fix-572-mcp-identity

Conversation

@MongLong0214

@MongLong0214 MongLong0214 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #572.

This fixes the live MCP identity check used by doctor. It verifies that the
registered command completes MCP initialization as CommitLore and advertises
the required read tools plus commitlore_prepare_capture,
commitlore_verify_capture, and commitlore_stage_capture.

A server that has only the two read tools is reported as a healthy
read-delivery server, but not as an unattended capture initiator. Launch,
identity, and incomplete tool sets remain distinct diagnostic outcomes.

Boundary

This verifies MCP identity and advertised tool membership only. It does not
verify asset readiness or prove that prepare_capture will succeed: required
assets such as SPEC.md may still be missing. Asset readiness is the separate
F-002 finding tracked under #633 and broader runtime convergence #635; this PR
does not close #635.

Test plan

  • npx vitest run test/doctor.test.ts
  • npx vitest run test/init.test.ts
  • Both doctor/init file orders
  • npx vitest run test/installer-hosts.test.ts
  • npx tsc --noEmit
  • Linux/amd64 dist/ rebuild and canonical-artifact manifest refresh

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 19 commits in origin/main..d53b92bfc69c936bc33d690a3cfc8231309035f8
Active constraints: not read — commitlore: git log --follow accepts exactly one pathspec, so renames are not followed for 25 paths; query one path at a time to follow its rename chain (25 changed paths)

Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR.

A registered MCP entry proves only that a host has a command string to launch.
It does not prove that the command starts CommitLore or exposes its capture tools.

Ruled-out: a separate MCP protocol probe | reuse the #595 implementation to prevent drift
Blast: module
Undo: easy
Certainty: firm
Record-Id: r-mcpid572
Provenance: authored
Verified: npx tsc --noEmit; npx vitest run test/doctor.test.ts test/installer-hosts.test.ts; npx vitest run test/doctor.test.ts -t 'unattended capture initiator'; npx vitest run test/installer-hosts.test.ts; docker linux/amd64 npm ci && npm run build; find dist -type f | wc -l
The harness stub was a fake one-line hook response that the old presence-only
check could not distinguish from a real command. The MCP probe caught it, so the
test harness now delegates to the real CLI available to the test file.

Ruled-out: retaining the fixed one-line stub | it is not an MCP server and the new probe correctly rejects it
Blast: local
Undo: easy
Certainty: firm
Record-Id: r-stub572
Provenance: authored
Verified: npx tsc --noEmit
Verified: npx vitest run test/init.test.ts
Verified: npx vitest run test/doctor.test.ts test/installer-hosts.test.ts
Verified: npx vitest run test/init.test.ts -t 'creates a portable registration|authorises unattended capture'
Verified: git diff --stat -- src/
Merging main brought in the canonical-artifact check from #605, which this
branch's macOS-built dist does not satisfy. Rebuilt with the documented
builder and regenerated the manifest so the committed bytes are the ones
that command produces.

Provenance: authored
Record-Id: r-canon572
Verified: docker linux/amd64 npm ci, npm run build, npm run artifact:manifest; artifact:verify reports 34e5f551; 277 files in dist; 101 cases across init and doctor
The synchronous wrapper was waiting for the helper to exit after its stdout
verdict. The helper only sent SIGTERM to the MCP child, so a server that stayed
alive kept the helper, and therefore spawnSync, alive until its outer timeout.

Terminate the isolated probe process group with a bounded SIGTERM-to-SIGKILL
sequence, then flush the verdict and exit the helper explicitly. The helper's
protocol timeout now remains distinct from a failure to run the probe.

Ruled-out: treating stdout as completion | spawnSync waits for helper exit after stdout
Blast: module
Undo: easy
Certainty: firm
Verified: container npm ci && npm run build && npm run artifact:manifest; container npm ci && npx vitest run test/init.test.ts test/doctor.test.ts; container npx vitest run test/init.test.ts; container npx vitest run test/doctor.test.ts; local npx tsc --noEmit; local npm run artifact:verify
Record-Id: r-probesync572
Provenance: authored
Require the registered MCP server to advertise the three capture tools as
well as its read tools. A read-only server remains a distinct healthy delivery
result, while the doctor report names asset readiness as an unverified gap.

Ruled-out: asset readiness preflight | F-002 and #633/#635 own that separate runtime check
Blast: module
Undo: easy
Certainty: firm
Verified: container npx vitest run test/doctor.test.ts (78 passed)
Verified: container npx vitest run test/init.test.ts (24 passed)
Verified: container npx vitest run test/doctor.test.ts test/init.test.ts (102 passed)
Verified: container npx vitest run test/init.test.ts test/doctor.test.ts (102 passed)
Verified: container npx vitest run test/installer-hosts.test.ts (12 passed)
Verified: container npx tsc --noEmit; macOS npm run artifact:verify; dist file count 277
Record-Id: r-initiator636
Provenance: authored
The close-stdin case asserted that either command-closed-input or
initialize-timed-out was acceptable, on the reasoning that scheduling decides
which one the probe observes. Measured on linux/amd64, it does not: the shell
reaches exec 0<&- before the parent's write lands, and the outcome was
command-closed-input 10 times out of 10.

An assertion that accepts either code stops distinguishing two different
facts. "It hung up" and "it is listening and silent" are not the same
diagnosis, and doctor exists to tell them apart. A regression that tolerates
both would pass while the probe collapsed one into the other.

Each fact now has its own fixture: a command that closes its input and stays
alive, and a command that keeps its input open and never answers. The second
takes the full probe budget, which is what makes it the timeout case.

Limit: the close-stdin outcome is measured, not guaranteed by the shell -- a platform whose shell defers the redirect past the parent's first write would produce the timeout code instead, and this case would then need its own seam rather than a looser assertion
Ruled-out: accepting either code in one case | it makes the regression pass whether or not the probe still separates the two facts, which is the property under test
Blast: local
Undo: easy
Certainty: firm
Provenance: authored
Record-Id: r-probefacts572
Verified: 10/10 runs of the close-stdin fixture through dist/core/mcp-probe.js on linux/amd64 report command closed its input; five initiator cases pass in-container with one exact probe code each, the never-answers fixture taking 6.2s against a 5s budget; tsc clean
Declaring a single runtime asset and then executing a second file is a contract the product breaks against itself.

Ruled-out: sidecar MCP helper | it is outside the declared single-file runtime
Blast: module
Undo: easy
Certainty: firm
Record-Id: r-bundleprobe636
Provenance: authored
Verified: npx tsc --noEmit; npx vitest run test/doctor.test.ts; npx vitest run test/init.test.ts; npx vitest run test/doctor.test.ts test/init.test.ts; npx vitest run test/installer-hosts.test.ts; Linux node:24-bookworm doctor and init suite; npm run artifact:verify
A stat of one extensionless candidate is not a search of PATH on Windows. Terminating a shim is not terminating what the shim started.

Ruled-out: process manager framework | taskkill is a bounded Windows helper
Blast: module
Undo: easy
Certainty: firm
Record-Id: r-winprobe640
Provenance: authored
Verified: npx tsc --noEmit; npm run artifact:verify; canonical docker build; Windows CI 31763744143 and 31764253576
PATHEXT can select a cmd shim, but Node cannot spawn that shim with shell disabled. Let Windows run the shim through its command shell so the probe reaches its MCP child.
Use the launcher's .cmd command directly now that the probe dispatches command shims through cmd. This preserves the test's actual process-tree shape while keeping the protocol streams attached to the launched server.
timeout exits immediately when its stdin is redirected by the MCP probe. A bounded loopback ping keeps the cmd wrapper alive for the protocol exchange and task-tree cleanup assertion.
Temporarily use the direct-child termination path so install-ps1 proves that the launcher descendant survives without taskkill /T.
Restore taskkill /T after the Windows cleanup control. A probe now distinguishes a reclaimed child tree from one it could not reclaim.
The CI integrity guard fingerprints ci.yml. Refresh its reviewed digest after adding the bounded Windows MCP probe checks.
The close-stdin fixture was pinned to command-closed-input on the strength of
a measurement that did not reproduce the conditions it runs in. Invoked on its
own the probe reports that code every time -- 5/5 on macOS, 10/10 on
linux/amd64. Under the suite's load it reports initialize-timed-out instead,
because the parent's write can win the race to a pipe the shell has not closed
yet.

Both are correct readings of what the probe observed. Which one happens is the
scheduler's choice, so pinning either makes the suite flaky and pinning both
would stop the pair distinguishing "it hung up" from "it is listening and
silent" -- the distinction the two fixtures exist to hold apart.

The case now asserts what does not move: the check warns and the initiator is
unhealthy. The timeout code stays pinned by the fixture that produces it
deterministically, a command that keeps its input open and never answers.

Limit: this leaves one outcome unpinned, so a regression that swapped closed-input for timed-out on that fixture would not be caught here -- the four other cases still pin their codes exactly
Ruled-out: accepting either code in both fixtures | that is what makes the pair meaningless, since neither would then hold its own fact
Ruled-out: forcing determinism by having the fixture signal readiness before closing stdin | the probe writes immediately on spawn, so there is no point at which a fixture could report ready first without changing the protocol
Blast: local
Undo: easy
Certainty: firm
Provenance: authored
Record-Id: r-proberace572
Verified: 104 cases across doctor and init pass; the probe invoked directly reports command-closed-input 5/5 on macOS and 10/10 in a linux/amd64 container, while the same fixture under the suite reports initialize-timed-out, which is the contradiction this change records
Unverified: whether the race resolves differently again on a machine with more or fewer cores -- the assertion no longer depends on it either way
Provenance: authored
Record-Id: r-canon572reb
Verified: docker linux/amd64 npm ci + build + artifact:manifest; artifact:verify reports 31b08e7c; 277 files in dist; spec/verify.sh 32 fixtures; check-readme-numbers matches bench/report.ts
The step failed at setup, not at its assertion: the probe reported
initialize-timed-out against the launcher, so `the wrapper MCP server was not
healthy before cleanup` threw before any cleanup was exercised. That leaves two
readings — the product cannot reclaim a cmd launcher's child tree, or the
fixture cannot stand one up — and the log cannot separate them, because the
precondition rested on the same Windows process handling the assertion is
about.

The wrapper used `start "" /b` and then slept. That detaches the child, so
whether the probe's `initialize` ever reached it depended on console handle
inheritance through `start`. Running node in the foreground makes the protocol
path unambiguous while keeping the tree identical: cmd.exe is still the probe's
direct child and node is still a grandchild, which is the shape #640 is about.

This does not claim the product works. It makes the next Windows run able to
say which of the two readings is true: a probe that now reports ok and a child
that survives cleanup means the product is incomplete; both green means the
fixture was.

Blast: local
Undo: easy
Certainty: tentative
Ruled-out: weaken or delete the failing step | it is the only coverage of Windows child-tree reclamation in the repository, and no unit test touches taskkill
Limit: windows-latest is the only evidence for this path; nothing here was verified locally
Provenance: authored
Record-Id: r-childtreepre640
Verified: python yaml parse of the workflow confirms the step and that the wrapper now runs node in the foreground with no start /b and no ping sleep
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant