v1.0.126
Architectural defenses for #548 + #550 — every fix expressed as a registry / contract / interface, not adapter-specific patch. Adding adapter #16 inherits the defenses automatically. 6 algorithmic mechanisms ship together as a single release.
What broke (and why per-adapter patches weren't enough)
#548 — ctx_doctor reports hook scripts FAIL on Windows + space-in-username
@jasonwujch (and @jstan2525 via PR #549) hit the same root cause: extractHookScriptPath's third regex alternative (\S+\.mjs) silently grabbed the path tail after the last whitespace when commands weren't quoted. Doctor printed FAIL paths like …\1.0.124\Wu\.claude\…\pretooluse.mjs even though every script was actually present.
PR #549 in v1.0.125 added quotes to claude-code's generateHookConfig — surgical, but left the structural defect intact (other adapters route through buildNodeCommand; only claude-code hand-rolled raw template literals; a future adapter author could repeat the mistake). v1.0.126 closes the class of bug, not just the instance.
#550 — start.mjs boots without server.bundle.mjs (partial install)
@rpickmans hit a state where ~/.claude/plugins/cache/context-mode/context-mode/1.0.123/ was missing critical bundle files. Tarball was verified clean (post-mortem: every npm tarball v1.0.120-v1.0.125 contains both start.mjs and server.bundle.mjs); his cache was corrupted by something local-side (interrupted extract / marketplace puller race / AV quarantine — unproven without his install logs).
The architectural defect on our side: start.mjs had no boot-time invariant on its sibling tree. A missing server.bundle.mjs produced a downstream import failure that surfaced as opaque "MCP server failed to start". v1.0.126 makes the invariant explicit and derives it from package.json files[] so the next file added to the tarball auto-extends coverage.
The 6 algorithmic defenses
Algo-D1 — HealthCheck protocol on HookAdapter
src/adapters/types.ts adds optional getHealthChecks?(pluginRoot: string): readonly HealthCheck[] to the HookAdapter interface. HealthCheck = { name: string; check: () => { status: "OK" | "FAIL"; detail?: string } }. claude-code overrides with hook-script existence checks using DIRECT existsSync(join(pluginRoot, "hooks", scriptName)) — no regex round-trip. src/cli.ts doctor iterates adapter.getHealthChecks?.(pluginRoot) ?? [] and prints each.
Adapter #16 with hook scripts inherits the contract; adapters that don't have this class of check (the other 14 today) get nothing — opt-in by override, no 14-way fan-out.
Algo-D2 — parseNodeCommand as strict inverse of buildNodeCommand
src/adapters/types.ts gains parseNodeCommand(cmd: string): { nodePath: string; scriptPath: string } | null, the inverse of the existing buildNodeCommand. Returns null for any command that didn't come from buildNodeCommand — no silent fallback. Both extractHookScriptPath definitions (src/util/hook-config.ts:24 AND src/adapters/claude-code/hooks.ts:178 — the twin definition tech debt called out in the original PRD §6) collapse to thin wrappers around parseNodeCommand.
The same architectural shape as v1.0.125's is_exact_matcher — one canonical (de)serializer replaces multiple regex flavors. Adding adapter #16: if it uses buildNodeCommand (enforced by Algo-D3.5 below), parseNodeCommand already understands its output.
Algo-D3 — claude-code adapter migrates to buildNodeCommand
5 raw template literals in src/adapters/claude-code/index.ts:115,129,140,151,162 replaced with buildNodeCommand() calls. claude-code stops being the lone outlier; with all 15 adapters on buildNodeCommand, structural drift is gone. PR #549's quote-fix from v1.0.125 was the symptom patch; this is the structural cleanup.
Algo-D3.5 — CI invariant locks claude-code on buildNodeCommand
Test in tests/adapters/claude-code.test.ts algorithmically scans src/adapters/**/*.ts and asserts no string literal matches /^"?node\s+["']?\$\{/ (raw template forming a node command). Adapter #16's first attempt to write node "${pluginRoot}/hooks/X.mjs" directly fails CI — the trap is closed at build time, not at runtime when a Windows user with HOME-spaces hits it.
Algo-D4 — start.mjs integrity check derived from package.json files[]
scripts/plugin-cache-integrity.mjs (new, raw .mjs so start.mjs can import without TS toolchain at boot) walks package.json files[], expands directories recursively, and verifies every expected file via existsSync(join(pluginRoot, name)). On any missing → start.mjs writes a structured CONTEXT_MODE_PARTIAL_INSTALL stderr block and process.exit(2).
Single source of truth: the same files[] field that drives npm pack. Adding a new tarball entry auto-extends integrity coverage. start.mjs is the universal MCP entry point — one file change protects all 15 adapters.
Algo-D5 — Doctor surfaces D4 invariant via getHealthChecks()
src/util/plugin-cache-integrity.ts (new, TS bridge to the .mjs helper for typed sync access — required because HealthCheck.check() is sync and TS consumers need typed exports). claude-code's getHealthChecks() (from D1) includes a "Plugin cache integrity" check that calls the bridge. Boot fail-fast (D4) and doctor diagnostic (D5) agree byte-for-byte because both call the same exported function.
Why algorithmic, not per-adapter
v1.0.124 (#545 env-leakage) went algorithmic with PLATFORM_ENV_VARS registry → 15×14×3 leak-matrix; v1.0.125 (#547 Codex regex) added is_exact_matcher charset + drift-guard test; v1.0.126 follows the same playbook for the doctor / start.mjs / hook command class.
The pattern: when a failure class can recur across N adapters, the fix is a registry / contract / invariant test that adapter #16 inherits automatically — not 15 manual edits we hope future contributors remember.
Tests
3202 pass · 8 pre-existing baseline failures (OpenCodeAdapter config-paths — verified present continuously since v1.0.122, unrelated)
- 16 new tests in
tests/adapters/claude-code.test.ts(D2 + D3 + D3.5 + D1 + D5 — extends existing file per CONTRIBUTING L275 "no new test files" rule) - 6 new tests in
tests/core/cli.test.ts(D1 doctor wiring + D4 helper + start.mjs integration + package.json wiring) - ZERO new test files created (CONTRIBUTING compliance)
- ZERO changes to 14 non-claude-code adapter files (apart from the optional
getHealthChecks?interface addition insrc/adapters/types.ts)
Compatibility
15 adapters / 3 OS. Zero observable behavior change for any healthy install. Strict mode is opt-in via getHealthChecks? override (default undefined = legacy doctor flow). Boot-time integrity check fails fast on partial installs — this is fail-fast for already-broken state, never triggers on healthy installs.
| Adapter | Effect of v1.0.126 |
|---|---|
| claude-code | Doctor uses getHealthChecks + direct existsSync (no regex round-trip). Hook commands now via buildNodeCommand (parity with 14 others).
|
| 14 other adapters | Zero observable change. Optional interface, no override. |
| All 15 (universal) | start.mjs partial-install fail-fast at boot; package.json files[] derived.
|
Upgrade
npm install -g context-mode@latest
# inside Claude Code / Codex / etc:
/ctx-upgrade
# restart your session
/ctx-doctor # see new "Plugin cache integrity" checkIf your install was previously broken (rpickmans-class missing files), upgrade brings start.mjs's fail-fast online. Future partial states emit a structured CONTEXT_MODE_PARTIAL_INSTALL stderr block with the exact missing files and a remediation command — no more silent MCP boot failures.
Thanks
@jasonwujch + @jstan2525 for the precise #548 / PR #549 diagnoses (regex round-trip + tail-after-space — exactly the silent validation path the algorithmic fix targets). @rpickmans for #550 + the architectural suggestion to gate publish on bundle integrity (already in place — but his issue motivated D4 fail-fast at the consumer side too). Every report this cycle came with line-level citations or empirical reproductions; the algorithmic redesign is built on top of that ground truth.