github mksglu/context-mode v1.0.126

2 hours ago

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)

#548ctx_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.

#550start.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 in src/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" check

If 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.

Don't miss a new context-mode release

NewReleases is sending notifications on new releases.