22 Commits

Author SHA1 Message Date
Mert Koseoglu e918eac6c1 fix(windows): stop boot-install EPERM + cmd-window flash from shell:true dropping cwd (#861)
The background install of the three optional fetch deps (turndown,
turndown-plugin-gfm, @mixmark-io/domino) spawned npm with `shell: IS_WIN32`.
On Windows + Node, `shell: true` DROPS the `cwd` option, so the spawned cmd.exe
runs in an arbitrary working dir (C:\Windows under Claude Code). `npm install`
then tries to create `C:\Windows\node_modules` -> EPERM on every MCP boot, and
a cmd.exe window flashes each time; the install never persists, so the
existsSync guard never short-circuits and it re-fires every session (reported
by @lravizzoni with npm-debug-log evidence).

Prefer running npm's own CLI through node directly (no `.cmd` shim, no shell):
resolve `npm-cli.js` beside `process.execPath` and spawn it with
`shell: false` (honors `cwd`) + `windowsHide: true` (no console window). Fall
back to the `npm.cmd` shim only when npm-cli.js can't be located, so a working
host — e.g. a POSIX layout where npm-cli.js isn't beside node — never regresses
(POSIX already used shell:false, so its behavior is unchanged). Also surface
spawn failures and non-zero exits to stderr; this EPERM was invisible for
months behind stdio:"ignore" + an empty error handler.

Tests: structural regression pins the node/shell:false/windowsHide/fallback
contract (regex-free, matches the start.mjs test pattern); a portable behavioral
test pins the runtime property the fix relies on (shell:false honors cwd). #634
background-install contract preserved. Needs Windows CI / on-device confirmation.
2026-06-22 16:53:14 +03:00
Mert Koseoglu dfff7b873f fix(lifecycle): propagate client death through the Linux re-exec proxy (#862)
On Linux, start.mjs re-execs under Bun to dodge the better-sqlite3 SIGSEGV
(#564); the node proxy forwards stdin to the Bun child. Its stdin EOF handler
was a no-op and the proxy parked forever, so when the MCP client exited the
proxy never closed the child's stdin nor exited — the child's direct parent
stayed alive (defeating its ppid watchdog) and its stdin never EOF'd, leaving
an orphaned pair pinning a CPU core at PPID=1 (reported by @elhoim: 5 orphans,
~3 of 6 cores).

The proxy now tears the child down on stdin end/close/error: forward EOF for a
graceful self-reap via the child's own lifecycle guard, then escalate SIGTERM
(2s) -> SIGKILL (5s) so a wedged child can never outlive its client. Re-exec
under Bun is unchanged (#564 preserved). The escalation timers are deliberately
not unref'd so teardown liveness never depends on the top-level await surviving
a refactor.

Behavioral e2e (real proxy bytes, old-vs-new x graceful-vs-wedged): old leaks
the child; new reaps it in ~4ms graceful / 5s wedged. Source-introspection test
pins the contract; lifecycle + start.mjs suites green.
2026-06-22 16:45:18 +03:00
Mert Koseoglu 1aee4808d0 fix(install): derive version from package.json across all manifests so none bake stale (#768) 2026-06-21 18:37:32 +03:00
Mert Koseoglu 73cf07e015 docs/tests: update adapter count 15→17 (copilot-cli + antigravity-cli) 2026-06-21 17:48:58 +03:00
Ken Jo 9f34c6f11b Add GitHub Copilot CLI + Antigravity CLI (agy) support (#787)
* feat(adapters): add Antigravity CLI (agy) + GitHub Copilot CLI support

Add two agentic CLI adapters onto next's existing adapter registration —
without the abandoned PR's setup subcommand / consolidated registry.

Antigravity CLI (agy):
- MCP + capture-only PostToolUse hook adapter (agy honors no stdout veto in
  auto-run mode; verified against agy 1.0.5). The agy hook payload
  {conversationId, toolCall, workspacePaths} is mapped onto the shared
  capture pipeline.
- Ships a Claude-layout plugin bundle (configs/antigravity-cli/) installed via
  `npm run install:agy` (mirrors install:openclaw), with a version-skew
  capture-hook probe in the installer.

GitHub Copilot CLI (1.0.59):
- json-stdio hook adapter with six events: PreToolUse, PostToolUse, PreCompact,
  SessionStart, UserPromptSubmit, Stop. Overrides CopilotBaseAdapter to emit the
  FLAT {type,command} + top-level "version": 1 hook config Copilot CLI requires.
- MCP install via `copilot mcp add context-mode -- context-mode`.
- Fix a latent Stop-hook bug: a session_end event with no `data` threw inside
  insertEvent (createHash(undefined)) and was silently dropped.

Cross-cutting:
- #774: probe agy/copilot config markers before the generic ~/.claude check.
  The copilot marker is narrowed to context-mode-written files
  (~/.copilot/mcp-config.json | hooks/context-mode.json), not a bare ~/.copilot/
  dir, so a co-installed-but-unconfigured Copilot CLI cannot steal detection
  from a Claude Code user.
- Dispatcher fails OPEN (exit 0) on a missing hook script: GitHub Copilot CLI
  treats an exit-1 PreToolUse hook as DENY, so a version skew (a newer adapter's
  hook command on an older global) would otherwise brick the agent.

Fixes #774. Fixes #775.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: regenerate bundles for antigravity-cli + copilot-cli support

Picks up the new HOOK_MAP entries, client-map keys, validPlatforms,
getSessionDirSegments cases, and the fail-open dispatcher into the
esbuild-generated runtime bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(platform-support): sync support docs to 18 platforms + fix stale Kiro classification

Make README.md and docs/platform-support.md internally consistent and aligned
with the adapter source of truth.

Header sync (18 platforms everywhere):
- The Main Comparison Table (was 11 cols), the Capability Matrix (was 11), and
  the README Platform Compatibility table (was 17, missing Kimi Code) now list
  the SAME 18 platforms in one shared order. Adds the two branch-new platforms
  (GitHub Copilot CLI, Antigravity CLI `agy`) plus previously-omitted Qwen Code,
  KiloCode, OpenClaw, Zed, Pi as columns. Each cell sourced from the per-platform
  detail sections / adapter source and independently verified.
- Fix five ragged rows in the Main Comparison Table (a dropped trailing OMP cell)
  and add CLI Hook Dispatcher rows for qwen-code + copilot-cli.
- GitHub Copilot CLI section: normalize the `**Hook Names:**` label and add the
  missing `**Output Modification:**` field for json-stdio-family parity.

Fix stale Kiro classification (code is the source of truth):
- The kiro adapter is json-stdio with working preToolUse/postToolUse hooks
  (hooks/kiro/{pretooluse,posttooluse}.mjs + a kiro HOOK_MAP entry), yet the docs
  called it "MCP-only (Phase 2 — not implemented)" and the README contradicted
  itself ("no hook support" in one place, "native preToolUse/postToolUse" in two
  others).
- Reclassify Kiro as json-stdio with PreToolUse + PostToolUse + exit-code-2
  blocking across the Overview paradigm table, both wide tables, the dispatcher
  table, and the Kiro detail section; document that agentSpawn (SessionStart) and
  stop are not yet wired, so session restore after compaction is unavailable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(antigravity-cli): drop vestigial .mcp.json dependency that broke fresh clones

The agy plugin-bundle test asserted configs/antigravity-cli/.mcp.json, but
.mcp.json is gitignored repo-wide and was never committed — so the test passed
on the dev machine (file present locally) yet failed on a fresh clone with
ENOENT. Committing the file is the wrong fix: the .gitignore comment documents
that shipping .mcp.json has silently broken fresh installs before (#253/#531).

- The bundle declares MCP the Claude way via .claude-plugin/plugin.json
  mcpServers (committed — the mechanism agy reads on `agy plugin install`),
  mirrored by the agy-native mcp_config.json (committed). Remove the vestigial
  bundle .mcp.json and stop the test + docs from requiring it. Every file the
  plugin test reads is now git-tracked, so a fresh clone passes.
- README: Kiro was still grouped under "Non-hook platforms" in the routing-
  enforcement note. Kiro has native preToolUse/postToolUse hooks; it needs the
  manual KIRO.md copy only because agentSpawn/SessionStart is not yet wired.
  Reword to say so.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(adapters): cross-platform agy installer + copilot-cli COPILOT_HOME parity

Windows fix (real): replace the bash-only agy plugin installer with a
cross-platform Node script so `npm run install:agy` runs natively on Windows
(PowerShell/cmd), not just Git Bash/WSL. agy runs on Windows, so its installer
must too — the old `node -e` wrapper hard-exited 1 on win32. openclaw stays
bash-only (it is genuinely POSIX-only). Removes
scripts/install-antigravity-cli-plugin.sh in favor of
scripts/install-antigravity-cli-plugin.mjs (same preflight + version-skew probe).

copilot-cli hardening (COPILOT_HOME edge case only — the default ~/.copilot
install was and remains correct):
- CopilotCliAdapter.getSessionDir() now roots at getConfigDir() (COPILOT_HOME-
  aware), mirroring codex/kimi, so the TS server reads sessions from the same
  place the hook runtime (COPILOT_OPTS configDirEnv: COPILOT_HOME) writes them.
  Previously a relocated COPILOT_HOME split hook writes ($COPILOT_HOME/...) from
  server reads (~/.copilot/...), making sessions appear empty.
- detect.ts copilot-cli marker honors COPILOT_HOME, not just ~/.copilot.

No change to the default (COPILOT_HOME-unset) behavior; a regression test pins
both the ~/.copilot default and the COPILOT_HOME-rooted path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: regenerate bundles for copilot-cli COPILOT_HOME parity

Picks up CopilotCliAdapter.getSessionDir() and the COPILOT_HOME-aware detect.ts
marker into the esbuild runtime bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(antigravity-cli): installer registers the MCP server (agy plugin install skips it)

`npm run install:agy` ran only `agy plugin install`, which — verified against
agy 1.0.5 — processes a bundle's skills + hooks but logs "mcpServers : skipped
(not found)" and registers NO MCP server. agy reads a plugin's MCP only from a
bundle `.mcp.json` (intentionally not shipped — gitignored repo-wide after
#253/#531) and has no `agy mcp add` command, so context-mode's MCP server was
never registered: users had to add it to ~/.gemini/config/mcp_config.json by hand
(reported on Windows; reproduced on Linux: `mcpServers : skipped (not found)`).

The installer now also writes context-mode into agy's GLOBAL MCP profile
~/.gemini/config/mcp_config.json (idempotent JSON merge, preserves other servers,
tolerates a malformed file) — the file agy actually loads and `context-mode
doctor` checks. Verified end-to-end on agy 1.0.5: `npm run install:agy` →
mcp_config.json gains context-mode → `agy -p "... ctx_execute ... 7 + 5"` → 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(server): emit Gemini-safe tool schemas so agy/Gemini CLI expose ctx_* tools

Antigravity CLI (agy) and Gemini CLI use Gemini's function-calling API, which
rejects JSON Schema `const` and `additionalProperties`. When a tool's parameter
schema contains either, the host SILENTLY DROPS that tool from the model's
function list — so agy never sees the ctx_* tools and works around them by
hand-rolling the MCP protocol through its Bash tool (verified on Windows: agy
wrote scratch/call_ctx_stats.js + list_mcp_tools.js MCP clients instead of
calling the tools natively). That defeats the point of context-mode — bash
output floods the context window instead of staying in the sandbox.

context-mode builds schemas with Zod, which emits `const` (from coerce/preprocess
constructs) and `additionalProperties`, with no Gemini sanitization. Wrap the
SDK's tools/list handler to rewrite the EMITTED schema:
  - `const: X` -> `enum: [X]`   (an identical single-value constraint)
  - drop `additionalProperties` (advisory-only; every ctx_* handler parses args
    with Zod, which strips unknown keys server-side regardless)

Both transforms are behavior-preserving for every other client (Claude Code,
Copilot, Cursor): const and a one-value enum are equivalent, and no model sends
undeclared properties — only the wire schema changes, never validation or how a
tool is called. Best-effort: if the MCP SDK internals shift, the original handler
is left untouched (no regression). Verified on the real tools/list: all 11 ctx_*
tools now emit 0 `const` / 0 `additionalProperties`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: regenerate bundles for Gemini-safe tool schema sanitizer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(antigravity-cli): clear agy's stale MCP tool-schema cache on install

agy caches each MCP server's tool schemas under
~/.gemini/antigravity-cli/mcp/<server>/ and does NOT refresh them on reconnect
(verified on agy 1.0.6 against a live Windows install). A cache captured by a
context-mode older than the Gemini-safe-schema fix (ae6e7d3) keeps the
`const` / `additionalProperties` schemas that make Antigravity CLI silently drop
the ctx_* tools from the model's function list — so the schema fix never reaches
the model and the agent keeps working around the tools via shell scripts.

The installer now clears that cache after registering the MCP server, so agy
re-fetches the current Gemini-safe tools/list on its next launch. Verified on
Windows: clearing the cache + reconnecting makes agy re-store ctx_execute.json
with 0 `const` / 0 `additionalProperties`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document agy Gemini-safe schemas + installer cache-clear + copilot COPILOT_HOME

Reflect this branch's recent behavior changes in the support docs:
- agy: context-mode emits Gemini-safe tool schemas (const->enum, additionalProperties
  stripped) so Antigravity CLI exposes the ctx_* tools instead of silently dropping
  them; agy caches tool schemas and never refreshes them, so `npm run install:agy`
  clears that cache. Added to the agy Known Issues + install steps (platform-support.md
  + README).
- copilot-cli: COPILOT_HOME now relocates the session-DB root too (getSessionDir honors
  it), and the detection marker honors COPILOT_HOME.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: correct GitHub Copilot CLI plugin capability (plugins DO support MCP + hooks)

The README + platform-support docs claimed Copilot CLI plugins register only
skills/agents — not MCP servers or hooks. That's wrong: `copilot plugin --help`
and `copilot mcp --help` (Copilot CLI 1.x) confirm a plugin can register MCP
servers (a `.mcp.json` in the plugin root or `.github/mcp.json`) and hooks
(`hooks.json`), installed in one command via `copilot plugin install owner/repo:path`
(from a GitHub repo subdirectory, no clone). The "direct installs deprecated for
plugin@marketplace" note was also inaccurate (all source forms are current).

Corrected both docs. context-mode still registers via `copilot mcp add` +
`context-mode upgrade` today; a shippable Copilot plugin bundle
(configs/copilot-cli/) is noted as a planned follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(copilot-cli): ship a GitHub Copilot CLI plugin bundle (MCP + skill, phase 1)

`copilot plugin install mksglu/context-mode:configs/copilot-cli` registers the
context-mode MCP server + routing skill in one command — no `context-mode
upgrade` / agent call.

The bundle's .mcp.json pins CONTEXT_MODE_PLATFORM=copilot-cli so the server
self-identifies as Copilot. This fixes the detection trap where a co-installed
Claude Code (~/.claude/plugins/installed_plugins.json) makes standalone
`context-mode upgrade` — and even ctx_upgrade — resolve claude-code and write
Claude's config instead of Copilot's.

Real Copilot plugins discover MCP from a root `.mcp.json`, so this is the one
bundle whose .mcp.json is committed: .gitignore un-ignores exactly this path
(the repo-wide ignore from #253/#531 guards the repo-ROOT dev file, not a
plugin's own config).

Phase 2 (capture hooks via the plugin's hooks.json) follows once its format is
verified on Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(copilot-cli): add capture hooks to the Copilot CLI plugin bundle (phase 2)

configs/copilot-cli/hooks.json registers all six Copilot hook events
(PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop, PreCompact),
each dispatching `context-mode hook copilot-cli <event>` against the global
binary. It is byte-equivalent to what `context-mode upgrade` writes to
~/.copilot/hooks/context-mode.json (the format verified against the
@github/copilot binary), so `copilot plugin install …:configs/copilot-cli` now
registers MCP + skill + capture hooks in one command — no `upgrade` / agent call.

Verified on Windows: with the plugin's env-pinned MCP config + a current global
context-mode, Copilot calls ctx_execute (→ 12) and ctx_upgrade resolves
copilot-cli (writes the Copilot hook, leaves Claude Code's config untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(copilot-cli): document the plugin bundle as the recommended install

README + platform-support now lead with `copilot plugin install
mksglu/context-mode:configs/copilot-cli` (one command: MCP + hooks + skill, no
upgrade/agent call), keeping `copilot mcp add` + `context-mode upgrade` as the
manual no-plugin path. Notes the .mcp.json env pin (CONTEXT_MODE_PLATFORM=
copilot-cli) that fixes detection under a co-installed Claude Code, the
.gitignore un-ignore for the bundle's .mcp.json, and the `copilot --plugin-dir`
local-test path. Drops the earlier "planned follow-up" wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(antigravity-cli): ship .mcp.json so `agy plugin install` registers MCP directly

The agy bundle declared MCP in two places that nothing consumed — a `mcpServers`
block in .claude-plugin/plugin.json (which `agy plugin install` SKIPS) and a dead
mcp_config.json (read by no code) — and relied on the installer writing agy's
GLOBAL ~/.gemini/config/mcp_config.json as a workaround for not shipping .mcp.json.

agy's plugin system is Claude-compatible and reads MCP from a bundle `.mcp.json`,
exactly like the Copilot bundle. Verified on agy 1.0.6: `agy plugin install` with
a bundle .mcp.json logs "mcpServers : 1 processed" and registers the server (env
preserved) into ~/.gemini/config/plugins/<name>/mcp_config.json. So:

- ship configs/antigravity-cli/.mcp.json (un-ignored via a .gitignore negation),
  pinning CONTEXT_MODE_PLATFORM=antigravity-cli so the server self-identifies as
  agy — fixing the #774 mis-detection at the MCP level, not only via dir markers;
- drop the dead mcp_config.json and the manifest's redundant mcpServers;
- simplify the installer: `agy plugin install` now registers MCP + skill + hook;
  it keeps the stale tool-schema cache-clear + version-skew probe, and now
  self-verifies the plugin-scoped MCP registration (one-line manual fallback if a
  future agy skips it) instead of blindly writing the global profile.

Both CLI plugin bundles (copilot-cli, antigravity-cli) are now consistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(antigravity-cli): doctor recognizes the plugin-scoped MCP + hook registration

After the bundle moved to `.mcp.json` (so `agy plugin install` registers MCP +
the capture hook into agy's plugin profile ~/.gemini/config/plugins/context-mode/),
doctor still only checked the global ~/.gemini/config/{mcp_config,hooks}.json and
warned "context-mode not found" / "capture hook not configured" on a working install.

- checkPluginRegistration + validateHooks now accept the plugin profile (the
  canonical `agy plugin install` location) OR the global path (manual fallback).
- getInstalledVersion reads the installed plugin.json version so the version line
  shows a real semver (PASS when current) instead of the bogus "vconfigured".
- fix hints point to `npm run install:agy`.

Unit-tested (plugin-scoped PASS for both MCP + hook). Runtime already confirmed on
agy 1.0.6: `npm run install:agy` + `agy -p "...ctx_execute...7+5..."` → 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: clarify supported client count

* fix(copilot-cli): fail-open PreToolUse hook + gate debug logs (#787 review)

A thrown PreToolUse hook exited non-zero with empty stdout, which GitHub
Copilot CLI 1.0.59 treats as "Denied by preToolUse hook (hook errored)" and
uses to block EVERY tool — bricking the agent. parseStdin runs JSON.parse, so
a malformed payload alone triggers it. Wrap the hook body in a fail-open
try/catch: a legitimate veto is a normal stdout write + return (never a
throw), so only real errors are swallowed (empty stdout + exit 0 => ALLOW).
Adds a regression test that spawns the hook with a throwing payload.

Also gate the per-invocation debug logs (posttooluse/precompact/sessionstart)
behind CONTEXT_MODE_DEBUG, matching the kimi hooks — the PostToolUse log grew
on every tool call under the user's config dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(util/jsonc): string-aware trailing-comma strip (#787 review)

stripJsonComments stripped trailing commas with a regex over the whole string,
silently eating commas INSIDE string values (e.g. "[1, ]" -> "[1 ]") on the
comment-strip path (reached whenever strict JSON.parse fails). Move the
trailing-comma removal into a second string-aware pass over the comment-free
output: in-string commas are preserved while real trailing commas — including
those separated from } or ] by a comment — are still stripped. Regenerated
bundles (jsonc is bundled into cli/server.bundle.mjs).

The identical duplicates in src/server.ts and src/adapters/opencode/index.ts
are left for a follow-up consolidation PR (they parse third-party configs;
wider blast radius).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: consolidate per-adapter test files per CONTRIBUTING (#787 review)

CONTRIBUTING.md ("Test file organization") keeps one test file per adapter /
core module. Merge the standalone bundle-guard + schema files into their
canonical homes and delete the standalones — zero net-new test files:
  - copilot-cli-plugin.test.ts    -> adapters/copilot-cli.test.ts
  - antigravity-cli-plugin.test.ts -> adapters/antigravity.test.ts
  - strict-client-schema.test.ts  -> core/server.test.ts (sanitizeSchemaForStrictClients)

Also add the jsonc string-aware regression test to core/server.test.ts (its
home per the domain table; jsonc.ts has no test file of its own).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: rename copilot capture hooks file to the <platform>-hooks convention (#787 review)

The repo's per-platform hook test files are named tests/hooks/<platform>-hooks.test.ts
(cursor-hooks, gemini-hooks, vscode-hooks, jetbrains-hooks, kiro-hooks, kimi-hooks).
copilot-cli's was the lone deviation (copilot-cli-capture.test.ts). Rename it to
copilot-cli-hooks.test.ts and add the matching row to the CONTRIBUTING.md test-file
table. (antigravity-cli stays folded into antigravity.test.ts — capture-only single
hook, mirroring the GUI variant in the same family file, per the repo's precedent.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(version-sync): register the Copilot CLI bundle manifest (#787 review)

configs/copilot-cli/.github/plugin/plugin.json carries a pinned "version" but,
unlike the antigravity-cli bundle, was missing from version-sync — so it would
freeze on the next `npm version` bump (the .cursor-plugin v1.0.111 drift class
the version-sync test guards against). Add it to scripts/version-sync.mjs targets,
the package.json `version` git-add list, and the version-sync test (targets + pkg
list + SHIPPED lockstep + end-to-end), mirroring the agy bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(antigravity-cli): bounded PreToolUse enforcement via agy's native decision contract

agy honors a top-level PreToolUse decision `{"decision":"deny"|"ask",reason}`
(verified on agy 1.0.6) — not Claude's permissionDecision/additionalContext — so
context-mode can ENFORCE routing on agy, not just capture.

- PreToolUse routing hook (hooks/antigravity-cli/pretooluse.mjs) emits agy's
  native decision; deny/ask enforce, context/modify collapse to an enforceable
  deny (agy ignores additionalContext). Fail-open.
- Shared agy payload mapper (hooks/antigravity-cli/payload.mjs) used by
  pre/post/stop; posttooluse refactored onto it. New capture-only Stop hook
  (best-effort — agy Stop firing unconfirmed, so it's excluded from doctor health).
- Native root bundle: ships plugin.json + mcp_config.json + hooks.json +
  rules/context-mode.md (agy reads bundle-ROOT files); .mcp.json and
  .claude-plugin/plugin.json removed. hooks/hooks.json kept as the validate/install
  mirror — agy runtime fires from root hooks.json, but `agy plugin validate/install`
  only REPORTS hooks when the subdir hooks/hooks.json also exists.
- routing.mjs agy aliases (run_command->Bash, view_file->Read, ...) + CommandLine/
  AbsolutePath/URL extractors; tool-naming.mjs maps agy to context-mode/<tool>.
- adapter: capabilities preToolUse/postToolUse true, paradigm json-stdio, native
  decision formatter, doctor; cli.ts HOOK_MAP pretooluse/posttooluse/stop;
  version-sync tracks the bundle plugin.json.

Fixes a marker-handoff bug: pretooluse keyed rejected/redirect markers on
conversationId while posttooluse reads via getSessionId (which prefers the
transcript UUID) — both now use getSessionId, with a <uuid>.jsonl round-trip
regression test. Also corrects a stale core-routing assertion to agy's
context-mode/<tool> surface, adds the CONTRIBUTING test-file row, and includes
incidental CODEX_* test-env isolation hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(antigravity-cli): review polish — modify guidance, ask fallback, sync comments, test placement

- formatters: agy `modify` now surfaces routing's per-tool redirect guidance
  (curl/build-tool/inline-HTTP) extracted from the echo payload instead of one
  generic line; `ask` carries a fallback reason so a security-policy confirmation
  prompt is never bare. Adapter formatPreToolUseResponse ask branch mirrored.
- comments: cross-reference the three agy tool-name maps (payload.mjs /
  routing.mjs / extract.ts) and the two agyContextReason copies (formatters.mjs /
  adapter) so they don't silently drift (single shared table = follow-up).
- tests: move the agy formatter tests to the canonical tests/hooks/formatters.test.ts
  (formatDecision wrapper style, beside the other per-platform blocks); assert the
  surfaced modify guidance + the ask fallback. Update the run_command deny test to
  the specific (non-generic) guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(antigravity-cli): default exec timeout under agy + anti-dump rules

Two agy-specific hardening fixes surfaced by interactive testing:

- ctx_execute / ctx_execute_file / ctx_batch_execute apply a default execution
  timeout (120s, tunable via CONTEXT_MODE_AGY_EXEC_TIMEOUT_MS) ONLY under agy.
  agy does not enforce an MCP RPC timeout, so a runaway/blocking script hung
  forever and had to be interrupted; every other host keeps the unbounded
  behavior (Issue #406). resolveExecTimeout() centralizes this; timed-out
  messages now report the effective timeout (was "undefinedms"). Unit-tested +
  e2e-verified (runaway ctx_execute killed at the bound instead of hanging).
- rules/context-mode.md: add a prominent "Do not dump — derive" section. agy
  artifacts each MCP tool's stdout to a step file the model then reads back, so
  a whole-file dump costs the context window twice; steer the model to
  value/match/known-slice extraction instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(copilot-cli): use camelCase hook event names so hooks actually fire

GitHub Copilot CLI (verified against the @github/copilot 1.0.60 binary)
dispatches hooks by camelCase event names ONLY — preToolUse / postToolUse /
sessionStart / userPromptSubmitted / agentStop / preCompact. The adapter
shipped PascalCase keys (PreToolUse / ...), which the binary silently ignores,
so context-mode's PreToolUse routing enforcement and PostToolUse capture never
fired on Copilot CLI. MCP tool exposure (.mcp.json auto-discovery) was
unaffected, which masked the regression.

- HOOK_TYPES values -> Copilot's camelCase. UserPromptSubmit->userPromptSubmitted
  and Stop->agentStop are NAME changes, not just casing.
- Decouple the CLI dispatch token from the event name: buildHookCommand now
  derives the token from the .mjs script base (pretooluse, ...), so the event
  KEY can be camelCase while the dispatcher and cli.ts hook handler stay stable.
- Update configs/copilot-cli/hooks.json keys, README, index.ts comments, tests.

Verified e2e on real Copilot CLI 1.0.60 via the documented plugin install:
PreToolUse denied a raw `curl` and redirected to ctx_fetch_and_index (the model
obeyed); PostToolUse fired (posttooluse-debug.log advanced under
CONTEXT_MODE_DEBUG). The internal DB event-type labels in hooks/copilot-cli/*.mjs
are context-mode's cross-adapter taxonomy and are intentionally unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Keep Copilot CLI plugin MCP config loadable on older CLI

Mac smoke testing found that Copilot CLI 1.0.44 rejects the plugin MCP entry before startup unless the no-argument server still declares an explicit empty args array.

Constraint: Copilot CLI 1.0.44 requires an explicit args array for plugin stdio MCP entries

Rejected: Omit args because context-mode takes no arguments | older Copilot CLI rejects the plugin config before MCP startup

Confidence: high

Scope-risk: narrow

Directive: Keep args: [] in the Copilot plugin .mcp.json unless Copilot documents it as optional across supported versions

Tested: vitest copilot-cli adapter and hook suites; real Copilot CLI 1.0.44 loaded context-mode MCP after patch; real agy prompt returned 12

Not-tested: Copilot prompt completion, because local Copilot CLI fails to list models even without this plugin

Co-authored-by: OmX <omx@oh-my-codex.dev>

* Ship the agy installer in the npm package

Clean-install testing exposed that the package declared npm run install:agy but omitted the installer file from package.json files, so the installed tarball failed before agy plugin install could run.

Constraint: npm tarball contents are limited by package.json files

Rejected: Rely on repository-local installer presence | npm install -g ships only allowlisted files

Confidence: high

Scope-risk: narrow

Directive: Keep package scripts and package.json files in lockstep for shipped install commands

Tested: vitest antigravity and copilot adapter hook suites; npm pack includes scripts/install-antigravity-cli-plugin.mjs; npm uninstall -g context-mode then npm install -g tarball; npm --prefix installed package run install:agy; real agy prompt returned 12; Copilot loaded installed plugin MCP

Not-tested: Copilot prompt completion, because local Copilot CLI fails to list models after MCP startup

Co-authored-by: OmX <omx@oh-my-codex.dev>

* test(server): use valid tsc option for on-demand build

* fix(copilot-cli): validate plugin runtime hooks

* docs(copilot-cli,antigravity-cli): correct hook comments + fields to match upstream refs

Ground the new Copilot CLI / Antigravity CLI adapters against the real
upstream sources (refs/platforms) and fix misleading comments + one
contradicted field. No runtime behavior change to working paths.

Copilot CLI:
- version:1 is OPTIONAL, not mandatory — the CLI accepts hook configs
  that omit the version field (copilot-cli changelog.md:1109). Keep
  emitting version:1 (harmless, self-documenting); fix the comments,
  README, and docs that claimed hooks never fire without it.
- PascalCase event names are ACCEPTED and fire — the CLI loads configs
  across VS Code / Claude Code / CLI by accepting PascalCase alongside
  camelCase (changelog.md:1065, :811, :1081). Drop the 'silently
  ignored / never fires' claim; we use camelCase as the native naming.
- session_id (snake_case) is the documented payload field
  (changelog.md:811). Read it first; keep sessionId (camelCase) as a
  defensive, undocumented fallback.

Antigravity CLI:
- The only refs-backed payload field is workspace.current_dir, an object
  field (examples/title/title.sh:10, examples/title/README.md:11). Read
  workspace.current_dir FIRST for the project dir, falling back to the
  empirically-derived workspacePaths[0]. Annotate conversationId /
  workspacePaths as unverified. Stop stays best-effort/unverified on
  agy 1.0.6.

Docs: platform-support table + README continuity matrix now show
Antigravity CLI Stop as best-effort/unverified and the corrected
session-id / project-dir fields; 17-platform count unchanged (correct).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-06-21 16:06:25 +03:00
NgoQuocViet2001 04ff30fcc0 feat(fetch): add per-call cache ttl (#666)
* feat(fetch): add per-call cache ttl

* test: run npm pack dry-run on Windows
2026-05-24 02:17:38 +03:00
ByF dee946c64d fix(pi): prevent mixed-width TUI over-width crashes (CJK/Korean/emoji) (#676)
* fix(pi): CJK wide-character width-aware truncation in PiTextComponent (#665)

PiTextComponent/truncateAnsiLine counted every JS character as width 1,
but CJK ideographs occupy 2 terminal columns. This produced lines whose
actual visibleWidth exceeded the requested width, triggering a pi-tui
crash: 'visible width: 162 > terminal width: 147'.

Root cause: truncateAnsiLine() iterated chars with visible++ (always 1)
instead of accounting for east-asian-width W/F codepoints.

Fix:
- Add charWidth() helper that returns 2 for CJK/Hangul/fullwidth ranges.
- Change truncateAnsiLine to use charWidth and check visible + w > maxWidth
  (not >=), so a 2-wide char still fits when exactly 2 columns remain.
- Export PiTextComponent and truncateAnsiLine for testability.

Tests (Slice 10 in pi-mcp-bridge.test.ts):
- Pure CJK text respects requested width.
- Mixed ASCII + CJK is correctly truncated.
- ANSI escape sequences are preserved and not counted toward width.
- The real crash line from pi-crash.log fits within terminal width 147.
- Edge case: maxWidth 0 or negative returns empty string.

Fixes #665

* test(asymmetric-drift): make npm pack dry-run windows-safe

---------

Co-authored-by: baifan <ubuntu@BaiFanPC.localdomain>
2026-05-23 10:13:59 +03:00
Baijack-star fa61570941 Fix Claude plugin skills path and pack integrity guard (#661)
* ci: update server.bundle.mjs, cli.bundle.mjs, session hook & security bundles

* ci: update install stats

* ci: update install stats

* ci: update install stats

* Fix Claude plugin skills manifest path

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-22 13:05:09 +03:00
Ben Younes 4986019644 fix(codex): detach background npm install so MCP boot doesn't blow Codex's 30s timeout (#634) (#638)
* fix(codex): detach background npm install of fetch-and-index deps so MCP boot doesn't blow Codex's 30s timeout (#634)

Codex CLI 0.131.0 enforces a 30s startup_timeout_sec per MCP server
(codex-rs/config/src/mcp_types.rs RawMcpServerConfig). Before this
patch, every cold MCP boot of context-mode through Codex hit:

  MCP client for `context-mode` timed out after 30 seconds.

Root cause traced via probe instrumentation of start.mjs against a
real codex CLI run (codex 0.131.0 + context-mode 1.0.141 installed via
`codex plugin marketplace add mksglu/context-mode`):

  [+0ms]      start
  [+11ms]     before selfHealCacheHealHook
  [+13ms]     before normalizeHooks
  [+13ms]     before ensure-deps
  [+17ms]     after ensure-deps
  [+17ms]     before npm-install loop
  [+23182ms]  after npm-install loop / before server.bundle
  [+23295ms]  after server.bundle

The synchronous `execSync("npm install " + pkg)` loop for
`turndown` + `turndown-plugin-gfm` + `@mixmark-io/domino` blocked
the MCP boot path for ~23s. Codex's plugin marketplace git-clones into
`~/.codex/plugins/cache/<pkg>/` without running `npm install`, so on
every fresh install these three packages were absent and the loop paid
the full cold-fetch cost. With Codex's own ~15s websocket prewarm
running serially before the MCP handshake on slower hosts, total time
to `InitializeResult` blew straight past the 30s budget on macOS Wi-Fi
(originally reported by @mksglu).

These three deps are only consumed by `ctx_fetch_and_index`'s
sandboxed HTML→Markdown subprocess, which resolves them via
`require.resolve()` at call time. None of them are touched by the MCP
`initialize` handshake or by any other ctx_* tool. Detaching the
installs (`spawn(..., { detached: true, stdio: "ignore" }).unref()`)
lets the MCP server reply within milliseconds while the installs
complete in the background, typically long before any LLM-driven
fetch call can fire. If a user invokes `ctx_fetch_and_index` faster
than the install completes, the subprocess's existing
`require.resolve("turndown")` failure surfaces a typed error to the
caller — same posture as any other missing-runtime-dep situation in
that code path.

Measured end-to-end with real codex 0.131.0 + context-mode 1.0.141
swapped into Codex's plugin cache (Linux/Node v22):

  Cold boot (no node_modules): ~27s  →  ~8s
  Warm boot (deps already present): ~27s  →  ~7s

Coverage added: tests/scripts/start-mjs-mcp-boot.test.ts pins both
halves of the contract — (a) the MCP boot slice between
`./hooks/ensure-deps.mjs` (last sync step the boot is allowed to
block on) and the `server.bundle.mjs` import must not contain any
`execSync(... npm install ...)` call, and (b) the three packages are
still kicked off via the detached `spawn(NPM_BIN, …)` path so codex
marketplace users don't permanently lose fetch-and-index. Red-green
verified by stashing the start.mjs change and rerunning the test.

* chore(start): extract IS_WIN32 constant to dedupe `process.platform === "win32"` (review nit)
2026-05-19 19:56:21 +02:00
Mert Köseoğlu 6cb1490c9b fix(family-A): persistence-tier rules — drop stale .mcp.json + portable Tier C hooks (#620)
* fix(family-A): persistence-tier rules — drop stale .mcp.json + portable Tier C hooks

Single unified PR for the persistence-tier family. Three issues, one
architectural decision: classify every file by who reads / mutates it
(plugin-cache vs. user-home vs. workspace-committed) and enforce the
correct mutability contract per tier.

Issue #604 — hooks.json bidirectional ratchet (already fixed on `next`
by merged PR #611 / commit 97792c5 — closing the issue via the
"Closes #604" keyword in this PR). No additional code change needed.

Issue #609 — .mcp.json stale-write removal:

  - src/cli.ts: stop writing `.mcp.json` into the per-version plugin
    cache dir at upgrade time. Claude Code reads `.claude-plugin/
    plugin.json.mcpServers` as the canonical source (verified upstream:
    refs/platforms/claude-code/src/utils/plugins/mcpPluginIntegration.ts:131-212).
    The cli-side write was the producer of the stale carry-forward that
    Claude Code's native plugin auto-update copies into a fresh version
    dir → MODULE_NOT_FOUND on every MCP boot.
  - src/server.ts: same removal in the inline-fallback upgrade path.
  - scripts/heal-installed-plugins.mjs: new sweepStaleMcpJson() removes
    any pre-existing .mcp.json from every per-version cache dir, with
    path-traversal guard against malicious pluginKey segments.
  - start.mjs, scripts/postinstall.mjs, src/cli.ts: wire sweepStaleMcpJson
    into the existing heal block. Belt-and-braces second-pass assertion
    in cli.ts upgrade() — second sweep MUST report removed:[] or throw.

Issue #613 — buildHookCommand portable Tier C:

  - src/adapters/vscode-copilot/hooks.ts: drop the absolute-path branch
    added by commit f5c9d02 (2026-03-06). Always emit the CLI dispatcher
    form `context-mode hook vscode-copilot <event>`. The reverted shape
    is the pre-f5c9d02 portable form already in production for cursor
    and codex adapters.
  - src/adapters/jetbrains-copilot/hooks.ts: same fix — same Tier C
    (.github/hooks/context-mode.json is workspace-committed and lands
    in every teammate's `git status`).

Why Tier C MUST be portable: refs/platforms/vscode-copilot/assets/
prompts/skills/agent-customization/references/hooks.md line 7 confirms
`.github/hooks/*.json` is "Workspace (team-shared)". Embedding
`process.execPath` (which under fnm-windows is the per-session-ephemeral
`fnm_multishells/<PID>_<TS>/node.exe` shim) into a committable file
leaks PII (`C:/Users/<user>/...`) AND breaks cross-machine portability.

Test coverage:

  - tests/hooks/cache-heal-self-heal.test.ts: 6 new tests for
    sweepStaleMcpJson — happy path, no-op, missing cache root,
    path-traversal guard, sibling-file preservation, best-effort
    on race condition.
  - tests/adapters/vscode-copilot.test.ts: 4 new Tier C lock tests
    asserting buildHookCommand never bakes absolute paths.
  - tests/adapters/jetbrains-copilot.test.ts: 4 matching Tier C tests.
  - tests/core/cli.test.ts: amended .mcp.json describe block — reversed
    the #411 "must write" assertions to enforce "MUST NOT write" +
    "MUST sweep". server.ts inline-fallback assertion reversed in
    parallel. Bug-class protection from #531 (placeholder in example,
    files[] excludes .mcp.json) preserved unchanged.
  - tests/cli/upgrade-mcp-json-assertion.test.ts: pivoted from
    healMcpJsonArgs lock to sweepStaleMcpJson lock — same
    architectural-lock pattern, new mechanism.
  - tests/util/postinstall-heal-mcp-json.test.ts,
    tests/util/start-mjs-self-heal.test.ts: amended to assert
    sweepStaleMcpJson wiring in postinstall + start.mjs.
  - tests/scripts/asymmetric-drift-assert.test.ts: stub updated to
    export sweepStaleMcpJson alongside healMcpJsonArgs.

Targeted test verification: 321/321 tests pass across all touched
files. `npx tsc --noEmit` — clean.

Closes #604
Closes #609
Closes #613

* feat(doctor): proactive Tier C absolute-path + stale .mcp.json checks (PR #620 slice 4)

PR #620 fixed the WRITE-time root causes (#609 stop writing per-version cache
.mcp.json; #613 emit CLI-dispatcher form for vscode/jetbrains-copilot hooks),
but users running pre-v1.0.137 still carry poisoned state on disk:

  - Tier C workspace-committed files (.github/hooks/context-mode.json,
    .cursor/hooks.json, .jetbrains/copilot/hooks.json) with absolute
    Windows fnm shim paths baked by old /ctx-upgrade runs.
  - Leftover per-version .mcp.json files in
    ~/.claude/plugins/cache/context-mode/context-mode/<ver>/ that the
    architectural untrack now treats as drift.

Per ISSUE-604-VERDICT §11 ("silent-green doctor while hooks are dead is itself
a P0 trust bug"), doctor must SURFACE this state before the user hits the
runtime failure.

  CHECK A (FAIL): scan each Tier C file under process.cwd(); recurse all
    string values; flag any absolute path (unix /, Windows [A-Z]:[/\\],
    double-backslash UNC), fnm_multishells shim, or process.execPath
    literal. Missing config -> SKIP (no false fail). Remediation points
    at /context-mode:ctx-upgrade.

  CHECK B (WARN): enumerate cache version dirs under homedir() (Mert
    standing Windows-safety rule -- never use literal '~/'); count
    stale .mcp.json. Recoverable, so WARN not FAIL. Remediation: next
    ctx_upgrade sweep removes them via sweepStaleMcpJson.

TDD evidence:
  RED: 3 new tests in tests/core/cli.test.ts under 'PR #620 slice 4 --
       doctor() surfaces persistence-tier bug class' -- all 3 fail on
       current main (anchors '#613' / '#609' / 'fnm_multishells' /
       homedir() absent from doctor()).
  GREEN: 3/3 pass; 160/160 cli.test.ts tests pass; tsc --noEmit clean.

Tests slot into existing tests/core/cli.test.ts (CONTRIBUTING L275 -- no
new test files). Static-source-analysis pattern matches the Issue #564
doctor test precedent (lines 2056-2101). No bundle files touched.

* test(ci-lint): configs/** Tier C portability invariant (PR #620 slice 5)

PR #620 surgically fixed vscode-copilot + jetbrains-copilot adapters
(commit f5c9d02 had baked absolute process.execPath + script paths into
workspace-committed .github/hooks/context-mode.json). The fix was
adapter-local; nothing structural prevents a future contributor from
re-introducing the same bug class in any of the other 13 adapters
under configs/.

This invariant extends tests/scripts/asymmetric-drift-assert.test.ts
(the existing CI lint surface wired into `npm run build`) with a
recursive scan of every .json template under configs/**. For each
string value, fail the test if it matches:

  - unix absolute paths (^/Users/, ^/home/)
  - Windows drive-letter absolute ([A-Z]:[/\\])
  - Windows UNC (^\\\\)
  - fnm session shim (fnm_multishells) -- the #613 reporter symptom
  - process.execPath literal -- the f5c9d02 anti-pattern signature
  - literal `~/...` tilde paths (not JSON-portable)
  - `${HOME}/...` shell expansion (not JSON-portable)

Error message names the offending file:jsonPath:value and the matched
pattern so future contributors get the fix direction without grepping.

Per ISSUE-613-VERDICT 6.1 persistence-tier rule: Tier C files MUST be
born portable -- no heal seam exists for files committed to user repos.
This invariant catches the bug class at PR review time across the
entire configs/ surface, not just the two adapters PR #620 fixed.

TDD evidence:
  RED proof: dropping a poisoned `configs/vscode-copilot/poison.json`
  with `/opt/homebrew/...`, `/Users/jowch/...` and `fnm_multishells/...`
  paths -> test fails with the exact offence list (verified locally,
  fixture removed before commit).
  GREEN: 9/9 tests in asymmetric-drift-assert.test.ts pass against
  the post-PR-620 clean source tree; full Family-A regression
  91 files / 2018 tests pass; tsc --noEmit clean.

Slots into existing CI-lint test file (CONTRIBUTING L275 -- no new
test files). Same wiring posture as the existing assert-asymmetric-drift
invariant: catches the regression at `npm run build` before publish.

* feat(doctor-dx): solution-first messages for Tier C + stale .mcp.json checks (PR #620 slice 6)

Slice 4 (commit f17e8a1) added two new doctor checks that surface
pre-v1.0.137 poisoned state on disk. The detection logic is correct,
but the user-facing messages were written in internal vocabulary
("Tier C", "per-version cache dirs", "sweepStaleMcpJson", "command
shapes"). Per Mert's DX/UX directive — "anlamsiz mesajlar vermeyelim
User'a. Yonlendirici olmali. Cozum odakli olmali." — rewrite each
message to lead with the fix, not the diagnosis.

Each new message now follows: diagnosis (one sentence, user words)
-> why-it-matters (consequence the user emotionally cares about)
-> fix (single actionable command, /context-mode:ctx-upgrade for
both) -> link to issue for deep-dive.

Cross-OS safety: no `rm`/`del` in any remediation. All paths route
through /context-mode:ctx-upgrade which is portable on macOS, Linux,
and Windows.

Changes:
  CHECK A (Issue #613 — workspace hook config):
    - Step line: "Tier C" -> "team-shared in your workspace"
    - FAIL: leads with "this file is committed to git, your teammates
      and CI will get your path and the hooks will break for them"
      before naming the technical cause; drops "portable command shape"
      jargon; adds issues/613 URL.
    - PASS / SKIP / parse-WARN: aligned to plain-English "Hook config:"
      label; parse-WARN now explains why the user should care + gives
      two recovery paths.

  CHECK B (Issue #609 — stale .mcp.json):
    - Step line: "stale per-version .mcp.json" -> "leftover .mcp.json
      from older versions"
    - Stale-WARN: opens with "these are harmless but should be cleaned
      up so they cannot confuse Claude Code after an auto-update" to
      prevent panic at WARN; replaces internal function name
      "sweepStaleMcpJson" with "it sweeps these files automatically";
      adds issues/609 URL.
    - PASS / SKIP / enumerate-WARN: aligned to "Leftover .mcp.json
      check:" label; enumerate-WARN now labels path + reason on
      separate lines + gives a concrete next step.

Test preservation: test contract in tests/core/cli.test.ts asserts
on `#613`/`#609` anchors, `fnm_multishells`, `homedir()`, log-level,
and `ctx_upgrade` token within window slices of doctorBody(). All
anchors live in in-function comments + the detection helper, which
are unchanged. Issue URLs at the end of FAIL/WARN messages keep the
`ctx_upgrade` token comfortably inside the window.

Verification:
  - npx vitest run tests/core/cli.test.ts -> 160/160 passed
  - npx tsc --noEmit -> clean

No new test files (CONTRIBUTING L275). No bundle files touched.
2026-05-18 23:04:25 +03:00
Mert Koseoglu 63fa0841f5 test(postinstall-heal): stub heals to keep regression test under CI budget
The regression test added in ceaec16 ran the live `scripts/postinstall.mjs`
end-to-end against a scratch dir. That brought in `healBetterSqlite3Binding`
(prebuild-install download / native rebuild) and the installed-plugins
heals, which together took ~22s on macOS and timed out at the 30s
spawnSync budget on Ubuntu/macOS CI runners. spawnSync killed the child
on timeout, status came back null, the assertion `expect(r.status).toBe(0)`
saw `null !== 0`, test went red.

The behavior under test is section 4's GUARD — `isGlobalInstall() && !TMPDIR_UPGRADE_RE.test(pkgRoot)`
— not the heal modules themselves. Those have their own unit tests in
tests/util/. Stubbing them out keeps the test focused on the actual
contract (postinstall must not mutate source-tracked plugin.json on
contributor / CI installs) and brings runtime from ~22s → 34ms.

The stubs export the same named functions postinstall.mjs imports, so
the dynamic-import chain still goes through the real `normalizeHooksOnStartup`
code path that the guard protects.
2026-05-12 16:58:08 +03:00
Mert Koseoglu ceaec16eff fix(postinstall): gate hook-normalization heal with isGlobalInstall (#531 fix-of-fix)
CI run 25734987495 (windows-latest) failed on `npm run build` at the
`assert-asymmetric-drift` step with:

  asymmetric-drift: FAIL
    - .claude-plugin/plugin.json args[0] is
      "D:/a/context-mode/context-mode/start.mjs" but must equal
      "${CLAUDE_PLUGIN_ROOT}/start.mjs".

Root cause: scripts/postinstall.mjs section 4 ("Hook normalization at
install time (#414)") calls `normalizeHooksOnStartup` which substitutes
`${CLAUDE_PLUGIN_ROOT}` with an absolute pluginRoot path. Section 4's
only existing guard was a `TMPDIR_UPGRADE_RE` check that catches
/ctx-upgrade staging but does NOT catch contributor / CI installs.

On Windows CI the pipeline runs:
  1. `npm install`  → triggers `npm run postinstall` → section 4 runs
                      against the cloned repo, rewriting source-tracked
                      `.claude-plugin/plugin.json` args[0] from the
                      placeholder to `D:/a/.../start.mjs`.
  2. `npm run build` → invokes `scripts/assert-asymmetric-drift.mjs` (the
                       new Issue #531 invariant). It reads the now-mutated
                       file, sees drift, exits 1, build fails.

Fix: gate section 4 with `isGlobalInstall()` — the same heuristic
section -1 already uses ("npm_config_global=true" AND no `.git` walking
up the tree). A contributor's `npm install` from a clone (and CI checkouts)
always have `.git` → isGlobalInstall returns false → section 4 skips →
source files stay untouched. Real `npm install -g context-mode` is
unaffected: no `.git` near the cache dir → guard passes → heal runs.

Test coverage:
- New regression test "postinstall.mjs DOES NOT mutate source-tracked
  plugin.json when run from a clone (Windows CI regression)" in
  tests/scripts/asymmetric-drift-assert.test.ts. The test runs the REAL
  postinstall.mjs (not a mock) against a temp clone-like layout with
  `.git` and `node_modules` siblings, asserts plugin.json args[0] is
  STILL the placeholder. Any future regression that lets section 4
  mutate source files surfaces immediately as a vitest failure.

Verified locally:
  $ npm run build  → all bundles OK + asymmetric-drift OK + exit 0
  $ npx vitest run tests/scripts/asymmetric-drift-assert.test.ts
    Test Files  1 passed (1)
    Tests       8 passed (8)
2026-05-12 16:46:29 +03:00
Mert Koseoglu 5a3091a996 fix(tests): asymmetric-drift uses .mcp.json.example + postinstall-heal 90s timeout
Two CI failures on v1.0.122 (run 25729116323):

1. tests/scripts/asymmetric-drift-assert.test.ts — 3 tests asserted on
   repo-root .mcp.json which is no longer tracked after the #531
   architectural untrack (commit 9261377). Switch to .mcp.json.example
   (the canonical template), mirroring the script-side fix in commit
   4da170e.

2. tests/util/postinstall-heal.test.ts — 2 tests timed out at the
   default 30s vitest gate on CI. The tests spawn real npm install
   subprocesses which legitimately take 60-90s on cold ubuntu/windows
   runners. Bump those two specific tests to a 90s timeout via the
   per-test option. Local run confirms 65s typical.

Verified: 14/14 pass on the affected files locally.
2026-05-12 13:50:32 +03:00
Mert Koseoglu 951cd925f6 fix(install): CI invariant prevents future .mcp.json / plugin.json asymmetric drift (closes #531 partial — 9 of 10)
Architectural guardrail that prevents the class of bug that caused #531.
The repo ships TWO sibling files carrying MCP server args:
  1. .mcp.json                  (Claude Code reads at plugin load)
  2. .claude-plugin/plugin.json (used by Cursor adapter)

v1.0.118 fixed .mcp.json (#411). v1.0.119 fixed plugin.json AND added
self-heal — but ONLY for plugin.json. Asymmetric coverage. Then commit
aea633c (#253, 2026-04-13) regressed .mcp.json to bare ./start.mjs and
there was no invariant to catch it. Fresh installs broke for a full
release cycle.

This invariant has two layers:
  - tests/scripts/asymmetric-drift-assert.test.ts — vitest source-tree check
  - scripts/assert-asymmetric-drift.mjs — build-chain check, wired into
    npm run build alongside assert-bundle so regressions surface in CI
    before publish

Any future commit that rewrites either sibling without rewriting the
other fails loud — no more silent #531-class regressions.
2026-05-12 12:39:52 +03:00
Mert Koseoglu 1e73a13d19 fix(codex): make plugin discoverable via .agents/plugins/marketplace.json (#525)
PR #525 (tedjy971) reported that context-mode never shows up in Codex
CLI's `/plugin` listing despite shipping a `.codex-plugin/marketplace.json`.
The reported claims were verified against the Codex Rust source in
refs/platforms/codex and OpenAI's published docs — all three load-bearing
assertions hold:

  1. Codex reads `MARKETPLACE_MANIFEST_RELATIVE_PATHS` =
     `[.agents/plugins/marketplace.json, .claude-plugin/marketplace.json]`
     (codex-rs/core-plugins/src/marketplace.rs:21). `.codex-plugin/
     marketplace.json` is NOT in this list — Codex never opens it.

  2. The local-plugin source `path` must be `./<subdir>`, not `./`.
     Codex's `resolve_local_plugin_source_path` (marketplace.rs:502-518)
     does `path.strip_prefix("./")` then rejects empty results with
     `"local plugin source path must not be empty"`. Our shipped
     `.claude-plugin/marketplace.json` uses `source: "./"`, which hits
     this rejection. The error is swallowed silently at marketplace.rs:
     446-452 via `warn!(... skipping marketplace plugin that failed to
     resolve)`, so `codex plugin marketplace add` succeeds with exit 0
     but the plugins vec is empty and the user sees nothing in /plugin.

  3. `${CODEX_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_ROOT}` placeholders are
     NOT interpolated by Codex (upstream openai/codex#19582 OPEN). Grep
     of codex-rs/core-plugins/src/ confirms zero `interpolat*` /
     `expand_env*` / `envsubst*` logic in non-test source code.

These were also corroborated by OpenAI's own docs at
https://developers.openai.com/codex/plugins/build which spell out:
  - "a repo marketplace at $REPO_ROOT/.agents/plugins/marketplace.json"
  - "source.path points to that plugin directory with a `./`-prefixed
    relative path" (example: `./plugins/my-plugin`)
  - "Only plugin.json belongs in .codex-plugin/"

End-to-end verification with Codex CLI v0.130.0:
  $ codex plugin marketplace add /path/to/context-mode
    Added marketplace `context-mode`.
  No silent-drop warning emitted by the warn! path now that source.path
  resolves to a real plugin tree.

Changes:

  1. Add .agents/plugins/marketplace.json with canonical schema:
       { name, interface: { displayName }, plugins: [{
         name, source: { source: "local", path: "./plugins/context-mode" },
         policy, category
       }] }
     Matches the Rust serde shape at marketplace.rs:694-744 exactly.

  2. Add plugins/context-mode symlink → repo root, so Codex's
     `resolve_local_plugin_source_path` lands on a directory that
     contains `.codex-plugin/plugin.json` (the per-plugin manifest path
     Codex's load_plugin_manifest expects).

  3. Delete .codex-plugin/marketplace.json (dead — Codex never reads it,
     keeping it ships dead bytes and misleads contributors).

  4. Remove .codex-plugin/marketplace.json from version-sync.mjs targets
     and from the `version` lifecycle git-add list. The Codex marketplace
     schema has no top-level `version` field per the Rust serde struct,
     so the new .agents/plugins/marketplace.json doesn't need syncing.
     Per-plugin version still flows through .codex-plugin/plugin.json
     which remains in the targets list.

  5. Add tests/codex/marketplace-layout.test.ts (6 tests) that mirror
     Codex's exact discovery logic — strip_prefix("./"), non-empty
     check, plugin.json presence, placeholder absence — so future drift
     produces a deterministic local failure long before users hit it.

  6. Update tests/plugins/codex-manifest.test.ts and tests/scripts/
     version-sync.test.ts to reflect the deletion (with comments
     pointing at the Rust line numbers for future maintainers).

Why we shipped our own fix instead of merging tedjy971's PR #525:
Same end-state, more rigor — full Rust-source citations, mirror-the-
deserializer tests, e2e verification with the v0.130.0 CLI. Their
analysis pointed us at the right problem; this commit gives the project
a durable test contract so a regression can't slip past CI silently
like the original bug did.

Credit: tedjy971's PR #525 surfaced the issue and the canonical layout.
2026-05-11 17:32:42 +03:00
Mert Koseoglu d3574d564e merge: v119/bundle-assert-g3 — post-build invariant CI assert (G3 architectural guardrail) 2026-05-11 09:51:52 +03:00
Mert Koseoglu 3a12609c41 fix(codex): repair .cursor-plugin drift + lockstep guard (extends PR #512 by @tedjy971)
`.cursor-plugin/plugin.json` had been stuck at v1.0.111 across seven
releases (now v1.0.118) because it was missing from the npm `version`
lifecycle `git add` list — version-sync rewrote it correctly each
time, but the modification was never staged into the release commit.
Slice 2 already plugged that hole; this slice replays version-sync to
heal the actual drifted file and adds a per-manifest assertion so any
future drift fails CI immediately rather than silently shipping.

The new "shipped manifests are in lockstep with package.json" block
covers every manifest in the version-sync targets[] — adding a new
plugin surface forces an explicit test update, surfacing the
"did you also wire it into version-sync and the `git add` list?"
question at PR-review time.
2026-05-11 09:40:51 +03:00
Mert Koseoglu 308a80f9a3 fix(codex): add .codex-plugin/* to version-sync targets (extends PR #512 by @tedjy971)
Without this, every release bump would drift `.codex-plugin/plugin.json`
and `.codex-plugin/marketplace.json` further out of sync with the
canonical `package.json:version`. Same hazard previously hit
`.cursor-plugin/plugin.json` (stuck at v1.0.111 vs current v1.0.118)
because it was missing from BOTH the targets[] in version-sync.mjs
and the npm `version` lifecycle `git add` list.

Two-part fix:
- `scripts/version-sync.mjs` → append the two Codex manifests to
  `targets[]` (so the rewrite touches them).
- `package.json` → extend the `version` script's `git add` list to
  include the two Codex manifests AND `.cursor-plugin/plugin.json`
  (the cursor manifest had the same defect; without it staged, the
  rewrite is silently discarded by the npm `version` commit).

End-to-end test in tests/scripts/version-sync.test.ts copies all
manifests into a scratch repo with a synthetic version, runs the
script, and asserts every (version | metadata.version | plugins[].version)
field gets rewritten — catches future targets[] drift automatically.
2026-05-11 09:40:11 +03:00
Mert Koseoglu 0bcea04a27 chore(ci): slice 4 — regression guard against real production bundles
EXPECTED RED until the Issue #511 ESM sweep agent merges createRequire
fixes for src/server.ts and src/cli.ts. The current
server.bundle.mjs and cli.bundle.mjs ship with esbuild's throwing
'Dynamic require of ...' shim embedded — exactly the state that
broke users in #511.

This test is the *forcing function* that turns G3 from advisory into
load-bearing. Skipping it would let the sweep land without proof the
guardrail wired through to real bundles. Once the sweep merges and the
next bundle.yml run rebuilds, this test flips green and the invariant
is permanently locked in for every PR.
2026-05-11 09:39:07 +03:00
Mert Koseoglu 30a39d6706 chore(ci): slice 3 — wire assert-bundle into npm build chain
Adds 'assert-bundle' npm script targeting all five produced bundles,
and chains it from 'build' so every build that finishes successfully
has guaranteed-clean ESM output. Pretest, prepublishOnly, and CI Build
steps all inherit the assertion.

Test pins both the script presence and the chain wiring so a future
config refactor can't silently drop the guardrail.
2026-05-11 09:38:38 +03:00
Mert Koseoglu baf39ec219 chore(ci): slice 2 — assert-bundle exits 0 on clean createRequire fixture
Pin the negative-path behavior. A bundle that uses
createRequire(import.meta.url) at module top has no shim and no bare
require('node:...') strings — the script must accept it.
2026-05-11 09:37:00 +03:00
Mert Koseoglu 61f85398b1 chore(ci): slice 1 — assert-bundle script detects 'Dynamic require of' shim
G3 guardrail (Issue #511 class). Adds scripts/assert-bundle.mjs which
scans bundle files for the esbuild throwing-require shim and exits 1
if any forbidden pattern is matched.

Tracer-bullet RED→GREEN: fixture bundle containing the shim string is
correctly rejected.
2026-05-11 09:36:36 +03:00