188 Commits
Author SHA1 Message Date
NgoQuocViet2001 4112e8485b docs(readme): sync Zed config fix to next (#875) 2026-06-24 15:12:58 +03:00
Ben Younes e0c3bb2afa fix(adapters/omp): self-register MCP server on plugin load (#677) (#872)
The `omp plugin install context-mode` path wires the extension factory
so routing hooks fire, but never creates an mcp.json entry — so the 11
ctx_* tools stay unreachable even though curl/wget are hard-blocked.

Register the server on plugin load, only when absent (never clobbering a
user's existing entry). Spawn via `node <plugin>/server.bundle.mjs`
rather than the `context-mode` bin: under the plugin install the package
lives in ~/.omp/plugins/node_modules and its bin is not on PATH.

Takes effect on the next OMP restart, same as the manual mcp.json
workaround the issue documents.
2026-06-23 23:33:21 +03:00
Mert KoseogluandClaude Opus 4.8 d1ae562557 fix(security): confine ctx_execute_file to the project boundary (#852)
ctx_execute_file fed its `path` straight into resolve(projectRoot, path),
so an absolute path (/home/user/secret, /etc/passwd) or a `../` traversal
escaped the workspace and read any file on the host. With Claude Code's
sandbox enabled the host denied such a read, but the agent retried through
the MCP sandbox — and the host's MCP approval prompt cannot inspect the
tool's input params, so the escape was invisible to the approver.

Mitigation (defense-in-depth):
- New pure, no-regex primitive isPathInsideProject() + policy wrapper
  evaluateProjectContainment() in security.ts: a path resolving outside the
  project root (absolute escape, `..` traversal, or symlink-canonical
  escape) is refused. The symlink check mirrors evaluateFilePath().
- ctx_execute_file handler runs checkProjectBoundary() before execution.
- Escape hatch reuses the host's existing permissions.allow Read(...) rules
  (via generalized readToolPermissionPatterns), NOT a bespoke context-mode
  env that would rot into dead code — an out-of-project grant lives once in
  the same config Claude Code itself honors.
- MCP approval titles for ctx_execute / ctx_execute_file now read as code
  execution, since refs show the host prompt renders only the tool title +
  raw args (the title is the one server-controlled signal).

Residual: ctx_execute / ctx_batch_execute run arbitrary code and still
inherit the process FS; the boundary guard hardens the file-read tool, not
a full OS sandbox. Host-level sandboxing remains the primary control.

Tests: tests/security/project-boundary-852.test.ts (containment geometry,
symlink escape, allow-rule opt-in, server wiring). typecheck green; full
suite 4484 passed. Bundles intentionally not committed (CI rebuilds on main).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:08:36 +03:00
Ken JoandClaude Opus 4.8 2608e344bb refactor(antigravity-cli): one-command agy plugin install (drop npm wrapper) + doc cleanup (#853)
refactor(antigravity-cli): one-command agy plugin install; drop npm wrapper

agy 1.0.7 added GitHub-subpath plugin install (with branch resolution), so the
former three-step flow shipped in #787 — `npm install -g` + `git clone` +
`npm run install:agy` (scripts/install-antigravity-cli-plugin.mjs) — is dead
weight. agy (<=1.0.6) `plugin install` accepted only a local directory, which is
why the wrapper existed; that constraint is gone.

Install is now one command, no clone, no wrapper:

  npm install -g context-mode
  agy plugin install https://github.com/mksglu/context-mode/tree/main/configs/antigravity-cli

- remove scripts/install-antigravity-cli-plugin.mjs + the install:agy npm script
  and its files[] entry
- antigravity-cli doctor `fix` strings now point at the one-command install
- README: split the conflated "Antigravity" entry into Antigravity IDE vs
  Antigravity CLI (agy); bring the agy section to the other install guides'
  level (Prerequisites / Install / MCP-only / Verify / Routing / Full configs);
  Verify points to the existing "Try It" prompts. Deep mechanics and
  troubleshooting stay in docs/platform-support.md
- docs/platform-support.md: one-command update + a "Verified: agy 1.0.10" note
  recording the >=1.0.7 install floor; hook contract unchanged through 1.0.10
  (config/hooks.json canonical since 1.0.8)
- tests: drop the wrapper-shape regression assertions (no leftover trace);
  bundle-content tests still guard the installable artifact

Preserved invariants: the bundle still registers MCP via its native
mcp_config.json (command: context-mode, env-pinned
CONTEXT_MODE_PLATFORM=antigravity-cli); the dual hooks.json + hooks/hooks.json
is kept (agy runtime reads root, validate reads subdir).

Verified on agy 1.0.10 (Linux): clean-room single-command install registers
MCP + hooks + skill from a zero baseline; tools/list exposes 11 Gemini-safe
ctx_* tools (0 const / 0 additionalProperties); `agy -p` smoke returns 12.
npm run build + tsc --noEmit + targeted vitest (47) pass. Bundles are
CI-managed (bundle.yml) and not included.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 04:50:52 +03:00
Mert Koseoglu a880c4bf24 docs(readme): link docs/adapters/kimi-code.md from the Kimi Code section 2026-06-21 18:22:14 +03:00
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
Mert Koseoglu 8db9102b57 chore(insight): drop pricing + platform URL, remove stale root docs
Keep context-mode.com/insight as the single source of truth for the ctx_insight tool/CLI/skill/README copy — remove the platform.context-mode.com sign-in line and the $20/seat pricing, which can change. Also delete stale root docs: release-notes-v1.0.148.md, llms-full.txt, llms.txt.
2026-06-21 15:39:17 +03:00
Mert Koseoglu 11b8b58701 feat(insight): pivot ctx_insight from local dashboard to hosted product
Insight is now a hosted B2B product at context-mode.com/insight (sign-in + org analytics at platform.context-mode.com). The local build-and-serve dashboard is removed; ctx_insight (MCP tool + CLI) now opens the hosted URL in the default browser via the shared openBrowserSync helper.

- server.ts: replace the ~250-line build/spawn/port handler with a browser-open handler; drop _insightChild tracking, ChildProcess/execSync imports, and the shutdown kill hook. Keep openBrowserSync/killProcessOnPort helpers and the ctx_upgrade legacy insight-cache cleanup (#469).
- cli.ts: 'context-mode insight' opens the hosted URL.
- openclaw: update ctx_insight stub description.
- Remove the insight/ dashboard app and its package.json files entries.
- Tests: drop the port-schema, containment, and CORS suites for the removed local server; keep helper, #469, and #697 description tests.
- Docs: README + ctx-insight SKILL point at the hosted dashboard.
2026-06-21 15:34:55 +03:00
pg-adm1n 9f63a0b2c1 fix(hooks): capture Stop turn ends (#773)
fix(hooks): capture Claude stop turn ends
2026-06-21 14:47:47 +03:00
Burak Sormageç 1b4f2be855 feat(adapter): add Kimi Code CLI hook integration (#729) 2026-05-31 17:03:22 +03:00
Dennes Neumann b51873fdc0 feat(cli): add indexing and search commands (#721) 2026-05-31 16:43:19 +03:00
NgoQuocViet2001 a90d399c6a fix(codex): set platform env for plugin MCP server (#722)
fix(codex): set platform env for plugin MCP
2026-05-26 18:04:44 +03:00
Mert Köseoğlu 9e29a940a1 fix(server): comprehensive ctx_* tool description audit + WebFetch refusal (substitutes #654) (#683)
* fix(server): replace "blocked" wording in WebFetch refusal with imperative retry hint (substitutes #654)

Three redirect messages in hooks/core/routing.mjs (curl/wget, Inline HTTP,
WebFetch) reframed from the negation-heavy "blocked" voice to imperative-
positive "redirected (NOT a network restriction)" — and crucially append
"— retry if it fails with a transient DNS error" so the next action is
explicit across all model tiers.

PR #654 (contributor) correctly identified Opus 4.6's "blocked → capitulate
to training" failure mode under the EAI_AGAIN cascade. Our internal A/B
audit (Probe 3, 6 trials Haiku) confirmed the fix on Opus but uncovered
a 2/6 Haiku regression — the parenthetical "(NOT a network restriction)"
landed as information without a paired action, and 2 trials concluded
"since the redirect isn't a restriction either, I can just use training
data." Audit recommended appending the imperative retry clause — this
substitute ships exactly that.

Sibling-tool consistency on ctx_fetch_and_index:
  - ssrfGuard pre-flight DNS path: classify EAI_AGAIN / ETIMEDOUT /
    ETIMEOUT / ENETUNREACH / EPERM as transient and append the same
    retry hint. Non-transient codes (ENOTFOUND) stay silent — retry
    won't help on a genuinely bad domain.
  - Subprocess fetch stderr path: closes the contributor's flagged
    "Known follow-up" (batch wrapper bypassed the single-URL hint).
    Same code regex on result.stderr — same retry hint surfaces in the
    common multi-URL batch case the original PR couldn't reach.

Tests updated, no new test files (CONTRIBUTING L275):
  - tests/hooks/core-routing.test.ts: assert "redirected" + retry hint,
    explicit negative-assert .not.toContain("blocked") regression guard.
  - tests/hooks/cursor-hooks.test.ts, tool-naming.test.ts: wording sync.
  - tests/hooks/integration.test.ts: curl-warning regex relaxed to
    survive both old "Do NOT use curl" and new "Do NOT retry with curl".

Bundles untouched — CI rebuilds on main push (project_ci_bundles).

Targeted: npx vitest run tests/hooks/ → 438/438 pass.
TypeScript: npx tsc --noEmit clean.

Closes work on #654 (PR closed in favor of this direct-to-next commit).
Audit doc: .cw/ctx-analytics/TOOL-DESCRIPTIONS-AUDIT.md §6.1
Substitute log: .cw/ctx-analytics/PR-654-SUBSTITUTE-LOG.md

* docs(adr): tool description style (ADR-0002) + routing deny reasons (ADR-0003)

ADR-0002 formalizes the structure every ctx_* tool description must
follow (1-line role / WHEN: / WHEN NOT: / RETURNS: / EXAMPLE:), the
forbidden-token list (MANDATORY:, BLOCKED, PREFER X OVER Y, Do NOT,
Never use, SESSION STATE, emoji bullets), and the MUST/SHOULD/MAY
hierarchy reserved for post-call obligations only. Grounded in 38
trials x 6 probes A/B evidence: heavy framing helps ctx_purge on Haiku
(5/5 vs 3/5 parameter fidelity) but hurts ctx_execute selection — one
size does not fit all, so rewrites are probe-gated.

ADR-0003 splits routing deny reasons into CASE A (redirect — supported
via alternative tool) and CASE B (true policy restriction). PR #654's
finding: the bare word "blocked" in WebFetch's CASE A denial was
misread by Opus 4.6 as a network restriction, triggering training-data
capitulation. CASE A MUST use "redirected", state "this is NOT a
network/security restriction", and end with a transient-error retry
hint. CASE B keeps "denied"/"blocked by security policy".

PR #683 (substitutes #654).

* fix(server): apply ADR-0002 voice to 6 ctx_* tool descriptions + contract test

Comprehensive audit of all 11 ctx_* MCP tool descriptions (see
TOOL-DESCRIPTIONS-AUDIT.md). Six tools rewritten per ADR-0002 verbatim
templates; the remaining 5 are unchanged (3 minimal-description
exemptions, 1 MUST-allowed post-call obligation, 1 deferred to a
probe-gated follow-up PR).

HIGH severity (audit §3): voice consistency on the ctx_execute family.
  - ctx_execute (src/server.ts:1419): drop "MANDATORY:" opener,
    "PREFER THIS OVER BASH", "THINK IN CODE" voice-of-trainer paragraph,
    "Do NOT read raw data". Replace with role definition + WHEN: /
    WHEN NOT: / RETURNS: / EXAMPLE: sections. ~1200 -> ~700 chars.
  - ctx_execute_file (src/server.ts:1755): same shape; drop "PREFER
    THIS OVER Read/cat" and "Don't read files into context to analyze
    mentally". Probe 2 evidence: disambiguation was already strong;
    this is a voice-consistency pass.
  - ctx_batch_execute (src/server.ts:3109): drop "THIS IS THE PRIMARY
    TOOL", "THINK IN CODE — NON-NEGOTIABLE", and the emoji-bulleted
    PARALLELIZE I/O block. Replace with WHEN: / CONCURRENCY: prose.
    ~1700 -> ~900 chars.

MEDIUM severity (audit §3):
  - ctx_search (src/server.ts:2072): drop the SESSION STATE clause
    (it is a routing-block.mjs concern; semantic-equivalence proof
    in GRILL-Q1-VERDICT.md Round 5). Add explicit WHEN: structure
    and a one-line EXAMPLE with batched queries.
  - ctx_index (src/server.ts:1900): rewrite "Do NOT use for: log
    files..." as a positive WHEN NOT: clause pointing at
    ctx_execute_file. Keep the existing WHEN TO USE: header
    (transitional alias permitted by ADR-0002).
  - ctx_fetch_and_index (src/server.ts:2865): replace
    "PARALLELIZE I/O" banner + ✅/❌ emoji bullets with a positive
    CONCURRENCY: prose block. ✅/❌ tokenize inconsistently across
    Llama/Gemini and act as negative-example leakage (rubric #4 +
    Probe 3 evidence).

Regression guard (audit §10.1, folded into existing test file per
CONTRIBUTING.md L282 "Do NOT create new test files"):
  - tests/core/server.test.ts: new describe block "tool description
    style contract (#683 ADR-0002)" parses every
    server.registerTool() block and asserts:
      * MUST NOT contain forbidden tokens (MANDATORY:, BLOCKED,
        PREFER X OVER Y, Do NOT read/use/pull, Never use,
        SESSION STATE, ✅, ❌)
      * MUST contain a WHEN: section (WHEN TO USE: accepted)
    Exemptions: ctx_stats/ctx_doctor/ctx_insight (minimal by
    design), ctx_upgrade (MUST is appropriate for post-call
    obligation), ctx_purge (deferred entirely — see below).
  - Updated two existing tests that asserted the old wording:
    concurrency-field guidance now checks prose form;
    PARALLELIZE I/O test now checks CONCURRENCY: section.

Explicitly deferred — ctx_purge:
  Probe 4 (5 trials x 2 variants, Haiku) showed the proposed soft
  rewrite REGRESSES parameter fidelity 5/5 -> 3/5. Counter-intuitive:
  the heavy negative framing (DESTRUCTIVE, REFUSAL RULES, NEVER call
  with bare {confirm:true}) actually anchors small models to the
  required scope discipline. A follow-up PR must run a tri-LLM probe
  (Haiku/Sonnet/Opus) and gate merge on that probe before changing
  this tool. Documented inline in tests/core/server.test.ts via
  EXEMPT_FROM_FORBIDDEN_TOKENS with rationale.

Verification:
  - npx tsc --noEmit: clean
  - tests/core/server.test.ts: 361/364 pass (3 pre-existing failures
    unrelated to this PR — confirmed via git stash diff)
  - tests/hooks/: 438/439 pass (1 skipped, unchanged)
  - tests/adapters/: 787/787 pass

PR #683 (substitutes #654). See docs/adr/0002 and docs/adr/0003.

* fix(hooks): apply ADR-0002 + ADR-0003 contract to routing-block.mjs + lock with regression test

Extends PR #683 to the highest-blast-radius prompt surface in the project:
hooks/routing-block.mjs ships into the system prompt of every session, while
src/server.ts tool descriptions only fire at tool-selection time. The original
PR #683 scope cleaned up the per-tool surface and the routing.mjs deny reasons
but missed the system-prompt surface itself.

Three forbidden-token violations rewritten per ADR-0002 rubric #2 (affirmative >
negative) + #9 (cross-LLM Constitutional AI safety bias):

- <forbidden_actions> XML container -> <when_not_to_use>; the container name
  itself is a Constitutional AI trigger on Anthropic-tier models.
- "NEVER use ctx_execute ... for file writes" -> descriptive form
  "File writes use the native Write or Edit tool -- ctx_execute,
  ctx_execute_file, and Bash subprocesses do not persist edits to the host
  filesystem." Same operational intent, no forbidding voice.
- "Write artifacts ... NEVER inline" -> "Write artifacts ... to files. Return
  only: file path + 1-line description."

Semantic-equivalence verified by enumerating all 16 directives in the current
block and mapping each to its rewrite (0 orphans, 0 additions). Net character
delta: -76 chars. RFC 2119 MUST kept in <priority_instructions> per the
ADR-0002 post-call-obligation carve-out.

Adds a sibling contract describe block to tests/core/server.test.ts:
"hook routing prompt-surface contract (#683 ADR-0002 + ADR-0003)". Folded
into the same file per CONTRIBUTING.md L282 (no new test files). Scans:

- hooks/routing-block.mjs and hooks/core/routing.mjs for forbidden tokens
  (<forbidden_actions>, NEVER, FORBIDDEN, "NO X for Y" bullets).
- Every "redirected"-bearing template literal in routing.mjs for ADR-0003
  CASE A compliance: MUST open with "redirected", MUST NOT contain bare
  uppercase BLOCKED, MUST name at least one ctx_* alternative tool.

15 assertions total. CASE B strings (Blocked by security policy: ...)
correctly excluded by the extractor. This is the contract test ADR-0003
Consequences L79-82 invited as follow-up.

Three tests/hooks/core-routing.test.ts assertions and one tests/core/
server.test.ts hook-injection assertion updated to match the new positive
wording (same semantic coverage, new container name).

Full regression sweep: 841 passing / 1 skipped / 3 pre-existing storage-
roots failures (verified by git stash on the branch HEAD; out of scope per
PR #683 body).

* fix(server): apply ADR-0002 canonical structure to ctx_purge + 4 ctx_* tools (PR #683 WS2/WS3)

WS2 — ctx_purge rewrite (audit §6.5, Probe 4 evidence preserved):
  - Replace negative flat framing (DESTRUCTIVE/REFUSAL RULES/NEVER) with the
    canonical WHEN/WHEN NOT/SCOPES/CONTRACT/RETURNS/EXAMPLE structure.
  - Preserve all four refusal rules verbatim under CONTRACT (confirm:false,
    sessionId+scope ambiguity, scope:'session' without sessionId, deprecated
    bare {confirm:true}) so Probe 4 parameter-fidelity discipline holds on
    Haiku (5/5 baseline must not regress).
  - Keep DESTRUCTIVE headline as accurate user-facing signaling (distinct
    from the cross-LLM-bias negative framing the ADR-0002 rubric forbids).
  - Add two EXAMPLE lines for the two valid input shapes (per-session +
    per-project) so the LLM has explicit parameter templates.
  - Add WHEN NOT clause covering the ambiguous-scope handler ("User says
    'reset'/'clear'/'wipe' without naming a scope -> ask first").

WS3 — corpus-wide canonical structure pass:
  - ctx_index: add EXAMPLE: line (was missing); fold the path-hash sentence
    into the RETURNS block so the canonical four-section shape holds.
  - ctx_search: drop the non-canonical TIPS: header (fold into RETURNS
    prose); add explicit WHEN NOT clauses (empty-index redirect, single
    one-off question -> ctx_execute).
  - ctx_fetch_and_index: drop the non-canonical CONCURRENCY: header (fold
    the I/O-bound split into the WHEN clause; fold the SQLite single-writer
    note into RETURNS); add WHEN NOT clauses (local content -> ctx_index,
    SPA-rendered content -> headless browser).
  - ctx_batch_execute: drop the non-canonical CONCURRENCY: header (fold the
    I/O-bound guidance into WHEN; fold the CPU-bound + stateful guidance
    into WHEN NOT).

Section order on all six routing-target tools is now strictly
WHEN -> WHEN NOT -> RETURNS -> EXAMPLE per ADR-0002 §Canonical structure.
Bullets are markdown '- ' only. ctx_stats/ctx_doctor/ctx_upgrade/ctx_insight
remain minimal one-line diagnostic descriptions (exempt).

Empirical reference: TOOL-DESCRIPTIONS-AUDIT.md §6.1 (ctx_purge Probe 4),
audit §3 row-by-row standardization verdicts.

* test(server): lock canonical-structure contract + amend ADR-0002 (PR #683 WS3)

ADR-0002 amendment (docs/adr/0002-tool-description-style.md):
  - Add ### Canonical structure (locked rubric — PR #683 WS3) subsection
    with seven numbered rules (section order, bullet uniformity, header
    casing, indent, blank-line spacing, single canonical EXAMPLE per tool,
    per-tool carve-out allow-list).
  - Add ### Cross-LLM rationale subsection citing the tokenizer-uniformity
    argument across Claude / GPT / Gemini / Llama as the empirical basis
    for the UPPERCASE+colon header shape.
  - Update ### Exemptions and ## Consequences to reflect that ctx_purge is
    no longer deferred — the WS2 rewrite ships with audit-approved
    DESTRUCTIVE/SCOPES/CONTRACT carve-outs allow-listed in the contract
    test, while still meeting the canonical four-section shape.

Contract test extensions (tests/core/server.test.ts):
  - Remove ctx_purge from EXEMPT_FROM_FORBIDDEN_TOKENS and EXEMPT_FROM_WHEN
    (the WS2 rewrite passes the canonical structure with the carve-outs).
  - Add ALLOWED_EXTRA_SECTIONS map carving out DESTRUCTIVE/SCOPES/CONTRACT
    on ctx_purge only, with inline rationale citing Probe 4.
  - Add four new per-tool assertions (run on every non-exempt ctx_* tool):
    1. MUST contain RETURNS: and EXAMPLE: (mandatory presence).
    2. Section order WHEN -> WHEN NOT -> RETURNS -> EXAMPLE (strictly
       increasing flat.indexOf() positions for canonical sections).
    3. UPPERCASE+colon headers must be in the canonical set OR the
       per-tool carve-out list (rejects off-spec sections like CONCURRENCY:
       and TIPS:).
    4. Bullets must be markdown '- ' only (rejects 1./1-/* /•).
  - Add flattenDescription() helper that collapses the literal '\n' escapes
    and joins the "+ \n " concat continuation so the assertions run against
    the shape the host LLM actually sees at tool-selection time.

Two stale-test updates (folded CONCURRENCY: into WHEN: prose):
  - "tool description documents the concurrency field with positive
     guidance" — expect 'parallelize I/O-bound calls' + 'concurrency 4-8'
     + 'CPU-bound or stateful' + 'keep concurrency at 1' (inline now).
  - "PARALLELIZE I/O guidance + locked requests:[] schema in description"
     — expect 'requests: [{url' + 'concurrency 4-8' + 'FTS5 indexing then
     serializes writes' (inline now).

CONTRIBUTING.md L282 compliance: all assertions folded into the existing
tests/core/server.test.ts file; no new test files.

Result: tests/core/server.test.ts goes from 88 to 124 contract assertions
across 7 non-exempt ctx_* tools; all 124 pass. Three pre-existing baseline
failures (ctx_index storage-error + 2 ctx_doctor settings.json) are
environment-specific and not introduced by this PR.

Empirical reference: PR-683-FINALIZE-LOG.md (WS1 verdict table, WS2 probe
design, WS3 before/after section structure).

* fix: skip context-mode redirect echoes in isToolError + rename forbidden_actions test anchor

PR #683 CI failed across all 3 OS on two tests, both downstream of this
PR's own intentional changes:

1. tests/session/continuity.test.ts:79 'outputs additionalContext with
   XML routing block' — Expected <forbidden_actions> tag.

   The PR renamed <forbidden_actions> → <when_not_to_use> in hooks/
   routing-block.mjs (ADR-0002, affirmative framing — describe when NOT
   to reach for a tool instead of declaring it forbidden). The
   continuity test still asserted on the old name. Update the assertion
   to match the new tag + cross-reference ADR-0002 in the failure
   message so a future maintainer who runs `npm test` sees the rename
   instead of a bare diff.

2. tests/opencode-plugin.test.ts:1241 'blocked tool command is replaced
   before execution' — expected snapshot=="", got <session_resume
   events="1"> containing a fake <errors count="1"> with our own echo
   text.

   The PR rewrote the curl/wget/inline-HTTP/WebFetch redirect echo
   from "context-mode: curl/wget blocked. …" to user-friendlier
   "context-mode: curl/wget redirected … retry if it fails with a
   transient DNS error. …". The new copy legitimately mentions failure
   modes ("fails", "transient DNS error"), but `isToolError` at
   src/session/extract.ts:63 keyword-matches /FAIL/i and `failed/i`
   (case-insensitive, no word boundary), so "fails" inside "if it
   fails" triggered a substring match → our OWN guidance echo was
   captured as a session error → next chat would show a fake error in
   <session_resume>.

   Fix: gate isToolError on the unique `context-mode:` prefix. The
   check is defensive at the source — any future copy change to the
   guidance text cannot reintroduce the bug. Match BOTH sides because
   real shell runs report `response = "context-mode: …"` (the echo
   stdout), while the OpenCode plugin test path captures `response =
   'echo "context-mode: …"'` (the raw command itself, never executed).

Verified locally on Node 20:
  npx vitest run tests/session/continuity.test.ts -t "outputs additionalContext"
   → 1 passed
  npx vitest run tests/opencode-plugin.test.ts -t "blocked tool command"
   → 1 passed
  npx vitest run tests/session/  (all 28 files)
   → 594 passed | 4 skipped, no regressions

* fix(routing): drop negation framing from CASE A deny reasons (#683)

All four redirect-style deny reasons in hooks/core/routing.mjs (curl/wget,
inline HTTP, build tools, WebFetch) rewritten to fully positive imperative
voice per ADR-0002 + ADR-0003.

- Removed "(context-window optimization, NOT a network restriction)"
- Removed "Do NOT retry with curl/wget|Bash|WebFetch"
- Replaced "retry if it fails" hedge with imperative "Retry the same call
  on a transient DNS error (EAI_AGAIN, ETIMEDOUT, ENETUNREACH)"

Cross-LLM rationale: negation framing primes LLM attention on the
forbidden item (ironic process theory). Positive routing intent +
explicit capability affirmation + imperative next-action work uniformly
across Claude/GPT/Gemini/Llama.

ADR-0003 amended with §Amendment noting the empirical rationale.
Contract tests in tests/core/server.test.ts gain two guards (PR #683
follow-up) that fail loud if "NOT a network" or "Do NOT retry" reappear.

* test(hooks): update WebFetch + curl deny assertions for affirmative voice (#683)

77f4ec6 rewrote the CASE A deny reasons in hooks/core/routing.mjs to
positive imperative voice. The two existing assertions that pinned the
old "Do NOT retry" / "Think in Code" wording now need to pin the
surviving affirmative anchors instead.

- tests/hooks/integration.test.ts: WebFetch path asserts on the
  "Retry the same call on a transient DNS error" hint and the explicit
  "Call ctx_fetch_and_index" instruction.
- tests/hooks/tool-naming.test.ts: curl redirect path asserts on
  "Call ctx_execute" — the imperative call instruction that absorbed
  the "Think in Code" trainer voice.

Both keep the integration coverage of the redirect path intact while
matching the new wording.

* refactor(server): standardize MCP tool description source format (#683)

Tool descriptions in src/server.ts used two competing source styles —
template literals with embedded "\n\n" escape sequences, and multi-line
string concatenation with "+". Both render identically at runtime but
the inconsistency made the source hard to scan and made the canonical
WHEN/WHEN NOT/RETURNS/EXAMPLE rubric (ADR-0002) harder to enforce by
sight during review.

All multi-section tool descriptions now use a single style: template
literals with real newlines inside the literal. The runtime payload is
unchanged. The forbidden-word + canonical-structure contract tests in
tests/core/server.test.ts (PR #683 WS3) continue to pass against the
normalized source.

* fix(routing+desc): drop org-rationale + normalize RETURNS form (#683)

Second-pass review of PR #683. Two visual / prompt-economy regressions
the first pass missed.

(1) "for context-window efficiency" — org-rationale, not action input.

The first amendment replaced the bare-NOT parenthetical with the
affirmative opener "redirected to <ctx_tool> for context-window
efficiency". The "for X reason" preface is still post-hoc justification
the agent does not need to act on. Compare HTTP 301: the response
carries Location: <new-url> and the client uses it — the server never
appends "for SEO efficiency". The capability affirmation
"<ctx_tool> has full network access" already carries the substantive
signal the rationale was double-encoding.

All four CASE A sites in hooks/core/routing.mjs (curl/wget, inline HTTP,
build tools, WebFetch) now open with "redirected to <ctx_tool>." flat
— affirmative routing intent, no rationale preface.

ADR-0003 gains §Second amendment documenting the rule and rationale.
A new contract test in tests/core/server.test.ts asserts the phrase
"for context-window efficiency" never reappears in any CASE A site.

(2) RETURNS form inconsistency — three tools used inline form.

ADR-0002 L56-57 specifies RETURNS as a header on its own line with the
body indented below (matching WHEN: / WHEN NOT: shape). Three tools
(ctx_execute, ctx_execute_file, ctx_purge) used inline form
("RETURNS: only your printed output.") while four tools used canonical
header+body form. Mert flagged the visual inconsistency on review.

All three inline tools rewritten to header+body form. EXAMPLE: stays
inline per ADR-0002 L59 — the asymmetry is intentional (RETURNS prose
is multi-line capable; EXAMPLE values are one-call-per-line).

A new contract test asserts RETURNS: never appears in inline form on
any multi-section ctx_* tool. Both new guards run alongside the existing
ADR-0002 forbidden-token + canonical-structure suites.

* feat(desc): surface auto-captured session memory in ctx_search + ctx_purge (#683)

context-mode captures 23 categories of structured events at hook time
(decisions, errors, blockers, plans, user prompts, rejected approaches,
file ops, git ops, tasks, latency, MCP tool counts, etc.) and persists
them across compaction. The mechanism is documented in README and the
project CLAUDE.md routing block. The MCP tool descriptions themselves
do not surface this — so at tool-selection time the LLM only sees
"search indexed content" and misses the much larger
search-session-memory capability.

ctx_search description gains:
  - A 4th WHEN: bullet calling out session-memory queries as a valid
    use case alongside indexed content.
  - A RETURNS: note listing the common session-memory source labels
    (decision, error, error-resolution, blocker, plan, user-prompt,
    rejected-approach, compaction) and a pointer to ctx_stats for
    live category counts.
  - A second EXAMPLE: showing a timeline-sorted decision lookup —
    permitted under ADR-0002 L103-106 (tools with two valid input
    shapes MAY include two EXAMPLE lines).

ctx_purge SCOPES block gains:
  - Per-session: clarifies "events" means auto-captured session
    memory so the agent knows what is being deleted.
  - Per-project: adds a "use ctx_stats first to preview category
    counts" hint to prevent destructive surprises.

No structural changes: WHEN/WHEN NOT/RETURNS/EXAMPLE canonical order
preserved, header-on-own-line RETURNS form preserved, no forbidden
tokens introduced, no org-rationale prefaces. 131 ADR-0002 forbidden-
token tests + 7 canonical-structure tests + 12 PR #683 contract tests
all pass.

* feat(desc): principle-first rewrite of ctx_execute/_file + truth-fix ctx_index/fetch (#683)

Five maintainer critiques on tool descriptions, addressed together. Two
overarching principles emerged from the review:

1. PRINCIPLE OVER HEURISTIC. The LLM picks a tool BEFORE seeing the
   output. "Use when output >= 20 lines" is unactionable — the LLM
   can't predict that. Replace with the INTENT principle: "use when you
   intend to derive an answer FROM the data". The LLM knows its own
   intent at tool-selection time.

2. TRUTHFUL RETURN DESCRIPTIONS. ctx_index handler at L2022 returns
   chunk count + source label + ctx_search call hint — NOT a summary.
   The headline "Only a brief summary is returned" was false. Same
   false-claim pattern existed in ctx_fetch_and_index RETURNS
   ("plus an indexing summary"). Reframe as "indexing metadata" and
   make explicit that raw content is NOT echoed back.

ctx_execute (largest rewrite):

- Headline + philosophy paragraph: Think-in-Code is now taught
  explicitly with the concrete 47-files / 700 KB → 3.6 KB example
  (700 KB into conversation vs 3.6 KB summary printed). The principle
  is the load-bearing concept, not the implementation detail.
- WHEN bullets reframed around INTENT (derive / parse / aggregate)
  rather than predicted output size.
- WHEN NOT bullets reframed around INTENT (observe vs process) rather
  than enumerated command shapes.
- Examples replaced: 'npm test | tail -40' (naive truncate) → smart
  grep for failure-relevant lines; awkward 'aws' example → gh JSON
  query with filter+count in code.

ctx_execute_file:

- Same Think-in-Code framing scoped to single-file analysis.
- "raw contents would flood context" reframed in LLM-native terms
  ("every byte you Read enters your conversation memory and costs
  reasoning capacity for the rest of the session"). The LLM does not
  necessarily know which host it is running in or what "context" means
  in technical terms — describe the actual cost in cognitive terms it
  reasons about.
- Better examples: error filtering with count + tail; CSV row count
  with header read.

ctx_index:

- Headline corrected: stores raw content, returns indexing metadata
  + retrieval hint. Nothing is summarized.
- RETURNS body lists what is actually returned: chunk counts (total,
  code-bearing), source label, ctx_search call shape. Makes explicit
  that the raw content is NOT echoed back — it lives in storage and
  is retrievable via ctx_search.
- Added second EXAMPLE showing file-backed path (auto-refresh hash).

ctx_fetch_and_index:

- "raw HTML entering context" → "raw page bytes should NOT enter your
  conversation memory" (same LLM-native phrasing principle).
- RETURNS "plus an indexing summary" → "plus indexing metadata"
  + explicit "Raw content is NOT echoed back" — eradicates the same
  false-claim pattern.

131 ADR-0002 forbidden-token tests + 7 canonical-structure tests + 12
PR #683 contract tests all pass. No structural changes — WHEN / WHEN
NOT / RETURNS / EXAMPLE canonical order preserved, header-on-own-line
RETURNS preserved, no forbidden tokens introduced.

* feat(desc): comprehensive description batch — ranking, TTL custom, capabilities, technical depth (#683)

Maintainer's third-pass review covered ten distinct critiques. Addressed
together because they all stem from the same direction: surface the
load-bearing mechanism the LLM needs to act correctly, in terms the LLM
understands at tool-selection time.

ctx_search — full rewrite. The previous headline ("BM25 over FTS5") sold
the tool short. The actual ranking pipeline is BM25 + Reciprocal Rank
Fusion over two parallel tokenizers (Porter stemming + trigram
substring), plus a proximity rerank pass for multi-term queries, plus
Levenshtein typo correction, plus window-extracted smart snippets.
The knowledge base is unified: ctx_search reaches both content the user
indexed AND auto-captured session memory (26 event categories). WHEN
NOT bullets reframed to intent ("you have an ad-hoc question") so the
tool-name references don't get lost across long conversations.
contentType code|prose filter surfaced. Four EXAMPLE lines cover the
four most common shapes (source-scoped batch, timeline-sorted memory,
contentType-filtered, multi-source recall).

ctx_fetch_and_index — custom TTL (PR #666) now first-class. Default
24h, override per-call with ttl: <ms>, ttl: 0 bypasses like force:true.
Removed the "~3KB" hard preview claim — replaced with mechanism prose.
RETURNS explains the FTS5 single-writer reality and net-latency math
(parallel-fetch + serial-index). Concurrency guardrails kept generic
(I/O-bound 4-8, lower for rate-limited hosts) — no third-party API
specifics (Mert flag: not our policy to editorialize on someone else's
API contract).

ctx_batch_execute — Think-in-Code restored as the load-bearing concept
("concurrency parallelizes FETCH; derivation belongs in code"). Same
generic concurrency guardrails as ctx_fetch_and_index. EXAMPLE shows
the pattern with a summarize-step command at the end.

ctx_execute — background and intent capabilities now surfaced in
description (previously only in the schema field describe). background
covers server/daemon detach. intent triggers auto-index of large output
into the knowledge base, with title+preview return instead of raw stdout.

ctx_execute_file — same intent surfacing for file-derivation output.

ctx_insight — port (default 4747) and sessionDir/contentDir overrides
surfaced. Useful for diagnosing multi-install setups or pointing at a
sibling project's data.

README.md — TTL Cache section rewritten for custom TTL. The "Fresh (<24h)"
hard claim is now "Cache hit (within TTL)" with the default-and-override
mechanism explained. Tools table row for ctx_fetch_and_index updated to
match.

Two contract tests (tests/core/server.test.ts) that pinned the old exact
phrasing for batch_execute / fetch_and_index concurrency guidance now
pin the LOAD-BEARING CONCEPTS via regex (4-8 window, CPU/stateful keep
at 1, FTS5 serial-write) — same coverage, immune to future copy-edits.

Three pre-existing PR #617 failures (ctx_doctor + ctx_index storage path
e2e under CONTEXT_MODE_DIR) remain — out of scope, separate work.

* feat(hooks): apply principle-first pattern to routing-block + 4 CASE A deny reasons (#683)

The MCP tool descriptions in src/server.ts were rewritten over the
previous PR #683 commits to follow a consistent pattern: principle
over heuristic, intent over threshold, LLM-native phrasing over jargon,
Think-in-Code as the load-bearing concept. The hook layer
(routing-block.mjs system-prompt injection + routing.mjs CASE A deny
reasons) had not yet been brought to the same standard.

Two maintainer flags drove the work:
- "bash with output >20 lines → use ..." — the LLM cannot predict
  output size BEFORE running. Threshold-based heuristic, same anti-
  pattern as the original ctx_execute description.
- The curl/wget deny reason doesn't follow the same pattern as the
  description rewrites — overloaded with implementation prescription
  ("Write pure JS with try/catch, no npm deps"), missing principle.

routing-block.mjs (system-prompt injection on every session):

- <priority_instructions> reframed in cognitive-cost terms: "every
  byte enters your conversation memory and costs reasoning capacity
  for the rest of the session". Think-in-Code surfaced explicitly
  ("program the analysis, do not compute it by reading raw data").
- <tool_selection_hierarchy> entries enriched: ctx_search now
  describes the auto-captured session memory it reaches; ctx_batch_-
  execute explains why batching matters (round-trip cost paid once);
  PROCESSING entry frames around derivation intent rather than the
  old "API calls, log analysis, data processing" enumeration.
- <when_not_to_use> intent-based, no thresholds: "intend to PROCESS
  the output" vs "intend to OBSERVE a short fixed output". Same shape
  for Read (analyze vs Edit), WebFetch, ctx_execute/_file file writes.

createReadGuidance / createGrepGuidance / createBashGuidance /
createExternalMcpGuidance:

- All four rewritten with the same intent-based / principle-first
  framing. "May flood context" / "May produce large output" /
  "with output >20 lines" all replaced with intent triggers ("when
  you intend to PROCESS / count / filter / aggregate").
- LLM-native phrasing throughout: "your conversation" not "context",
  "your derived answer" not "your printed summary" (the latter
  implies a pre-shaped narrative; the former matches what code
  actually produces).
- Bash carve-out preserved (mutating state / observational commands)
  but expressed as intent shapes, not enumerated commands.

hooks/core/routing.mjs CASE A deny reasons (curl/wget, inline HTTP,
build tool, WebFetch):

- All four open with the bare "redirected" verb (no preamble), then
  go straight into the imperative call with the principle embedded
  ("derive your answer in code, and print only the result — the raw
  HTTP body stays in the sandbox instead of entering your
  conversation").
- Selection criteria added where multiple paths exist (curl: inline
  derivation via ctx_execute vs persist-for-later-query via
  ctx_fetch_and_index; WebFetch: same).
- "Write pure JS with try/catch, no npm deps" preachiness removed —
  the LLM gets that info from the ctx_execute schema when it actually
  calls the tool. The deny reason stays focused on routing.
- Build tool deny reason now also nudges toward smarter filtering
  (grep over ERROR|warning|FAIL patterns) instead of leaving the
  agent with naive `tail -30` as the only suggested shape.

All four CASE A sites still satisfy the ADR-0003 contract: open with
"redirected", name at least one ctx_* alternative, contain no bare
"BLOCKED" / "NOT a network" / "Do NOT retry" / "for context-window
efficiency". 635 tests pass (was 579 before this batch), zero new
failures. The 3 remaining failures are pre-existing PR #617 ctx_doctor
/ ctx_index storage-path e2e tests — out of scope.
2026-05-24 13:48:42 +03:00
+2 3d8db08bd7 feat: add runtime storage override (#617)
* feat: add runtime storage override

Co-authored-by: Codex <noreply@openai.com>

* test: update storage path source assertions

Co-authored-by: Codex <noreply@openai.com>

* test: normalize storage path expectations

Co-authored-by: Codex <noreply@openai.com>

* fix runtime storage override resolution

Consolidate storage overrides on CONTEXT_MODE_DIR, share resolver behavior across server, hooks, and statusline, and update docs/tests around the Codex Desktop failure contract.

Co-authored-by: Codex <noreply@openai.com>

* fix windows storage override tests

Make storage-path resolver expectations platform-aware and keep statusline multi-adapter tests on adapter-default discovery instead of the root override path.

Co-authored-by: Codex <noreply@openai.com>

* address runtime storage review

Compose tool registration wrappers, memoize storage writability checks, report storage roots in ctx_doctor, keep legacy statusline session-dir compatibility, and fold resolver tests into existing server coverage.

Co-authored-by: Codex <noreply@openai.com>

* fix windows storage path test

Use an absolute temp-directory fixture instead of a POSIX-rooted default path so resolver tests assert storage behavior consistently across platforms.

Co-authored-by: Codex <noreply@openai.com>

* fold storage resolver into session db

Move CONTEXT_MODE_DIR resolver exports into the existing session DB module so hooks and statusline can use the existing session-db bundle. Drop the new storage-paths source and hook bundle entries while preserving shared storage behavior.

Co-authored-by: Codex <noreply@openai.com>

* consolidate default storage session roots

Add the shared default session-dir helper to the existing session DB bundle boundary, route server/hooks/statusline through it, and document why storage resolution lives there. Expand storage resolver tests for the shared helper.

Co-authored-by: Codex <noreply@openai.com>

* fix codex default session guard

Update the source assertion for the shared default-session-dir helper so the CI guard checks CODEX_HOME via configDirEnvForSessionSegments instead of the removed direct Codex config import.

Co-authored-by: Codex <noreply@openai.com>

* 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>

* 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>

* fix(server): add z.preprocess coercions to ctx_fetch_and_index force and requests params (#679)

fix(server): add z.preprocess coercions to ctx_fetch_and_index params

The OpenCode/Kilo in-process native plugin bridge stringifies primitive types. Other tools already use z.preprocess(coerceBoolean/coerceJsonArray) to handle this, but ctx_fetch_and_index was missing these wrappers on its force and requests parameters.

Added schema-level test verifying preprocess wrappers are present. Updated existing schema tests to match new structure.

* feat(fetch): add per-call cache ttl (#666)

* feat(fetch): add per-call cache ttl

* test: run npm pack dry-run on Windows

* fix(batch_execute): preserve heredoc commands (#657)

Stops appending `2>&1` to user command strings in `runBatchCommands()` — that mutation broke heredoc terminators (`NODE` → `NODE 2>&1`). Now executes commands as-written and merges executor-captured stdout+stderr via new `combineExecOutput()` helper. Symmetric across serial + parallel paths.

Adds serial + parallel regression tests for heredoc commands with stderr. Updates the nodeOptsPrefix edge-case test to reflect the new behavior.

Bundles taken from `next` (CI-authoritative); regenerate on next main push.

Fixes #656.

Co-Authored-By: Noctivoro <nick@movermarketing.ai>

* fix(server): surrogate-safe preview truncation in ctx_fetch_and_index (#659) (#660)

The fetch-preview path at src/server.ts truncated `f.markdown` with
`String.prototype.slice(0, FETCH_PREVIEW_LIMIT)` where the limit
(3072) is a UTF-16 code-unit count. When the cut fell between the two
halves of an astral-plane character (e.g. 🟡 = U+1F7E1 = 🟡),
the high surrogate remained and the low surrogate was dropped.
JSON.stringify then emitted the orphan as a literal `\uD83D` escape
in the tool_result body, causing RFC 8259-strict consumers (the host
LLM API) to reject the next request with `400 ... no low surrogate
in string`. Sessions could not recover without removing the bad
message from the transcript.

Adds a new `charSafePrefix(str, maxChars)` export to `src/truncate.ts`
mirroring the existing internal `byteSafePrefix` semantics: cap by
UTF-16 code units, back off one unit if the cut would split a
surrogate pair. Wires it into the fetch-preview construction.

Tests cover the helper directly plus a regression that walks the
exact preview-construction pattern with an emoji at the LIMIT
boundary and asserts the resulting JSON contains no orphan high
surrogate and round-trips through a strict parser.

Other `.slice(0, N)` sites in src/ (small label/error/timestamp
truncations) are out of scope for this PR — they have low bounds and
their inputs are unlikely to contain emoji at the boundary. Happy to
extend coverage in a follow-up if the maintainer wants to remove the
class of bug entirely.

Fixes #659

* fix(auto-memory): scope memory dir by projectDir to stop cross-project leak (#663) (#664)

* fix(auto-memory): scope memory dir by projectDir to stop cross-project leak (#663)

getMemoryDir() ignored projectDir, so every adapter (except OpenClaw, whose
configDir IS the project root) returned a path shared by every project on
the machine. Two terminals open in different repos read each other's .md
memory files via searchAutoMemory(), then those notes contaminated
ctx_search timeline results.

Fix: HookAdapter.getMemoryDir now accepts an optional projectDir. When
supplied, the path is scoped via hashProjectDirCanonical(projectDir) under
the existing base. searchAutoMemory passes projectDir through; the
adapterless legacy fallback applies the same hash directly so the contract
holds at both call sites. Reuses the same canonicalization as ContentStore
and searchEvents, so all three project-scoped surfaces share one identity.

Backwards compat: getMemoryDir() without projectDir keeps returning the
unscoped path so external adapter consumers don't break.

Tests: leak canary (write under projectA scope, search from projectB,
assert 0 hits) + positive control + #663 negative test pinning that the
old unscoped path is no longer surfaced.

* test(windows): fix two flaky Windows-only CI failures

- asymmetric-drift-assert: spawnSync("npm", ...) needs shell:true on
  Windows because npm resolves to npm.cmd. Without it Node returned
  status=null/stderr=null/stdout=null and the assertion failed with no
  diagnostic. Also surface r.error in the assertion message.
- server.test.ts "python: cap works with python scripts": replace the
  10k-iteration Python loop with a single 100KB write. The cap still
  triggers (and stderr still gets "output capped"), but the test no
  longer races the 10s timeout on slow Windows CI VMs.

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Baijack-star <71923891+Baijack-star@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: ByF <222546298+ByronFinn@users.noreply.github.com>
Co-authored-by: baifan <ubuntu@BaiFanPC.localdomain>
Co-authored-by: LeoNardo <58056860+LeoNardo-LB@users.noreply.github.com>
Co-authored-by: NgoQuocViet2001 <123613986+NgoQuocViet2001@users.noreply.github.com>
Co-authored-by: /noctivoro-x <nick@movermarketing.ai>
Co-authored-by: ccheng555 <ccheng5@gmail.com>
Co-authored-by: Seba Breguel <62109266+sebastianbreguel@users.noreply.github.com>
Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
2026-05-24 02:39:34 +03:00
Mickey LazarevicandMert Köseoğlu 3e51e53ba1 feat(opencode): streamline installation process and improve CLI upgrade (#650)
* feat(opencode): streamline installation process and improve CLI upgrade

Remove redundant global npm install step from installation instructions for both OpenCode and KiloCode integrations. Refactor CLI upgrade command to omit global package updates for these platforms.

* refactor: extract opencode/kilo platform check into helper

Use helper in getPluginRoot and upgrade to gate global npm install
Add test for gate placement in upgrade

---------

Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
2026-05-20 14:02:41 +02:00
Mert Koseoglu ccd5911e2c docs(readme): remove dead Lifecycle env-var section + complete redaction list
Two adjacent README claims drift away from the code on origin/next:

1. The "Lifecycle environment variables" section documents
   `CONTEXT_MODE_STARTUP_SWEEP` as a default-on runtime knob. The env
   var is not read anywhere in src/, hooks/, scripts/, or tests/ on
   the current tree — the underlying `startupSiblingSweep()` call site
   was reverted in #602 ("revert: lifecycle idle-shutdown") along with
   the idle-shutdown class it was paired with. The same #602 commit
   body explicitly states: "Neither env var is read anywhere in src/,
   tests/, or scripts/ after the revert — leaving the README section
   in place would leak dead configuration knobs to users." #602 did
   delete the section; commit 09efefd (OpenCode plugin migration,
   #597) accidentally re-added it during a README-region rewrite.
   Also, the section's "removed after #592" reference is the wrong PR
   — #592 only made idle opt-in; #602 was the full revert. Removing
   the whole orphan section is the right correction.

2. The MCP redaction sentence lists 8 keys but the actual matcher in
   `hooks/posttooluse.mjs` covers 17 (incl. `auth_token`, `access_token`,
   `refresh_token`, `bearer`, `passwd`, `pwd`, `apikey`, `x_api_key`,
   `set-cookie`, `client_secret`). Rewritten to enumerate the full
   regex coverage and to point at the source file so a future regex
   tweak surfaces in code review against the README.

Empirically verified against origin/next at sha 1147411:
  - grep -r CONTEXT_MODE_STARTUP_SWEEP src/ hooks/ scripts/ tests/ → 0 hits
  - grep -r CONTEXT_MODE_IDLE_TIMEOUT_MS src/ hooks/ scripts/ tests/ → 0 hits
  - hooks/posttooluse.mjs redaction regex confirms all 17 keys
2026-05-20 10:41:55 +03:00
Mert Koseoglu 0c838a3dea docs(readme): clarify plugin+legacy-mcp coexistence in OpenCode/KiloCode sections
Adds symptom→cause→fix guidance for the v1.0.137-139 silent-suppression
edge case (#623). Mentions v1.0.140+ stderr diagnostic so users hitting
'zero ctx_* tools' can find their way to the upgrade command.
2026-05-19 00:04:03 +03:00
Omer Cohen eb1a629ed8 docs(opencode): note plugin-native upgrade cleanup (#619) 2026-05-18 22:08:07 +03:00
Omer CohenandOusama Ben Younes 09efefdc4a feat(opencode): register ctx tools natively via plugin (#574) (#597)
Move OpenCode/Kilo from plugin+MCP dual registration to plugin-native ctx_*
tools. The plugin now imports the shared server tool registry without
starting stdio, exposes all 11 ctx_* tools via the OpenCode/Kilo tool map,
and uses AsyncLocalStorage to pass project/session context into existing
handlers without a process.env race.

Upgrade safety for existing users:
- configureAllHooks removes only legacy mcp.context-mode while preserving
  other MCP servers.
- doctor warns when a legacy mcp.context-mode block remains and points to
  context-mode upgrade.
- stale legacy OpenCode/Kilo MCP children suppress ctx_* registration and
  become no-op rather than exposing duplicate tools.

Safety cleanup:
- Remove CONTEXT_MODE_IDLE_TIMEOUT_MS entirely; plugin-native tools remove
  the need for timer-driven MCP death, and timer shutdown was unsafe for
  hosts that keep registered tool handles.
- Guard process-wide exception handlers so importing server.js for native
  tools does not alter OpenCode/Kilo host crash semantics.
- Scope CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS to the dynamic import and restore
  it so child commands do not inherit the internal guard.

Tests cover native tool registration, native ctx_stats execution, host
side-effect leakage, native session attribution, legacy MCP config cleanup,
doctor warning, and stale MCP no-op predicate.

Refs: #574, #565, #592

Co-authored-by: Ousama Ben Younes <benyounes.ousama@gmail.com>
2026-05-18 17:10:16 +02:00
Ben Younes a888cac640 revert: lifecycle idle-shutdown (#568 + #595) — restore tools on 12 hosts (#602)
* Revert "fix(lifecycle): make MCP idle shutdown opt-in outside OpenCode/Kilo (#592) (#595)"

This reverts commit 5bcff7eda9.

* Revert "fix(lifecycle): idle self-shutdown + generalized sibling sweep (#565) (#568)"

This reverts commit ac11f10333.

* ci: rebuild bundles after lifecycle revert

Regenerates cli.bundle.mjs, server.bundle.mjs and hooks/session-extract.bundle.mjs
to match the source state after the revert of #568 and #595. No source change in
this commit — just minifier output realignment.

* docs(readme): remove orphan lifecycle env var section after #568+#595 revert

The section documented CONTEXT_MODE_IDLE_TIMEOUT_MS (added in #568, reverted)
and CONTEXT_MODE_STARTUP_SWEEP (added in #568 and tweaked in a follow-up commit
61e680f, the underlying startupSiblingSweep() call site reverted). Neither env
var is read anywhere in src/, tests/, or scripts/ after the revert — leaving
the README section in place would leak dead configuration knobs to users.
2026-05-18 10:13:30 +02:00
572d451f8d fix(codex): harden plugin hook routing (#575)
* fix(claude-config): static-import detect to fix Node 22.5 require(esm) gate

CI run 25877550371 failed on all 3 OS with 12 silent-empty assertion
errors in tests/security.test.ts > cross-adapter deny-policy parity:
every per-adapter test returned `policies = []` instead of the seeded
deny pattern. Local Node 20.19.2 + Node 22.22.2 both pass the same
tests; only CI's Node 22.5 was broken.

Root cause: `resolveAdapterGlobalSettingsPaths` lazy-loaded
`../adapters/detect.js` via
  createRequire(import.meta.url)("../adapters/detect.js")
Both files compile to ESM under `"type": "module"`. `require()` of an
ESM module is FLAG-GATED on Node 22.x before 22.12 — it needs
`--experimental-require-module`. CI's actions/setup-node@v4 install of
Node 22.5 does not pass that flag by default. The require throws
ERR_REQUIRE_ESM, the surrounding try/catch eats it silently, the
function returns `[claudeGlobalPath]` only, and the test's seeded
`<fakeHome>/.cursor/settings.json` is never read → empty policies →
assertion fails on every adapter (cursor, codex, qwen-code, gemini-cli,
jetbrains-copilot, vscode-copilot) × 2 patterns = 12 fails per OS.

Why Node 20 and 22.22.2 passed locally: 20.x has require(esm) under the
same flag but vitest's test runner sets it via NODE_OPTIONS in its node
import; 22.12+ has it default-on. Node 22.5 specifically lives in the
flag-gated window with no implicit enabler — which is exactly the
version CI hopped to in 1b7d7a7.

Fix: replace the lazy createRequire dance with a static ESM import of
the two pure functions (`detectPlatform`, `getSessionDirSegments`). The
original "cycle worry" comment was hypothetical — detect.ts only
imports node: builtins, `./types.js` (type-only), and `./client-map.js`
(pure data). It does NOT import claude-config back. No cycle. Static
import sidesteps the require(esm) flag entirely, so the same code path
works on every supported Node version (20, 22.5, 22.12+, 24+).

Verified locally on Node 20:
  npx vitest run tests/security.test.ts
  → 94 passed (94)
  npx vitest run  (full suite)
  → 3327 passed | 29 skipped (3356) — 147 files

* fix(codex): harden plugin hook routing

Ship Codex plugin hooks, pin hook execution to the Codex platform, and route exec_command cmd payloads through Bash policy checks.

Co-authored-by: Codex <noreply@openai.com>

* fix(codex): harden plugin hook routing

Ship Codex plugin hooks, pin hook execution to the Codex platform, and route exec_command cmd payloads through Bash policy checks.

Co-authored-by: Codex <noreply@openai.com>

* test(codex): relax hook manifest assertions

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
2026-05-17 11:12:38 +02:00
Omer CohenandUbuntu 5bcff7eda9 fix(lifecycle): make MCP idle shutdown opt-in outside OpenCode/Kilo (#592) (#595)
Issue #592 confirms the #568 idle self-shutdown default is unsafe for
Claude Code/Codex-style MCP hosts: after context-mode exits cleanly on
idle, the host can keep registered ctx_* tool handles but mark the MCP
server disconnected, leaving tools stale until the user reconnects or
restarts. #583 fixed Pi by making its bridge respawn, but Claude Code
and Codex host MCP clients are outside this repo and cannot be patched
from lifecycle.ts.

Change the policy from global default-on to host opt-in:

- idleTimeoutForEnv({}) now returns 0 (disabled), not 900000.
- malformed/negative CONTEXT_MODE_IDLE_TIMEOUT_MS also falls back to 0,
  the safe default.
- positive env values still opt in exactly as before.
- OpenCode and KiloCode configs explicitly set
  CONTEXT_MODE_IDLE_TIMEOUT_MS=900000 because those hosts were the
  original #565 accumulation case (one MCP child per session/subagent).
- README lifecycle docs now describe the conservative default and why
  Claude Code/Codex/editor MCP clients should not idle-exit unless users
  explicitly opt in.

Regression tests:
- lifecycle.test.ts pins missing/malformed env => 0 and explicit env =>
  honored.
- opencode-idle-config.test.ts pins OpenCode and KiloCode shipped configs
  as explicit 15 min opt-ins, preventing accidental reintroduction of a
  global default or removal of the targeted host opt-in.

Verification:
- npx tsc --noEmit: PASS
- npm run build: PASS
- npx vitest run tests/lifecycle.test.ts tests/adapters/opencode-idle-config.test.ts: PASS (26/26)
- npx vitest run: same 3 pre-existing environment-dependent failures on
  this workstation (VSCODE_PID inheritance, JetBrains IDEA_INITIAL_DIRECTORY),
  unchanged from upstream/next baseline.

Refs: #592, #568, #565, #583

Co-authored-by: Ubuntu <omer@Omer.dn3uxh3znnmu5eefnjnut0i1af.tlvx.internal.cloudapp.net>
2026-05-17 11:43:04 +03:00
7ae9ee7fc3 feat: Make context-mode output collapsible (#594)
* ci: update install stats

* ci: update install stats

* feat: Make context-mode output collapsible

* docs: Document collapsible feature for Pi

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
2026-05-17 10:26:17 +03:00
Ben YounesandClaude Opus 4.7 752d5236ec fix(routing): re-fire external-MCP nudge periodically, not once per session (#593)
#529 added a one-shot PreToolUse guidance for external MCP tools
(slack/jira/notion/gdrive…) telling the agent to wrap large payloads in
ctx_execute. In MCP-heavy sessions (#567 follow-up reports 55 Jira calls,
context growing 24K → 75K tokens) the single nudge is dropped by context
compaction and later tool responses flood the conversation unchecked.

Replace `guidanceOnce("external-mcp", …)` with a new `guidancePeriodic`
helper that fires on calls 1, N+1, 2·N+1, … with N defaulting to 10.
Tunable via `CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY` (range [1, 100],
invalid values fall back to 10). Bash/Read/Grep nudges keep their
one-shot behavior — they advise on tool *category* usage, not per-call
flood pressure.

Counter is in-memory + file-backed (`<guidanceDir>/<type>.count`) so it
stays coherent across hook process invocations within the same session.
On any IO/parse failure we fire (lose a counter rather than silently drop
the advisory).

Test plan
- npx vitest run tests/hooks/core-routing.test.ts → 80 passed
- npm run typecheck → clean
- New tests cover: default cadence over 22 calls, env-tuned cadence,
  invalid env value fallback, resetGuidanceThrottle clears the counter.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:39:44 +03:00
Mert Koseoglu 61e680f1f3 fix(v1.0.132): stats measurement + #563/#564/#567/#569 + PR follow-ups
- stats: populate bytes_avoided for external_ref via ctx_fetch_and_index preamble; indexer chunks now FK-attributed (chunks.session_id/event_id) at all 7 callers in server.ts
- #563: drop .refine() from ctx_purge schema (MCP SDK normalizeObjectSchema requires .shape); ambiguity check moved to handler; class-wide CI guard added for all 11 tools
- #564: engines.node>=22.5 + scripts/postinstall.mjs hard-fail on Linux+Node<22.5+no-Bun + ctx_doctor RED FAIL + README/docs sync to canonical 22.5 floor
- #567: vscode-copilot + jetbrains-copilot mcp.json npx-y -> global context-mode (npx-y was scaffold residue from Mar 2026, ghost-installs bypass user's npm i -g causing better-sqlite3 ABI mismatch)
- #569: anti-pattern docs centralized to anti-patterns.md §8 + SKILL.md ref (capture-vs-filter principle, no tool enumeration)
- #571 follow-up: vswhere timeout 5s->15s, year regex caps at currentYear+5
- #568 follow-up: documented CONTEXT_MODE_IDLE_TIMEOUT_MS + CONTEXT_MODE_STARTUP_SWEEP env vars; realpath guard in lifecycle-e2e-real-binary.test.ts
2026-05-14 20:46:51 +03:00
Mert Koseoglu 2607f21de2 docs(csharp): bump language count 11 to 12 across docs
PR #546 added C# (dotnet-script) as the 12th supported runtime, but the
documentation across README, CONTRIBUTING, BENCHMARK, and llms-full kept
counting 11 languages and missing C# from the language enumerations and
runtime tables. Bring docs in line with the executor's actual capability.
2026-05-13 16:36:55 +03:00
Mert Koseoglu 675fec69c9 fix(codex): drop look-around from PreToolUse matcher (closes #547 partial — 1 of 1)
v1.0.124 shipped Codex matchers containing `(?!.*context-mode)` and
`(?!plugin_context-mode_)`. Codex's Rust `regex` crate does NOT support
look-around, so it rejects the matcher at boot with "look-around not
supported" — every Codex user on v1.0.124 is broken. Pin Codex matchers
to charset `[A-Za-z0-9_|]` so `is_exact_matcher`
(refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152)
short-circuits the regex engine entirely. Hook BODY's `isExternalMcpTool()`
filter (hooks/core/routing.mjs:379) preserves end-to-end semantics.

RED→GREEN: tests/adapters/codex.test.ts "Codex matcher #547 — is_exact_matcher charset compliance"
2026-05-13 16:28:42 +03:00
Mert Koseoglu ac0c761149 feat(kiro): route external MCP tools through PreToolUse (closes #529 partial — kiro)
Extends PR #532's external-MCP routing protection to Kiro. Adds
`@(?!context-mode/)` to the preToolUse matcher (Kiro's MCP wire shape is
`@<server>/<tool>`, with own tools at `@context-mode/ctx_*` — verified via
hooks/core/tool-naming.mjs and src/adapters/kiro/hooks.ts:47-49).

The routing.mjs `isExternalMcpTool` extension landed with the cursor slice
already covers the `@<server>/<tool>` shape, so this slice only needs the
matcher-list addition + config-file mirroring.

- src/adapters/kiro/hooks.ts: export EXTERNAL_MCP_MATCHER_PATTERN, append to PRE_TOOL_USE_MATCHERS
- configs/kiro/agent.json: mirror runtime matcher
- README.md: keep docs in sync
- tests/adapters/kiro-external-mcp-routing.test.ts: 6-test vertical slice (matcher regex, adapter config, routing.mjs integration for `@context-mode/ctx_*` vs `@other/foo`)
2026-05-12 13:19:45 +03:00
Mert Koseoglu 742456552f feat(qwen-code): route external MCP tools through PreToolUse (closes #529 partial — qwen-code)
Extends PR #532's external-MCP routing protection to Qwen Code. Adds the new
`hooks.ts` module exporting `EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!.*context-mode)"`
and appends it to both `generateHookConfig` and the `configureAllHooks` upgrade
path so slack / telegram / gdrive / notion-style MCPs trigger the
context-guidance nudge before their large payloads flood context.

Verified Qwen's MCP wire shape: as a Gemini fork
(packages/core/src/tools/tool-names.ts) it uses `mcp__<server>__<tool>`. Own
context-mode MCP surfaces as both `mcp__plugin_context-mode_context-mode__*`
(Claude marketplace shim) and `mcp__context-mode__*` (Qwen canonical via
tool-naming.mjs). The negative lookahead excludes both.

- src/adapters/qwen-code/hooks.ts: NEW — export EXTERNAL_MCP_MATCHER_PATTERN
- src/adapters/qwen-code/index.ts: append matcher in both code paths
- README.md: keep docs in sync
- tests/adapters/qwen-code-external-mcp-routing.test.ts: 3-test vertical slice
2026-05-12 13:19:45 +03:00
Mert Koseoglu 3900db8c4a feat(gemini-cli): route external MCP tools through PreToolUse (closes #529 partial — gemini-cli)
Extends PR #532's external-MCP routing protection to Gemini CLI. Adds
`mcp__(?!.*context-mode)` to the BeforeTool matcher so slack / telegram /
gdrive / notion-style MCPs trigger the context-guidance nudge before their
large payloads flood the model's context window.

Verified Gemini's MCP wire shape (`mcp__<server>__<tool>`) via
hooks/core/tool-naming.mjs (`mcp__context-mode__<tool>` for own MCP). The
negative lookahead excludes any `mcp__` server segment containing
`context-mode` so both the canonical own-MCP and the Claude shim
(`mcp__plugin_context-mode_*`) are skipped. Also broadens the explicit own-MCP
matcher to include the canonical `mcp__context-mode` prefix (previously only
`mcp__plugin_context-mode` was listed — a latent Gemini-routing gap).

- src/adapters/gemini-cli/hooks.ts: export EXTERNAL_MCP_MATCHER_PATTERN
- src/adapters/gemini-cli/index.ts: append + broaden own-MCP coverage
- README.md: keep docs in sync
- tests/adapters/gemini-cli-external-mcp-routing.test.ts: 3-test vertical slice
2026-05-12 13:19:45 +03:00
Mert Koseoglu 996f8c4420 feat(codex): route external MCP tools through PreToolUse (closes #529 partial — codex)
Extends PR #532's external-MCP routing protection from Claude Code to Codex CLI.
Adds `mcp__(?!.*context-mode)` to the PreToolUse matcher so slack / telegram /
gdrive / notion-style MCPs trigger the context-guidance nudge before their
large payloads flood the model's context window.

Verified Codex MCP wire shape (`mcp__<server>__<tool>`) via configs/codex/hooks.json
existing matchers and tests/adapters/codex.test.ts:56-141. Codex own context-mode
tools surface as both bare (ctx_*) and `mcp__<server>__ctx_*` — both already
wired by the explicit entries above the new catch-all, and the negative
lookahead excludes any `mcp__` server segment containing `context-mode` to
prevent double-firing.

- src/adapters/codex/hooks.ts: export EXTERNAL_MCP_MATCHER_PATTERN
- src/adapters/codex/index.ts: append the pattern to PRE_TOOL_USE_MATCHER_PATTERN
- configs/codex/hooks.json: mirror runtime matcher
- README.md: update documented Codex matcher (drift-guarded by codex.test.ts)
- tests/adapters/codex-external-mcp-routing.test.ts: 4-test vertical slice
2026-05-12 13:19:45 +03:00
Mert Koseoglu d05a37584e feat(prose): retire prose-style enforcement entirely (#482)
Issue #482 (makoMakoGo) reported that context-mode's caveman/terse
injection pressures the model toward brevity on its FINAL ANSWER, not
just on tool-output reporting. Cited evidence: Moonshot AI on
kimi-k2.5 (anomalyco/opencode#20258, PR #20259) — aggressive brevity
prompts measurably degrade coding/reasoning benchmarks because the
model drops assumptions, caveats, verification evidence, failure
modes, and security warnings the user actually needs.

Considered:
  A — config switch ("injectCommunicationStyle: false"). Rejected:
      switches default-on become dead code.
  B — close FR with rationale. Rejected: ignores valid evidence.
  C — refine wording ("compress when reporting raw tool output, be
      complete for technical answers"). Rejected: still text
      injection, model-dependent, half-measure.
  D — full strip everywhere. Adopted.

The decision after grilling: context-mode's value is data routing
(sandbox, FTS5, session continuity), not prose styling. The brevity
injection conflated three goals — keeping raw data out of context
(real, hard-enforced), summarizing tool output compactly (LLMs auto-
calibrate), and final-answer prose style (the wrong target). Strip
all 22 sites where prose-style language landed.

Sites stripped (A-Z):

  hooks/routing-block.mjs
    - <communication_style> block  (Terse like caveman, fragments OK,
      auto-expand for security warnings)
    - <response_format> block       (Concise summary, 2-3 bullets)

  src/server.ts (5 MCP tool descriptions + 2 cosmetic comments)
    - ctx_execute            "When reporting results — terse..."
    - ctx_execute_file       same
    - ctx_search             same
    - ctx_fetch_and_index    same (URL + commands shapes, both)
    - cosmetic comment       "Caveman style — terse status line"
    - rewrote concurrency note: "Indexing is serial regardless of
      concurrency" → "Fetches parallelize up to your concurrency
      setting; FTS5 indexing serializes the writes after (SQLite
      single-writer rule)." — same fact, less jargon.

  configs/ (15 adapter MD files — every shipped system prompt)
    antigravity/GEMINI.md, claude-code/CLAUDE.md, codex/AGENTS.md,
    cursor/context-mode.mdc, gemini-cli/GEMINI.md, jetbrains-copilot/
    copilot-instructions.md, kilo/AGENTS.md, kiro/KIRO.md, omp/SYSTEM.md,
    openclaw/AGENTS.md, opencode/AGENTS.md, pi/AGENTS.md, qwen-code/
    QWEN.md, vscode-copilot/copilot-instructions.md, zed/AGENTS.md
    All had identical "## Output" block: 3 caveman lines stripped,
    workflow lines ("Write artifacts to FILES", "Descriptive source
    labels") kept.

  CLAUDE.md (repo root — internal dev instructions)
    Same caveman block stripped. We don't ship this file but we do
    eat our own dog food.

  README.md
    Pillar 4 ("Output Compression — Terse like caveman...") rewritten
    to "No prose-style enforcement" — explicitly cites the kimi-k2.5
    benchmark evidence as the rationale.

  web/index.html
    Removed Ch 4b entirely (the "Output compression" chapter with
    before/after example pushing terse style on the model).

Tests:

  - tests/session/continuity.test.ts: SessionStart routing-block
    assertion flipped from "must include 'Terse like caveman'" to
    "must NOT include caveman/terse-style directive".
  - tests/core/server.test.ts: Task hook injection assertion same
    flip. Two cosmetic comment renames ("Caveman style — terse status
    line" → "Status line: counts + sections + size"), test name
    rename ("caveman style" → "compact format"). Added new
    "prose-style policy (#482)" describe block at end of file with 3
    negative-pin tests covering server.ts MCP descriptions,
    routing-block, and README.

  Full suite: 82/82 files passed, 2645 passed, 20 skipped, 0 failed.
  Net +3 new tests (the policy describe block).

CONTRIBUTING.md
  New "Prose-style policy (#482)" section documents the decision so
  future contributors don't re-add the injection. Cites the Moonshot
  benchmark evidence + the regression test that pins the deletion.

This addresses #482 in full. Closing the issue with a comment that
walks the requester through the decision and links the policy section.
2026-05-10 14:45:41 +03:00
Mert Koseoglu b465acd834 docs(omp): drop hardcoded version from install guide + prune redundant manifest field
The previous manual install path pasted a literal `"version": "1.0.111"`
into a JSON snippet for omp-plugins.lock.json. That number drifts
silently on every release — anyone reading the README a week from
now would copy a stale version into their lock file.

Verified upstream that the snippet was unnecessary in the first
place. The plugin loader at refs/platforms/oh-my-pi/packages/
coding-agent/src/extensibility/plugins/loader.ts:89-94 only consults
the lock file when a plugin is explicitly disabled:

    const runtimeState = runtimeConfig.plugins[name];
    if (runtimeState && !runtimeState.enabled) continue;

Plugins missing from the lock file load with default-enabled state.
So the manual install collapses to two commands: `cd ~/.omp/plugins`
+ `bun add context-mode`, then restart. No JSON to edit, no version
to pin.

Same logic eliminates the `omp.version` field we had been carrying in
the root package.json. The upstream loader stamps
`manifest.version = pluginPkg.version` from the top-level
package.json:version on every load (loader.ts:87), so duplicating it
inside the omp block adds a drift surface and zero signal. The
matching `pi` block follows the same convention, so consistent.

Drops the corresponding omp.version sync code from
scripts/version-sync.mjs — it can no longer drift if the field
doesn't exist.
2026-05-10 13:34:43 +03:00
Mert Koseoglu a783d2b334 docs(omp): rewrite install steps as concrete commands + JSON, drop bullet-style explanation
Previous version mixed user-runnable steps with explanatory bullets
("OMP runs bun install...", "OMP reads package.json...") that read
like an internals tour, not an install guide. Replaced with three
discrete numbered paths:

  1. Plugin path (recommended) — `omp plugin install context-mode`
     + `omp plugin list` / `omp plugin doctor` verification.

  2. Manual plugin path — for cases where `omp plugin install` is
     unavailable (older OMP, restricted env, dev workflow). Direct
     `bun add context-mode` inside ~/.omp/plugins/ + a lockfile JSON
     snippet for omp-plugins.lock.json that flips enabled: true. Both
     mechanics verified against upstream manager.ts:158 (bun install
     into getPluginsDir()) and types.ts:141 (PluginRuntimeConfig shape).

  3. MCP-only path — unchanged, retained as fallback for users who
     don't want hooks.

Routing paragraph rewritten to describe what each of the four
`pi.on(...)` handlers actually does, with the upstream
hooks/types.ts:566 link for the block contract.
2026-05-10 13:27:20 +03:00
Mert Koseoglu 2ddae394c4 feat(omp): plugin path with native hook enforcement (HookAPI tool_call/tool_result/session_start/session_before_compact)
Promotes OMP from MCP-only delivery to a proper plugin. `omp plugin
install context-mode` now wires programmatic enforcement equivalent to
Claude Code's PreToolUse/PostToolUse/PreCompact/SessionStart pipeline.

Verified end-to-end against the upstream OMP source cloned to
refs/platforms/oh-my-pi @ v3.20.1 (no LLM trust, every claim
file:line cited):

  - Manifest format: `omp` or `pi` field on root package.json
    Source: refs/.../extensibility/plugins/loader.ts:75
      `const manifest = pluginPkg.omp || pluginPkg.pi;`
    + line 82: `manifest.version = pluginPkg.version;` (loader stamps
      version from top-level pkg.version on load — explicit
      `omp.version` is belt-and-suspenders, kept synced by
      scripts/version-sync.mjs).

  - Install command: `omp plugin install <pkg>` runs
    `bun install <pkg>` inside ~/.omp/plugins per
    refs/.../extensibility/plugins/manager.ts:158, then reads
    `~/.omp/plugins/node_modules/<pkg>/package.json` for the manifest.

  - HookFactory contract: `(pi: HookAPI) => void` per
    refs/.../extensibility/hooks/types.ts:809.

  - Block return shape: `{ block?: boolean; reason?: string }` per
    refs/.../extensibility/hooks/types.ts:566.

  - Event payloads:
    - ToolCallEvent  (refs/.../hooks/types.ts:448): {toolName, toolCallId, input}
    - ToolResultEvent (refs/.../hooks/types.ts:461 onward): {toolName, toolCallId, input, content[], isError}

  - Example reference: refs/.../examples/hooks/permission-gate.ts.

What the plugin actually does:

  - tool_call: hard-blocks bash containing curl/wget/inline-fetch
    (`requests.get`, `http.get`, `Invoke-WebRequest`, etc.) — same
    pattern set as the Pi extension.
  - tool_result: feeds OMP-shaped events through the existing
    extractEvents pipeline → SessionDB at ~/.omp/context-mode/.
  - session_start: derives a stable 16-hex session id from
    sessionManager.getSessionFile() (or wall-clock fallback), runs
    7-day cleanup.
  - session_before_compact: persists a buildResumeSnapshot output via
    upsertResume + increments compact_count for resume-on-restart.

Reference parity:

  - Mirrors src/adapters/pi/extension.ts shape closely. OMP differs in
    two ways that justify a dedicated file:
      1. Storage at ~/.omp/context-mode/ via OMPAdapter (not ~/.pi/)
      2. OMP has native MCP via mcp.json — the Pi extension's
         mcp-bridge.ts is dead weight under OMP and is intentionally
         omitted here.
  - Mirrors src/adapters/openclaw/plugin.ts integration shape (root
    package.json field → built JS entry).

Smoke test (run locally before commit):
  - pkg.omp.hooks resolves to build/adapters/omp/plugin.js ✓
  - default export is a function ✓
  - 4 handlers register: session_start, tool_call, tool_result,
    session_before_compact ✓
  - tool_call({toolName: 'bash', input: {command: 'curl ...'}}) →
    {block: true, reason: '...'} ✓

Tests: tests/adapters/omp-plugin.test.ts adds 17 cases across 4 TDD
slices (routing, extraction, session lifecycle, resume snapshot). All
green. Full vitest run: 2642 passed, 20 skipped, 0 failed.

scripts/version-sync.mjs now also stamps package.json:omp.version
when running on `npm version` lifecycle so OMP manifest version
never drifts from top-level pkg.version (verified by simulating a
stale 0.0.0 value and watching it correct to current).

README updated:

  - OMP install section reordered: plugin path is now primary, with
    upstream file:line citations for the loader and block contract;
    MCP-only path retained as the alternative.
  - Hook coverage table (lines ~1024-1031): OMP rows promoted from
    "--" to ✓ (via tool_call event), etc.
  - Platform compatibility table: OMP PreToolUse/PostToolUse/
    SessionStart/PreCompact/CanBlockTools all marked Plugin.
  - Routing-enforcement note: OMP moved from non-hook list to
    hook-capable list.
  - All "OMP MCP-only / no hook integration" prose paragraphs
    rewritten.
2026-05-10 13:22:22 +03:00
Mert Koseoglu a8a3e6c36a docs(platforms): bump 14 → 15 across README + web — count OMP officially 2026-05-10 12:58:41 +03:00
Mert Koseoglu 755697c7ca fix(omp): correct env var, MCP filename, and instruction file per upstream evidence
The OMP adapter and README were written from secondary sources and
diverged from the actual oh-my-pi runtime. Verified against
can1357/oh-my-pi v3.20.1 by cloning the repo into refs/platforms/
and reading source line-by-line:

  - Env var: OMP_PROCESSING_AGENT_DIR → PI_CODING_AGENT_DIR
    Source: packages/utils/src/dirs.ts:193
      `let dirs = new DirResolver(process.env.PI_CODING_AGENT_DIR);`
    No `OMP_*` runtime env exists; `OMP_*` keys are only mirrored
    to `PI_*` from .env files (packages/utils/src/env.ts:38-41),
    not from process.env.

  - MCP config filename: mcp_config.json → mcp.json
    Source: packages/utils/src/dirs.ts:455-459 (getMCPConfigPath)
            docs/mcp-config.md:17-18 ("User: ~/.omp/agent/mcp.json")
    Both user-scope (~/.omp/agent/mcp.json) and project-scope
    (.omp/mcp.json) are documented loading paths.

  - Instruction file: PI.md → SYSTEM.md (+ AGENTS.md)
    Source: packages/coding-agent/src/main.ts:394-402 reads
            .omp/SYSTEM.md (project) and ~/.omp/agent/SYSTEM.md
            (global).
            packages/coding-agent/src/discovery/agents-md.ts:25-29
            also auto-discovers AGENTS.md walking up from cwd.
    Zero `PI.md` matches in the upstream repo — the previous
    naming was speculative.

  - Hook surface: "no hook support" framing → MCP-only delivery
    Source: packages/coding-agent/src/extensibility/hooks/types.ts:695
            (`export interface HookAPI`)
            packages/coding-agent/examples/hooks/auto-commit-on-exit.ts
            (`pi.on("session_shutdown", ...)`)
    OMP DOES expose pre/post tool-call hooks. Our adapter still
    delivers via MCP only — but as our scoping choice, not OMP's
    limitation. Wiring OMP-native hooks is future work.

Also creates the missing `configs/omp/` shipped artifacts:
  - configs/omp/SYSTEM.md  (routing rules; the file the README's
                            `cp` step now actually has to copy from)
  - configs/omp/mcp.json   (MCP config snippet, parity with antigravity)

README.md OMP section rewritten to match other adapters' format —
no link, no "isolated storage under" prose in the title; install
flow is JSON config + SYSTEM.md copy + restart.

Tests updated and passing (77 OMP + detect tests, full suite
baseline preserved at 13 pre-existing flakes, 0 net new).
2026-05-10 12:52:51 +03:00
Marcusandboederzeng 043a1f52be [codex] add compact hook support (#492)
* feat(codex): add compact hook support

* chore(codex): address hook review cleanup

---------

Co-authored-by: boederzeng <86715671+boederzeng@users.noreply.github.com>
2026-05-09 21:48:19 +03:00
7b86ee5a20 feat(cursor): add Marketplace plugin packaging (#489)
* feat(cursor): add Marketplace plugin packaging

Mirror the Claude Code plugin layout for Cursor's plugin marketplace:

- .cursor-plugin/plugin.json: manifest pointing at ./configs/cursor/context-mode.mdc, ./skills/, ./hooks/cursor/hooks.json, and an MCP server entry running 'npx -y context-mode'.

- hooks/cursor/hooks.json: registers preToolUse, postToolUse, sessionStart, afterAgentResponse, and stop, all dispatched through 'npx -y context-mode hook cursor <event>' so users do not need a local clone.

- src/adapters/cursor/index.ts: doctor now detects plugin installs under ~/.cursor/plugins/{local,cache} and warns when both the plugin and a native .cursor/hooks.json register context-mode hooks.

- scripts/version-sync.mjs: keeps .cursor-plugin/plugin.json in lockstep with package.json.

- README.md, docs/platform-support.md: document the Marketplace install path alongside the existing manual install.

Refs #485

* feat(cursor): add plugin README + drop non-schema displayName field

- Add .cursor-plugin/README.md so the Marketplace tile has a dedicated landing page (project root README is unchanged).

- Remove 'displayName' from .cursor-plugin/plugin.json: the field is not in Cursor's plugin manifest schema (https://cursor.com/docs/reference/plugins) and would be flagged by the validator.

Validated all manifest keys against the official schema; no other extra fields. Cursor adapter test suite: 50/50 pass.

* feat(cursor): add Marketplace logo

Adds .cursor-plugin/assets/logo.png and references it via the manifest 'logo' field. Cursor resolves relative paths to raw.githubusercontent.com URLs at the commit SHA, so the Marketplace tile renders the snowflake icon directly from the repo.

* docs(cursor): add local-install quickstart for testers

Document the robocopy/symlink workflow so reviewers (and early adopters) can try the plugin from the repo before Marketplace acceptance. Calls out the Windows symlink limitation explicitly so testers do not waste time debugging mklink.

* docs(cursor): mark Marketplace plugin as work-in-progress until review

Per maintainer feedback: until Cursor's review team lists the plugin, the README needs an explicit 'work in progress' notice plus copy-pasteable local-install commands for both Windows (robocopy) and macOS/Linux (ln -s). Calls out the Windows symlink limitation directly so testers do not waste time debugging mklink.

Refs #485, #489

---------

Co-authored-by: Maxwell_sun <Maxwell_sun@noreply.gitcode.com>
Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
2026-05-09 18:14:36 +03:00
990586a2d6 fix(codex): normalize hook errors and guard patch continuity (#479)
* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* docs(codex): document hooks feature flag rename

* fix(codex): manage native hooks.json lifecycle

* fix(codex): capture apply_patch continuity events

* fix(codex): harden hooks.json lifecycle and apply_patch continuity

Create ~/.codex before writing hooks.json, distinguish missing, invalid JSON, and unreadable hook configs, and refuse to overwrite malformed user hook files.

Only resolve prior Read errors after successful follow-up edits, and make ctx-upgrade fail clearly when hook configuration cannot be updated instead of reporting a false success.

Also document [features].hooks as the preferred key while keeping [features].codex_hooks as a legacy alias in current Codex builds.

* fix(codex): normalize hook errors and guard patch continuity

Normalize Codex PostToolUse error state into the extractor input, block failed apply_patch calls from emitting error_resolved, file continuity, or plan continuity events, and harden hooks.json lifecycle handling against schema-invalid but parseable shapes.

Also keep context-mode upgrade running Codex hook repair on the already-latest path so doctor-driven hook fixes are not skipped.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
2026-05-09 12:09:53 +03:00
Ben YounesandClaude Opus 4.7 84b8d15c0f feat(adapters): add OMP (Oh My Pi) platform adapter (#473) (#480)
* feat(adapters): add OMP (Oh My Pi) platform adapter (#473)

Issue #473 reported that switching from Pi to OMP left context-mode
data under ~/.claude/ instead of an OMP-rooted directory. Audit shows
session DB / stats / content paths already follow the adapter's
sessionDir, but OMP had no adapter — detection fell through to Pi or
to the default Claude fallback, so storage rooted at ~/.claude/.

Adds a dedicated OMPAdapter (mcp-only paradigm, modeled on
AntigravityAdapter) with sessionDirSegments=[".omp"], wires
OMP_PROCESSING_AGENT_DIR detection BEFORE pi in PLATFORM_ENV_VARS,
adds the same precedence to the ~/.omp/ vs ~/.pi/ config-dir tier,
and threads "omp" through PlatformId, getSessionDirSegments, the
CONTEXT_MODE_PLATFORM allowlist, and getAdapter().

Honors OMP_PROCESSING_AGENT_DIR for the MCP settings root (defaults
to ~/.omp/agent), so OMP installs that relocate their agent dir keep
working. No new flag, no env-var override layer — purely closes the
adapter gap that produced the misrouted storage.

Note: PiAdapter still does not exist; pi falls back to the
ClaudeCodeAdapter via the default branch in getAdapter(). That is a
separate pre-existing bug — out of scope for this PR.

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

* docs(adapters): document OMP adapter in README/platform-support/CONTRIBUTING (#473)

Addresses maintainer review on #480: any contribution that adds a new
adapter must update the user-facing docs in the same PR so the support
matrix, install instructions, and architecture surface stay in sync.

- README.md: new <details> install block for OMP after Pi; OMP column
  added to the 5-hook session continuity table and the Platform
  Compatibility table; per-platform notes paragraph and the
  routing-instructions caveat both mention OMP.
- docs/platform-support.md: bumped "twelve" → "thirteen" platforms;
  added OMP to the MCP-only paradigm row, the Main Comparison Table,
  and the Capability Matrix; new Platform Details section for OMP
  covering hook support (none), path resolution, detection priority,
  and the issue-473 motivation.
- CONTRIBUTING.md: added omp/ to the src/adapters/ directory tree.

Code paths unchanged. Full vitest suite still green (2450 passed,
25 skipped); typecheck clean; pre-existing kiro-hooks worker
termination flake is unrelated.

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

* ci: retrigger flaky macOS better-sqlite3 dlopen

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:40:44 +03:00
Mert Koseoglu c3d0467923 fix(#413): /resume snapshot fallback in SessionStart hook
Per CC docs, --continue, --resume, and /resume all fire SessionStart with
source="resume" + the ACTIVE session_id. For /resume the active id is a
*fresh* uuid for the resumed conversation, so live-events lookup misses
even when prior context exists. The resume branch now falls back to
db.claimLatestUnconsumedResume(currentSessionId) — same pattern OpenCode
and OpenClaw plugins already use (opencode-plugin.ts:454).

Live-events path keeps precedence; snapshot only surfaces when no live
rows match the incoming session_id (the /resume case, or when the prior
session was previously compacted).

Tests:
- "falls back to snapshot when resumed session has no live events"
- "prefers live events over snapshot when both exist (--continue stays correct)"

README:
- Session Continuity intro now lists --continue, --resume, AND /resume
- Adds one-paragraph note on snapshot fallback for non-latest /resume picks
- Claude Code platform line updated to match
2026-05-04 18:30:03 +03:00
Ben Younesandousamabenyounes 5432575ae3 fix(install): self-heal missing better-sqlite3 binding on Windows (#408) (#410)
Closes #408. 3-layer self-heal (prebuild-install via process.execPath → npm install fallback → actionable stderr). Wired into postinstall + ensure-deps + cli doctor hint.

Co-authored-by: ousamabenyounes <ousama.benyounes@gmail.com>
2026-05-04 17:43:18 +03:00
Mickey LazarevicandMert Köseoğlu 7e64e4fadb docs: update README with OpenCode session continuity improvements (#409)
* docs: update README with OpenCode session continuity improvements

- Update OpenCode adapter to enable sessionStart and canInjectSessionContext hooks
- Clarify SessionStart handling via experimental.chat.system.transform surrogate
- Update AGENTS.md usage instructions for both OpenCode and KiloCode
- Add new directories to .gitignore for development tools

* test: update OpenCode adapter test expectations for session capabilities

- Change sessionStart and canInjectSessionContext expectations from false to true
- Update test descriptions to reflect new expected behavior
- Ensure KiloCode adapter test maintains consistency with updated expectations

---------

Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
2026-05-03 23:29:06 +03:00
Mert Koseoglu d9a537fac3 docs(readme): inline concurrency annotation, drop verbose Parallel I/O section
Per user feedback: code blocks were too heavy for the Tools table area.
Compressed into one-liners next to the relevant tool rows:
  - ctx_batch_execute: "Opt-in concurrency: 1-8 for I/O-bound batches."
  - ctx_fetch_and_index: "Pass requests:[{url,source}, ...] + concurrency: 1-8 for parallel multi-URL."

The verbose Parallel I/O subsection with 30+ lines of example code is gone.
Schema details remain documented in src/server.ts JSDoc on each tool's
description string (which is the canonical source for LLM-facing usage hints).
2026-05-03 00:26:36 +03:00
Mert Koseoglu 5850e3bcb5 fix(release): pack bin/ + document concurrency + CTX_FETCH_STRICT for v1.0.104
DX validation post-tag found:
  1. bin/ missing from package.json files array — npm tarball did not include
     bin/statusline.mjs, breaking the v1.0.104 marquee statusline feature for
     fresh installs (cli.bundle.mjs forwards to context-mode statusline which
     resolves to a non-packaged path).
  2. README missing concurrency feature documentation (added in b392c2f).
  3. README missing CTX_FETCH_STRICT env var documentation (security knob).

This commit ships all three on top of the v1.0.104 tag. Tag is moved forward
to this commit so npm publish ships the corrected tarball.
2026-05-03 00:12:34 +03:00
+7 734bdbc2b7 v1.0.104 (#401)
* fix(insight): move showAllInsights useState before early return (React #310)

* 1.0.102

* fix(test): normalize pluginRoot path separators for Windows (#369)

buildNodeCommand() converts backslashes to forward slashes to prevent
MSYS path mangling on Windows. The test assertion was comparing against
raw pluginRoot (backslashes from mkdtempSync) causing CI failure on
windows-latest while macOS and Ubuntu passed.

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

* fix(stats): persist counter + show lifetime + auto-memory + business value framing

- Add tool_calls table to SessionDB — counter survives upgrades and --continue
- Show persistent memory totals (events across all sessions)
- Show auto-memory count from ~/.claude/projects/*/memory/
- Replace hardcoded '9 more' with actual category count
- Use Opus pricing ($15/M) for cost calculations
- Replace '3.0x' with '3x longer sessions' phrasing
- Add 'Bottom line' footer with session/lifetime cost summary

Closes the upgrade-resets-stats bug. ctx_stats now correctly shows
that data persists across compaction, restart, and upgrade.

* fix(windows): normalize hooks.json placeholders on startup (#378)

The committed hooks/hooks.json and .claude-plugin/plugin.json use
${CLAUDE_PLUGIN_ROOT} placeholders + bare 'node' command. On Windows
+ Claude Code, this causes runtime loader failures (cjs/loader:1479)
because:
1. bare 'node' may not resolve via PATH (Git Bash issue, see #369)
2. ${CLAUDE_PLUGIN_ROOT} resolution can hit MSYS path mangling (see #372)
3. backslash paths get corrupted in shell quoting

Fix: start.mjs detects Windows on every MCP server boot. If hooks.json
or plugin.json contain unresolved placeholders, rewrites them with:
- process.execPath (absolute, quoted) instead of bare 'node'
- Forward-slash paths (prevent MSYS translation)
- Double-quoted paths (handle spaces)

Idempotent — only rewrites when placeholder pattern is detected.
Survives upgrades — runs at every start.

Closes #378

* fix(cache-heal): use shebang on Unix, self-heal stale node paths

After Brew updates Node, the versioned Cellar path written to
~/.claude/settings.json becomes stale, causing 'session start' errors:

  /opt/homebrew/Cellar/node/25.9.0_2/bin/node  (gone after upgrade)

vs the stable symlink:

  /opt/homebrew/bin/node                       (always current)

Root cause: start.mjs wrote `process.execPath` directly, which on
Brew returns the versioned path snapshot.

Fix (2 layers):
1. New installs on Unix: write cache-heal script with shebang
   (#!/usr/bin/env node) + chmod +x, register hook as bare script
   path. `env` resolves node from PATH at runtime — survives
   any Node upgrade.
2. Self-heal: every MCP boot, check if existing hook command
   references a node path that no longer exists. If stale,
   rewrite using current pattern.

Windows unchanged (no shebang support) — uses process.execPath
+ buildHookCommand pattern, plus self-heal for any breakage.

Reported by @vigo on Discord.

* fix(lifecycle): trigger isParentAlive re-check on stdin EOF to close 30s CPU-spin window (#388) (#389)

* fix(test): normalize pluginRoot path separators for Windows (#369)

buildNodeCommand() converts backslashes to forward slashes to prevent
MSYS path mangling on Windows. The test assertion was comparing against
raw pluginRoot (backslashes from mkdtempSync) causing CI failure on
windows-latest while macOS and Ubuntu passed.

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

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* fix(lifecycle): trigger isParentAlive re-check on stdin EOF to close 30s CPU-spin window (#311, #388)

The vendored MCP SDK's StdioServerTransport only registers 'data' and
'error' listeners on process.stdin. When the parent (e.g. Claude Code)
dies abruptly without sending SIGTERM, the server keeps reading from a
half-closed pipe and CPU-spins until the 30s ppid poll catches up. In
practice this manifests as orphaned context-mode processes accumulating
~80h of CPU time before being SIGKILL'd manually (#388).

The fix adds a single 'end' listener on process.stdin inside the
lifecycle guard. It does NOT shut down on 'end' alone — that's the
false-positive behavior #236 tore out. Instead, 'end' triggers the same
isParentAlive() probe the periodic timer runs, just earlier:

- parent alive  → no-op (#236 regression test still passes)
- parent dead   → 30s detection window collapses to ~0

Skipped on TTY (OpenCode ts-plugin), where stdin is not the MCP channel.

Tests: added a unit test that emits stdin 'end' under both alive and
dead parent conditions, and updated the existing listener-invariance
test to pin the new contract (only 'end' touched, restored on cleanup).
All existing tests still pass.

---------

Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ray <cho_meiko@okuribito-funeral.jp>

* fix(executor): hide Windows console + drop .sh extension for shell exec (#384)

On Windows, ctx_execute(language: 'shell', ...) had two problems:

1. Silent output - child_process.spawn without windowsHide:true creates
   a visible console window that intercepts stdout, leaving the MCP
   response empty.

2. Git Bash popup - temp script written as 'script.sh' triggers Windows
   file association for .sh files. bash.exe opens a visible window over
   the user's IDE.

Fix (minimal, two surgical changes):
- spawn(..., { windowsHide: isWin }) via buildSpawnOptions(platform)
- isWin && language === 'shell' ? 'script' : 'script.{ext}' via
  buildScriptFilename(language, platform)

Both changes are Windows-gated. Linux/macOS behavior unchanged.

Does NOT change shell invocation semantics (no bash -c wrapper).
Does NOT add SHELL env override. Both deferred - separate features.

Helpers exposed as pure functions for unit testing without mocking
spawn or filesystem.

Closes #384.
Supersedes #385 with smaller surface area.

* fix(executor): full Windows shell coverage — bash -c source + SHELL override (#384)

Builds on commit 9d1f44f (windowsHide + no .sh extension) with the
remaining two root causes:

1. MSYS2 path mangling on non-C: drives. When bash.exe receives a
   script as a direct argument, MSYS rewrites paths like
   D:\tmp\script to D:\c\tmp\script, breaking execution. Fix: wrap
   in bash -c "source 'path'". The -c flag prevents MSYS from
   touching the file argument.

2. SHELL env var override. Users with non-standard shell setups
   (WSL, custom bash location, msys2 installations) need to point
   context-mode at their preferred shell. detectRuntimes() now
   checks process.env.SHELL first; if the path exists, uses it.

Single-quote escape applied to filePath in bash -c form to handle
paths containing apostrophes safely.

PowerShell uses -File flag (correct .ps1 invocation).
cmd.exe uses direct file (.cmd association is safe — no Git Bash issue).

Closes #384 fully (in addition to commit 9d1f44f).

Test coverage:
- SHELL env override (3 tests)
- buildCommand bash -c source (Windows + Unix variants, 5 tests)
- Single-quote escape edge case
- All previous Windows shell tests still pass

* fix(test): normalize scriptPath separators in cache-heal-self-heal assertion

Same root cause as the #369 test fix: buildHookCommand normalizes
backslashes to forward slashes for cross-platform safety (MSYS/Git Bash
mangling prevention). Test assertion at line 189 compared against the
raw scriptPath from mkdtempSync (backslash-separated on Windows),
causing CI failure on windows-latest while macOS and Ubuntu passed.

Aligns line 189 with line 234 which already had this normalization.

* fix(openclaw): route native tool aliases (#383)

* fix(test): normalize pluginRoot path separators for Windows (#369)

buildNodeCommand() converts backslashes to forward slashes to prevent
MSYS path mangling on Windows. The test assertion was comparing against
raw pluginRoot (backslashes from mkdtempSync) causing CI failure on
windows-latest while macOS and Ubuntu passed.

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

* ci: update install stats

* ci: update install stats

* fix(openclaw): route native tool aliases

---------

Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(hooks): passthrough on `ask` in headless mode (CLAUDE_CODE_HEADLESS) (#380)

* fix(test): normalize pluginRoot path separators for Windows (#369)

buildNodeCommand() converts backslashes to forward slashes to prevent
MSYS path mangling on Windows. The test assertion was comparing against
raw pluginRoot (backslashes from mkdtempSync) causing CI failure on
windows-latest while macOS and Ubuntu passed.

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

* ci: update install stats

* ci: update install stats

* fix(hooks): passthrough on `ask` in headless mode

In `claude --print`, the CLI has no TTY to surface a permission prompt.
When routing returns `action: "ask"`, the formatter emits
`permissionDecision: "ask"` and the CLI hangs forever waiting on a user
verdict that can never arrive.

Mirror gemini-cli.mjs: when `CLAUDE_CODE_HEADLESS=1` is set in the
environment, return null (passthrough) on `ask`. Other actions
(deny/modify/context) unchanged. Interactive sessions are unaffected —
without the env var, behavior is identical to before.

Launcher scripts running headless agents must export
`CLAUDE_CODE_HEADLESS=1` to opt in.

* fix(hooks): extend headless passthrough to deny + modify

Without this, v1.0.103's routing.mjs returns action:'modify' for
'dangerous' curl/wget invocations — silently rewriting the command into
an echo that suggests ctx_execute. In TTY sessions that nudge is useful
(the agent reconsiders or asks the user). In headless 'claude --print'
the agent has no UI to reconsider; the rewritten echo runs, produces
zero stdout, and downstream pipelines see a silent failure.

Same shape as the existing 'ask' fix:

  case "deny":
+   if (isHeadless()) return null;
    return { ... };

  case "modify":
+   if (isHeadless()) return null;
    return { ... };

The 'context' case is left as-is (additionalContext is informational,
doesn't block the tool). Two existing tests in formatters.test.ts that
asserted 'still formats deny/modify normally' under
CLAUDE_CODE_HEADLESS=1 are inverted to assert the new passthrough
behavior, matching the existing 'ask' test pattern. 21/21 tests pass.

---------

Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(memory): adapter-aware persistent memory across all 14 platforms

v1.0.100's "Unified Persistent Memory" feature was Claude-centric.
Auto-memory, prior session, and persist memory all hardcoded ~/.claude/,
breaking 13 of 14 platforms. Plus a worktree filename mismatch broke
Claude Code too on worktree sessions.

Architectural fix: adds 3 methods to HookAdapter interface so every
adapter declares its own conventions:
  - getConfigDir()        — ~/.claude, ~/.codex, ~/.qwen, ~/.gemini, etc.
  - getInstructionFiles() — ['CLAUDE.md'], ['AGENTS.md'], ['QWEN.md'], etc.
  - getMemoryDir()        — ~/.claude/memory, ~/.codex/memories, etc.

BaseAdapter ships sensible defaults derived from sessionDirSegments;
only the 11 non-Claude adapters override (Claude inherits).

Wiring changes:
  - searchAutoMemory() now accepts an adapter, dispatches via methods
  - ctx_search timeline uses _detectedAdapter.getConfigDir()
  - ctx_search timeline SessionDB filename now includes worktree suffix
    (matches what session-snapshot/session-extract write to)
  - extract.ts rule detection covers AGENTS.md, GEMINI.md, QWEN.md,
    KIRO.md, copilot-instructions.md, context-mode.mdc, and any
    *.md inside a memory/memories directory

Bonus fixes (from PR #376):
  - OpenCode/KiloCode cache path: now packages/context-mode@latest/
    layout (silently changed by upstream late 2024 — broke doctor/upgrade)
  - OpenCode SessionStart equivalent via experimental.chat.messages.transform
    — prior-session continuity now works on OpenCode/KiloCode

Tests added (8 new files, 65 new tests, all green):
  - tests/adapters/base-adapter-memory.test.ts        (4)
  - tests/adapters/claude-code-memory.test.ts          (3)
  - tests/adapters/memory-conventions.test.ts         (36)
  - tests/core/auto-memory-adapter.test.ts             (5)
  - tests/core/cache-plugin-root.test.ts               (2)
  - tests/core/server-timeline-adapter.test.ts         (3)
  - tests/opencode-session-start.test.ts               (2)
  - tests/session/extract-rule-detection.test.ts      (10)

Closes architectural root cause of #367 follow-ups.
Supersedes #379 (Codex), #370 (Qwen), #376 (OpenCode/KiloCode portions).

Co-Authored-By: Marcus Neufeldt <MarcusNeufeldt@users.noreply.github.com>
Co-Authored-By: btxbtxbtx <btxbtxbtx@users.noreply.github.com>
Co-Authored-By: Mickey Lazarevic <mikij@users.noreply.github.com>

* fix(test): make Windows-host assertions platform-aware

Three Windows CI failures in two test files, two distinct root causes:

executor.test.ts "buildCommand returns shell command array"
- PR #384 changed Windows+bash to return [bash, -c, "source 'path'"]
  (3 elements) to dodge MSYS path mangling on non-C: drives.
- Test still asserted length === 2 universally. Make it
  platform+shell aware: 3-element bash -c form on Windows+bash,
  2-element direct-exec form everywhere else.

cache-heal-self-heal.test.ts "Unix: rewrites command when node path is stale"
- selfHealCacheHealHook({platform: "linux"}) controls the WRITTEN
  command but ensureShebangAndExecBit's chmodSync is still a host
  syscall — NTFS cannot honor 0o755 exec bit.
- Skip just the mode assertion on Windows host; shebang check stays.

cache-heal-self-heal.test.ts "preserves other hooks unchanged"
- Same root cause as 35b6f90: buildHookCommand normalizes
  backslashes → forward slashes for cross-platform safety. Line 287
  was missed when 35b6f90 patched line 190. Apply identical fix.

* Add Insight directory overrides (#400)

* feat: add insight directory overrides

* build: update bundles for insight overrides

* fix(server): use sync platform detection for pre-adapter session dir

Edge case: when MCP server is called before initialize completes,
_detectedAdapter is null and getSessionDir() returns hardcoded
~/.claude/context-mode/sessions/. For non-Claude platforms this means
the wrong sessions dir until the adapter is detected.

Fix: when adapter is null, call detectPlatform() (sync, env-var-based)
and map to platform-specific session dir segments without adapter
instantiation. Falls back to .claude only if no platform signal found.

Closes the last hardcoded .claude fallback in server.ts.
Completes the multi-platform memory work from 6262c13.

* chore: rebuild bundles with sync platform detection fix

* fix(test): anchor XDG_CONFIG_HOME under fakeHome in memory-conventions

OpenCodeAdapter (and kilo variant) honor XDG_CONFIG_HOME / APPDATA
before falling back to homedir(). GitHub Actions Ubuntu runners can
have XDG_* set to the runner's real /home/runner, which bypasses the
homedir() mock from tests/setup-home.ts and leaks the real path into
test assertions.

Override XDG_CONFIG_HOME / XDG_DATA_HOME / APPDATA / LOCALAPPDATA at
the top of memory-conventions.test.ts (after setup-home runs) so all
adapters stay sandboxed under fakeHome. Reproduced locally with
XDG_CONFIG_HOME=/tmp/fake-xdg before the fix; passes after.

* test(session): regression test for cross-session bleed (#398)

Adds three tests pinning the contract LMS927369's PR #398 fixed:

1. getSessionEvents(db, sessionId) returns ONLY the requested
   session's events — no bleed from concurrent sessions even when
   another session has a more recent session_meta.started_at.
2. getSessionEvents returns [] for unknown sessionId — no fallback
   to global most-recent (which was the original bug's root cause).
3. getLatestSessionEvents still picks globally-most-recent by design,
   pinning the existing semantics so future callers can't be surprised.

All three would silently break if any of the 6 patched SessionStart
adapters regressed back to getLatestSessionEvents(db).

* fix(mcp): resolve ctx_index relative paths from project dir (#365)

fix(mcp): resolve ctx_index relative paths from project dir

Resolves ctx_index relative paths against the project directory (via
getProjectDir() env chain: CLAUDE_PROJECT_DIR / *_PROJECT_DIR /
CONTEXT_MODE_PROJECT_DIR / cwd) instead of MCP server cwd.

Known follow-up gaps tracked separately:
- IDEA_INITIAL_DIRECTORY missing from getProjectDir() cascade (JetBrains)
- FTS5 source label uses raw user-typed path (dedup gap)
- Bundle rebuild
- Negative-path test coverage (traversal / env-unset / label collision)
- getProjectDir() not unified across server.ts (deny-policy at L429)

Co-authored-by: Ousama Ben Younes <ousamabenyounes@users.noreply.github.com>

* test(hooks): add cross-platform regression matrix for MCP readiness (#354)

* test(hooks): add cross-platform regression matrix for MCP readiness

Locks in the directory-scan + PID-liveness contract from #347. The 11
test files updated by #347 changed the sentinel path but never asserted
that isMCPReady() returns true for a sentinel whose PID is outside the
test runner's process tree — the exact condition the PPID-keyed lookup
failed on under WSL2 / `bash -c "node ..."` topologies.

Coverage:
- sentinelPathForPid + deprecated sentinelPath shape
- sentinelDir platform branch (/tmp on Unix, os.tmpdir on win32)
- isMCPReady happy path + resilience to malformed payloads
- Stale-sentinel self-healing (gated to clean envs)
- PPID-independence regression: child PID ∉ runner tree → still true

Pure test-only PR. No production changes.

* test(hooks): apply review cuts (drop deprecated test, collapse it.each, env-var path)

- Drop sentinelPath() deprecated-export test. The JSDoc says it's kept for
  one release cycle; testing what's about to die is a maintenance trap.
- Collapse empty-payload + non-numeric-payload tests into a single it.each.
  Same contract, fewer lines.
- Pass resolved sentinel directory to the regression child via env var
  instead of recomputing the platform branch inline. Keeps mcp-ready.mjs
  as the single source of truth for the path shape.

* test(hooks): merge mcp-ready regression matrix into core-routing.test.ts

Per review feedback: move the three describe blocks (`contract`,
`stale-cleanup self-healing`, `PPID-independence`) from the standalone
tests/hooks/mcp-ready.test.ts into tests/hooks/core-routing.test.ts as
top-level describes after the existing `routePreToolUse` block. Same
test bodies, same assertions; only the host file changed.

The stale-cleanup block adds a local `beforeEach` that unlinks the
file-level `mcpSentinel` so the runner's own live sentinel does not
mask the dead-PID cleanup the test verifies.

---------

Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>

* fix(cli): use execFile to open URL without shell interpolation

execSync(`open "${url}"`) interpolates the URL through the shell. The
URL is localhost-only today, but the pattern is fragile — a future
remote URL or weak port-validation flips it into shell injection.

Switches darwin/linux/win32 branches to execFile with an arg array
and a try/catch that prints a copyable URL on failure.

* perf: cut per-tool-call latency across all 14 adapters

Five fixes targeting synchronous hot paths fired on every tool call.
Per-session reclaim: ~1.8s macOS / ~0.7-1.5s Linux / ~7.5-12.5s Windows
(Windows wins biggest because fork+exec is heavier).

A1. src/session/db.ts memoize getWorktreeSuffix per (cwd, env override)
    Prior: `git worktree list --porcelain` subprocess fork on every
    ctx_* tool call (~12ms macOS, 50-100ms Windows). Cached: 0.86μs
    warm — 17,000x faster on the hot path.

A2. hooks/session-helpers.mjs 2-level cache for getWorktreeSuffix
    Hooks are fresh node forks per fire — module cache alone won't
    survive across calls. Added cross-process tmpdir marker keyed by
    sha256(cwd) — Windows-safe filename, all-OS tmpdir(). Within a hook
    process, in-memory cache hits 2 of 3 callsites (db/events/cleanup
    paths). Across hook processes, marker file short-circuits the git
    fork. Bench: 41ms cold child vs 28ms warm child = -12ms/fire on
    macOS, ~50-100ms/fire on Windows.

A3. src/server.ts defer persistToolCallCounter via setImmediate
    SQLite open/select/update/close was on the response path. Now runs
    after response returns. Removes 1-3ms from user-perceived latency
    per ctx_* call.

C1. hooks/auto-injection.mjs collapse 4× O(N) Array.filter() into one
    O(N) pass. UserPromptSubmit fires this every prompt; with N up to
    100 events the prior code walked the array 4 times.

C5. src/session/db.ts + hooks/session-loaders.mjs add bulkInsertEvents
    PostToolUse emits 5-15 events per tool call; per-event insertEvent
    ran N transactions = N WAL commits. Bulk path pre-computes hashes
    outside the SQL transaction, then runs all dedup/evict/insert work
    inside one transaction. attributeAndInsertEvents prefers bulk when
    available, falls back to loop for backward compat.

C3. src/search/auto-memory.ts single statSync per candidate file
    Prior code stat'd each candidate twice (size guard, mtime). Reuse
    the first stat for both — one syscall per file instead of two.

Cross-platform: every fix uses platform-agnostic primitives (tmpdir(),
sha256-hashed filenames, setImmediate, in-process module cache).
Tested on macOS locally (2045/2064 vitest pass, tsc --noEmit clean);
CI exercises Linux + Windows. The 5 fixes apply uniformly to all 14
adapters because they all funnel through the same session-helpers /
session-db / MCP server hot paths.

* test(server): regression guard for ctx_fetch_and_index tmp cleanup

ctx_fetch_and_index writes fetched content (including auth headers and
API tokens via subprocess fetch) to os.tmpdir()/ctx-fetch-*.dat before
reading and indexing. On macOS /tmp is world-readable, so leaking even
one file is a P0 issue on shared hosts. The handler currently wraps the
read in try/finally with rmSync(outputPath), but nothing prevents a
future refactor from dropping that block.

Adds tests/core/fetch-cleanup.test.ts with two layers of protection:
  1. Static-source guard — fails if the handler in src/server.ts loses
     the `finally { ... rmSync(outputPath) ... }` block. Verified RED
     by deleting the finally block (matches fail) and GREEN by
     restoring it.
  2. Behavioural tests — replicate the read+cleanup pattern against a
     local HTTP fixture covering success, empty content, error before
     write, and partial-write-then-throw paths. All assert no
     ctx-fetch-*.dat file remains in os.tmpdir() after the call.

No production code change — the fix already landed in 45ecf90; this
commit only locks the invariant in.

* fix(server): include IDEA_INITIAL_DIRECTORY in getProjectDir() chain

JetBrains adapter sets IDEA_INITIAL_DIRECTORY but the server's
getProjectDir() cascade did not read it, so ctx_index relative paths
resolved against the IDE bin dir instead of the project root.

Adds it to the env cascade and pins the regression with a JSON-RPC
spawn test that asserts resolution under IDEA_INITIAL_DIRECTORY only.

* docs: correct repo path, tool names, version, and adapter list

- llms.txt referenced the wrong repo (claude-context-mode) in title
  and 18 raw URLs.
- llms-full.txt documented tools without the ctx_ prefix and was
  pinned to v0.9.22 with a 6-tool count; updates to 11+ tools
  matching the live server registry, drops the stale version pin.
- platform-support.md said "nine platforms"; adds detail sections
  for OpenClaw and Zed and updates the count.
- README "6 sandbox tools" updated to current count.

* fix(server): include URL in ctx_fetch_and_index cache key

getSourceMeta(label) returned the meta from any prior fetch with the
same label, so two distinct URLs sharing a label silently returned
the cached first response instead of fetching the second.

Composes the cache key from label+url so legitimate cache hits still
work but cross-URL label reuse no longer serves stale content.

* fix(codex): default projectDir to cwd when env and input missing

Codex's parser left projectDir undefined when neither input.cwd
nor the platform env var (CODEX_PROJECT_DIR) was set, so
downstream hooks received undefined and broke under worktrees /
non-default cwd. Aligns with cursor/opencode pattern by falling back
to process.cwd().

* fix(gemini-cli): default projectDir to cwd when env and input missing

Gemini CLI's parser left projectDir undefined when neither input.cwd
nor the platform env var (GEMINI_PROJECT_DIR / CLAUDE_PROJECT_DIR)
was set, so downstream hooks received undefined and broke under
worktrees / non-default cwd. Now also accepts cwd from the wire
input and falls back to process.cwd(), aligning with the
cursor/opencode pattern.

* fix(openclaw): default projectDir to cwd when env and input missing

OpenClaw's parser left projectDir undefined when neither input.cwd
nor the platform env var (OPENCLAW_PROJECT_DIR) was set, so
downstream hooks received undefined and broke under worktrees /
non-default cwd. Aligns with cursor/opencode pattern by falling back
to process.cwd().

* fix(zed): default projectDir to cwd when env and input missing

Zed's parser left projectDir undefined when neither input.cwd
nor the platform env var (ZED_PROJECT_DIR) was set, so
downstream hooks received undefined and broke under worktrees /
non-default cwd. Aligns with cursor/opencode pattern by falling back
to process.cwd().

Replaces the throw-on-call defensive parsers with parsers that
return a minimal event using the standard fallback chain. Zed
remains mcp-only (capability flags are still all false), so these
parsers should not be invoked in normal operation — they exist as
safe defaults if a misconfigured caller bypasses the capability check.

* fix(antigravity): default projectDir to cwd when env and input missing

Antigravity's parser left projectDir undefined when neither input.cwd
nor the platform env var (ANTIGRAVITY_PROJECT_DIR) was set, so
downstream hooks received undefined and broke under worktrees /
non-default cwd. Aligns with cursor/opencode pattern by falling back
to process.cwd().

Replaces the throw-on-call defensive parsers with parsers that
return a minimal event using the standard fallback chain.
Antigravity remains mcp-only (capability flags are still all
false), so these parsers should not be invoked in normal operation
- they exist as safe defaults if a misconfigured caller bypasses
the capability check.

* fix(server): unify deny-policy project-dir resolution with getProjectDir()

checkFilePathDenyPolicy used `process.env.CLAUDE_PROJECT_DIR ?? cwd()`
which skips GEMINI_PROJECT_DIR / VSCODE_CWD / OPENCODE_PROJECT_DIR /
PI_PROJECT_DIR / IDEA_INITIAL_DIRECTORY / CONTEXT_MODE_PROJECT_DIR.

Non-Claude adapters either failed open or matched the wrong repo's
deny rules. Routes resolution through the existing getProjectDir()
helper so all 12 adapters apply policy against the correct root.

* fix(server): canonicalize ctx_index source label to resolvedPath

source ?? path used the raw user-typed input, so the same absolute
file indexed via './foo.md', 'foo.md', or 'subdir/../foo.md' produced
three FTS5 rows because dedup keys on sources.label.

Default the label to the resolved absolute path; explicit `source`
still wins. Adds a regression test pinning that two relative spellings
of the same file yield exactly one row.

* test(server): pin negative-path coverage for ctx_index resolution

Adds three regression tests:
- Relative `../` path traversal stays allowed (matches current
  trust-boundary policy; pinned to surface future security changes).
- CLAUDE_PROJECT_DIR unset falls back to spawned-server cwd.
- Strengthens the absolute-path bypass test to assert the source
  label equals the absolute path, so the test fails on baselines
  that skip the resolver.

* refactor(server): unify ad-hoc project-dir resolution on getProjectDir()

PR #365 added the getProjectDir() env cascade but only routed ctx_index
through it; ctx_execute_file's executor captured project root once at
construction (CLAUDE_PROJECT_DIR ?? cwd), so the same relative path
resolved differently across the two tools when only
CONTEXT_MODE_PROJECT_DIR was set.

Switches the executor to lazy resolution via getProjectDir(). Adds a
regression test asserting ctx_execute_file resolves under
CONTEXT_MODE_PROJECT_DIR when CLAUDE_PROJECT_DIR is unset.

* Revert "fix(zed): default projectDir to cwd when env and input missing"

This reverts commit 7cd9535214.

* Revert "fix(antigravity): default projectDir to cwd when env and input missing"

This reverts commit 2f5442f5e9.

* fix(test): isolate Windows test env from start.mjs side effects

- start.mjs: skip normalizeHooksOnStartup under VITEST. server.test.ts spawns
  start.mjs from the repo root; on Windows it was mutating the committed
  .claude-plugin/plugin.json, which then poisoned cli.test.ts:156's
  ${CLAUDE_PLUGIN_ROOT} placeholder assertion.

- memory-conventions: make OpenCode/Kilo getConfigDir/getMemoryDir
  expectations platform-aware. Adapter honors XDG_CONFIG_HOME on POSIX and
  APPDATA on Windows; tests previously asserted ~/.config on all platforms.

* feat(batch_execute): opt-in concurrency for I/O-bound batches

Adds a `concurrency: 1-8` parameter to ctx_batch_execute. Default 1
preserves the existing serial path (shared timeout budget, cascading
skip on timeout). >1 switches to a worker pool with per-command
timeouts and order-preserving output.

Local benchmark: 5× sleep(500ms) → 533ms at concurrency=8 (4.97×
speedup vs 2651ms serial).

Why now: LLM agents fan out multi-source research (gh/curl/git
batches). Sequential I/O is pure wait; concurrency turns it into
overlapped wait without any user-facing API change.

Tool description hardened with positive guidance per
PRD-concurrency-architectural.md §4: ✅ network/I/O batches use 4-8,
❌ CPU-bound (npm test, build, lint) and stateful (ports, locks)
stay at 1.

Architecture:
- runBatchCommands() extracted as pure function with BatchExecutor
  interface — testable in isolation, no MCP/SDK dependency.
- Handler is now a thin wiring layer (executor + sessionStats + store).
- THINK IN CODE directive preserved at full strength in description.

Tests (tests/core/server.test.ts, 12 new):
- Serial: order, cascade-skip, shared-budget exhaustion.
- Parallel: order preservation, in-flight cap, per-command timeout
  isolation, FS bytes callback, cmd-count > concurrency safety.
- Edge: empty array, no-output sentinel, prefix prepending.

Schema/description coverage assertion in batch_execute FS read
tracking suite proves the contract stays documented.

Co-Authored-By: Sebastian Breguel <sebastianbreguel@gmail.com>

* chore(batch_execute): strengthen THINK IN CODE in tool description

THINK IN CODE upgraded from soft guidance to NON-NEGOTIABLE directive,
with explicit clarification of how it relates to concurrency:

  Concurrency parallelizes the FETCH; THINK IN CODE owns the PROCESSING.

Adds the tactical detail (pure JavaScript, Node.js built-ins, try/catch,
null-safe) so LLMs writing the processing command have an unambiguous
contract — same level of specificity already present in ctx_execute and
ctx_execute_file descriptions.

No behavior change. Existing description-coverage assertion still passes.

* fix(test): raise insight-cors beforeAll hookTimeout to 120s

Default vitest hookTimeout (10s) is shorter than the inner 3-attempt ×
30s waitForInsight polling, so any slow Windows runner that takes >10s
to spawn node + open sqlite deterministically fails. Pin to 120s to fit
the worst-case retry budget.

* feat(concurrency): opt-in parallelism for I/O-bound MCP tools

Adds a `concurrency: 1-8` parameter to ctx_batch_execute and
ctx_fetch_and_index, plus a shared `runPool` primitive, observability
extractor, and Parallel I/O guidance across all 14 adapter routing
docs.

What ships
- src/concurrency/runPool.ts (new): generic in-flight-capped worker
  pool returning Promise.allSettled-style results. Single primitive
  used by both batch tools — no copy-pasted worker logic.
- ctx_batch_execute: serial branch unchanged (shared timeout budget,
  cascading skip). Parallel branch routed through runPool. Description
  hardened with PARALLELIZE I/O ✅/❌ guidance and NON-NEGOTIABLE
  THINK IN CODE clause.
- ctx_fetch_and_index: accepts both legacy `{url, source}` (single,
  exact backward-compat wording) and new `{requests: [{url, source}]}`
  (batch). Workers fetch in parallel via runPool; FTS5 writes drain
  serially through indexFetched to avoid SQLite WAL contention.
  Per-URL preview capped at 384 chars in batch mode (~3KB total) so
  context-savings hold under 8-URL fan-outs. composeFetchCacheKey
  wiring preserved across the refactor — same-label-different-URL
  collisions stay fixed (commit 1f1243e regression test enforced).
- effectiveConcurrency = min(N, os.cpus().length) when capByCpuCount
  set. Response surfaces capped count in caveman style.
- mcp_tool_call extractor (src/session/extract.ts) persists tool_input
  for mcp__* events with UTF-8-aware truncation at 2KB. Unlocks
  getMcpToolUsage() analytics — median/max concurrency per batch tool
  visible in ctx_stats.
- 14 adapter routing docs updated with the same Parallel I/O
  paragraph adapted to each host's tool-call prefix style. GitHub
  rate-limit caveat included consistently.

Hardening from 2-round architectural review
- Worker try/catch + Promise.allSettled isolation: one job throw no
  longer strands siblings or leaves undefined output slots.
- Timeout sentinel routes through formatCommandOutput: __CM_FS__
  markers stripped + bytes counted on partial-stdout-on-timeout.
- trackIndexed moved after FTS5 write succeeds (no over-count on
  failed indexes).
- UTF-8-aware truncate (Buffer.byteLength + continuation-byte
  walk-back): multi-byte payloads (CJK, 4-byte symbols) honor the
  byte budget without landing mid-codepoint.
- cpuCountForCap helper deleted: was CommonJS require in an ESM
  file, silently always returning 1. Replaced with top-level
  `cpus` import from node:os.

Tests (per CONTRIBUTING.md no-new-test-files rule, all under
existing files)
- 7 runPool unit tests: order, throw isolation, in-flight cap,
  job-count clamp, os.cpus cap, onSettled callback ordering.
- 13 ctx_fetch_and_index batch source-level tests: schema accepts
  both shapes, serial-write contract holds, backward-compat wording
  preserved, batch preview cap enforced, caveman header formatting,
  composeFetchCacheKey wiring across the refactor.
- 3 P0 hardening tests: throw-isolation, timeout marker stripping,
  5-cmd × 100ms at concurrency=5 < 200ms (CI-checked timing
  regression replacing the deleted bench).
- 4 mcp_tool_call extractor tests including UTF-8 multibyte
  regression.
- 3 getMcpToolUsage analytics tests.

Verification
- 138/138 server.test.ts pass; 309/309 across server + extract +
  analytics on cw/ctx-analytics.
- On next: 318/326 pass. 8 pre-existing unrelated failures
  (ctx_index projectRoot resolution from #365, ctx_execute_file env
  cascade, getSessionDir pre-detection) untouched.
- Typecheck clean.

Co-Authored-By: Sebastian Breguel <sebastianbreguel@gmail.com>

* fix(stats): restore Auto-memory, Opus pricing, and business-value footer

Commit b392c2f rewrote src/session/analytics.ts as part of the
opt-in concurrency feature and inadvertently dropped the user-facing
stats improvements landed in 4742160 (bugs #5/#6/#7/#8): the
Auto-memory preferences-learned line, the "Your AI talks less,
remembers more, costs less" tagline, the Opus pricing breakdown,
the "$X this session / $Y lifetime" footer, and the per-prefix
auto-memory bars. The 4th arg to formatReport also collapsed from
an options object {lifetime, mcpUsage} into bare mcpUsage, breaking
every test that passed lifetime data.

Restores the options-object signature, re-renders all dropped
sections under their original guards, and keeps b392c2f's runPool
and getMcpToolUsage infrastructure intact (no concurrency revert).
Updates the src/server.ts call site to match.

tests/session/stats-output-format.test.ts back to green (12/12 pass);
no other tests regress.

* feat(stats): persist runtime stats + status line bar (#399)

Adds a Claude Code statusLine integration so users see live token
savings at the bottom of their terminal without invoking any MCP
tool.

- src/server.ts: persistStats() writes <sessionDir>/stats-<sessionId>.json
  after every trackResponse / trackIndexed, throttled to 500ms; cleared
  on ctx_purge.
- bin/statusline.mjs: single-file Node script, no extra deps; walks the
  parent process chain via /proc/<pid>/status to find Claude Code; falls
  back to most recent stats-*.json within 30 minutes.
- src/cli.ts: context-mode statusline / statusline-install subcommands
  with safe ~/.claude/settings.json merge + timestamped backup.
- tests/statusline.test.ts: 8 hermetic cases covering idle, render,
  PPID fallback, stale sentinel, NaN guard, corrupt file, --json, error
  exit.
- README.md: status line wiring snippet for the Claude Code section.

Render aligned with the restored ctx_stats business voice (Auto-memory,
Opus pricing, "preserved across compact, restart & upgrade" tagline,
\$X this session / \$Y across sessions footer) so both surfaces speak
the same language.

Co-authored-by: Ousama Ben Younes <ousamabenyounes@users.noreply.github.com>

* fix(adapters/detect): full 14-platform PLATFORM_ENV_VARS audit + opencode-plugin DRY

PR #376 follow-up. mikij flagged that src/opencode-plugin.ts hardcoded a
KILO_PID-only check that violated DRY against PLATFORM_ENV_VARS. Audit of
the canonical list itself surfaced the broader problem: half of the entries
were unverified placeholders, 4 platforms (antigravity, zed, pi, openclaw)
were entirely missing or incorrectly listed, and the plugin paradigm's
fallback to "opencode" was blind (didn't actively check OPENCODE env vars).

What ships
- Re-audited every entry against the platform's own runtime source code:
  - kilo: dropped bare `KILO` (Kilo-Org/kilocode never sets it; only KILO_PID
    is set unconditionally at packages/opencode/src/index.ts:140).
  - jetbrains-copilot: dropped IDEA_HOME and JETBRAINS_CLIENT_ID (no
    source-line evidence in any JetBrains repo). Kept IDEA_INITIAL_DIRECTORY.
  - qwen-code: dropped QWEN_SESSION_ID (0 hits in QwenLM/qwen-code).
  - openclaw: removed entirely from env-var tier (runtime never sets
    OPENCLAW_HOME/OPENCLAW_CLI). Detection falls through to ~/.openclaw/
    config-dir tier, which already worked.

- Added 3 new platforms with verified env vars:
  - antigravity: ANTIGRAVITY_CLI_ALIAS — verified in Google's
    google-gemini/gemini-cli packages/core/src/ide/detect-ide.ts (canonical
    IDE detection map). Listed before vscode-copilot since Antigravity is an
    Electron/VSCode fork.
  - zed: ZED_SESSION_ID + ZED_TERM — verified in zed-industries/zed
    crates/terminal/src/terminal.rs `insert_zed_terminal_env()` and
    cross-confirmed by Google's gemini-cli detect-ide.ts.
  - pi: PI_PROJECT_DIR — confirmed by our own consumers at
    src/pi-extension.ts:154 and src/server.ts:153.

- Reordered fork pairs so collision detection works:
  - kilo before opencode (Kilo sets OPENCODE=1 because it's an OpenCode fork).
  - cursor + antigravity before vscode-copilot (both inherit VSCODE_PID).

- src/opencode-plugin.ts getPlatform() rewritten to iterate
  PLATFORM_ENV_VARS instead of hardcoding KILO_PID. Filters to kilo+opencode
  so a stray CLAUDE_PROJECT_DIR can't leak into the plugin's platform decision.
  Symmetric: actively checks BOTH platform's env vars instead of blind
  fallback. Per-line JSDoc credits PR #376 (mikij).

Tests
- tests/adapters/detect.test.ts: removed 5 broken assertions for unverified
  env vars; added 4 assertions for new platforms (antigravity, zed×2, pi)
  and a fork-collision test (KILO_PID + OPENCODE both set → kilo wins).
- tests/adapters/detect-config-dir.test.ts: rewrote priority chain from
  OPENCLAW/CODEX assertions to fork-collision assertions
  (KILO/OPENCODE, CURSOR/VSCODE, ANTIGRAVITY/VSCODE, CURSOR/CODEX).

Verification
- 451/451 adapter + plugin tests pass on next worktree.
- Typecheck clean.

Co-Authored-By: Mickey Lazarevic <noreply@github.com>

* fix(server): re-apply path-resolution + adapter-aware fixes lost in b392c2f

Commit b392c2f rewrote ~600 lines of src/server.ts as part of the
opt-in concurrency feature and inadvertently reverted PR #365 plus
fixes 1, 2, 4, 10 from the prior fix-army landings (#400 cluster).
Six independent regressions slipped through: ctx_index ignored
CLAUDE_PROJECT_DIR / IDEA_INITIAL_DIRECTORY / source-label
canonicalization, the deny-policy and executor cwd fell back to
ad-hoc CLAUDE_PROJECT_DIR ?? cwd(), getSessionDir lost its
detectPlatform pre-detection branch, and timeline-mode search lost
its worktree suffix, adapter-aware configDir, and adapter
pass-through.

Re-applies all of the above as a single squashed restore:

- isAbsolute import + resolveProjectPath helper.
- IDEA_INITIAL_DIRECTORY in getProjectDir() env cascade.
- ctx_index uses resolveProjectPath; source label canonicalises to
  the resolved absolute path so FTS5 dedup keys stop fragmenting
  across cwds.
- Executor takes a () => getProjectDir() thunk so ctx_execute_file
  picks up the full env cascade lazily, not just the constructor
  snapshot of CLAUDE_PROJECT_DIR.
- checkFilePathDenyPolicy reads getProjectDir() instead of the
  divergent CLAUDE_PROJECT_DIR ?? cwd() pattern.
- getSessionDir consults detectPlatform + getSessionDirSegments
  before the .claude fallback.
- Timeline mode opens SessionDB at hash+worktreeSuffix, derives
  configDir from _detectedAdapter.getConfigDir(), and threads the
  adapter into searchAllSources.

Also relaxes the fetch-cleanup static guard: b392c2f extracted the
fetch path into a runFetchOne helper, so the prior slice from the
registerTool call no longer covered ctx-fetch-*.dat. Asserts the
patterns at the file scope instead.

Local: 5 failing tests on next reduced from 13. The remaining five
are bundle-stale ctx_index spawn cases that pass once CI rebuilds
server.bundle.mjs on main.

* fix(security+release): PR #401 5-mode review follow-up — B3 redaction, SSRF guard, SHELL allowlist + 6 hardening fixes

5-agent review on PR #401 (v1.0.104) flagged P0 security + P1 release-quality
issues. This commit addresses every actionable finding except those requiring
release-process changes (npm version bump, grill-me gate — handled separately
on the release path).

Security (B3, SSRF guard, SHELL allowlist)
------------------------------------------
- src/session/extract.ts: mcp_tool_call extractor redacts secret-bearing keys
  BEFORE serialization. Walk via redactSecrets() with ancestor-set cycle
  detection (path-based, so DAG / shared-ref shapes process every site).
  Keys matching /authorization|token|secret|password|api_key|cookie|signature|
  private_key|client_secret/i are masked to "[REDACTED]". DAG-safe so a
  shared `headers` object referenced by multiple sub-requests gets redacted
  at every site.
- src/server.ts: ssrfGuard for ctx_fetch_and_index. Hard-blocks file://,
  gopher://, javascript:, data: schemes; hard-blocks 169.254.0.0/16
  (link-local incl. AWS/GCP/Azure IMDS 169.254.169.254), IPv6 link-local,
  multicast, reserved. Loopback + RFC1918 ALLOWED by default (developer
  workflow: local dev servers on localhost / internal network) — strict mode
  via CTX_FETCH_STRICT=1 blocks those too. DNS-resolves to defend against
  attacker-controlled DNS records / DNS rebinding. Runs BEFORE cache lookup
  so a previously-poisoned source label can't serve from cache.
- src/runtime.ts: SHELL env var allowlist. Basename must match
  /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/i. Cross-OS basename
  split handles both / and \ separators. Defends against profile-script
  compromise redirecting executor to /usr/bin/python or arbitrary binary.

Release quality (P1.1, P1.2, P1.3)
----------------------------------
- src/server.ts P1.1: OPUS_INPUT_PRICE_PER_TOKEN dedup. Removed local
  definition; imports from src/session/analytics.ts (single source of truth).
  Architect + Ops 2-vote convergence.
- src/server.ts P1.2: gracefulShutdown flushes persistStats with throttle
  bypass before exit. Last 0-500ms of bytes_indexed/bytes_returned no longer
  silently lost on SIGTERM/SIGINT.
- src/server.ts + bin/statusline.mjs P1.3: STATS_SCHEMA_VERSION=1 in payload.
  Statusline reads schemaVersion (defaults 0 for legacy bundles), warns to
  stderr when reading future schema, still parses known fields. Eliminates
  silent schema drift (architect review found dollars_saved_lifetime was
  removed without versioning).

Architecture / dev experience
-----------------------------
- src/adapters/* getConfigDir contract: always returns absolute path.
  Pre-fix: cursor/vscode-copilot/jetbrains-copilot/kiro/openclaw returned
  relative segments → server.ts:1587 consumed verbatim → corrupted timeline
  configDir. New contract documented in HookAdapter JSDoc; all adapters
  resolve via path.resolve(projectDir ?? cwd, segment).
- bin/statusline.mjs cross-OS PID resolution (B4): macOS now walks parent
  chain via `ps -o ppid=,comm= -p <pid>` (mirroring Linux /proc walk).
  Windows degrades to ppid with one-shot stderr warning. Fixes session-id
  mismatch where statusline #1 would show stats from session #2 on macOS.
- Deleted PRD-347-ppid-mismatch-wsl2.md + PRD-wsl2-ppid-sentinel.md —
  shipped as docs without implementation per Diagnose review. Implement
  later or remove the orphan.

Test consolidation (CONTRIBUTING.md L275)
-----------------------------------------
- 3 cache-heal test files merged into 1 with shared fixture helper:
  tests/hooks/cache-heal-build-command.test.ts (deleted),
  tests/hooks/cache-heal-stale-node-detection.test.ts (deleted),
  tests/hooks/cache-heal-self-heal.test.ts (4 describe blocks, 24 tests
  preserved, makeTmp/writeJson helpers extracted).

Verification
------------
- All new/modified test files pass:
  - tests/session/session-extract.test.ts: 153/153 (B3 + 2 new redaction tests)
  - tests/runtime.test.ts: 12/12 (SHELL allowlist + 4 new tests)
  - tests/core/server.test.ts SSRF block: 12/12 (classifyIp + ssrfGuard source-grep)
  - tests/statusline.test.ts: 13/13 (B4 cross-OS + schemaVersion handling)
  - tests/hooks/cache-heal-self-heal.test.ts: 24/24 (consolidated)
  - tests/adapters/memory-conventions.test.ts: 62/62 (getConfigDir contract)
- Full vitest run: 2170/2188 pass, 19 skipped, 14 pre-existing failures
  (8 opencode config-paths + 6 ctx_index/ctx_execute_file projectRoot
  resolution — both documented in PRD-concurrency-architectural §8 and
  Diagnose review baseline; both resolve via `npm run build`).
- npx tsc --noEmit clean.

Co-Authored-By: Mickey Lazarevic <noreply@github.com>

* fix(stats): persist dollars_saved_lifetime for statusline (#402)

fix(stats): persist dollars_saved_lifetime so statusline can render the brand-poem triad

The README shipped in 58a60d8 promises:

  context-mode  ●  $0.42 saved this session  ·  $12.30 saved across sessions  ·  87% efficient  ·  23m

bin/statusline.mjs reads `stats.dollars_saved_lifetime ?? 0` and only
renders the "saved across sessions" block when > 0. After the b392c2f
concurrency refactor + e638bd6 analytics restoration, getLifetimeStats
came back, but persistStats() never wired it into the JSON sidecar —
the statusline would always read 0 and the "remembers more" half of
the brand poem (talks-less / remembers / costs-less) would never
render.

Wire `getLifetimeStats({ sessionsDir: getSessionDir() })` into
persistStats() with a 30s cache. The 500ms persist throttle would be
too aggressive for a function that scans every per-project SessionDB
plus the auto-memory dir; the statusline doesn't need second-by-second
lifetime accuracy. Conversion factor (256 tokens/event = ~1KB ÷ 4
bytes/token) is the same one used by analytics.ts renderBottomLine,
extracted to a TOKENS_PER_EVENT constant so it stays in lockstep if
either side moves.

Failures during the disk scan keep the stale cache (or 0) — same
best-effort discipline as the surrounding persistStats() try/catch.

Verification
- npm run typecheck         clean
- npm run build             clean (cli 552kb, server 511kb)
- npx vitest run            74/74 files, 2130/2130 pass
- targeted: tests/statusline.test.ts + lifetime-stats + stats-output-format
  all green (16/16)

Addresses Critical 3 from the PR #399 review:
https://github.com/mksglu/context-mode/pull/399#issuecomment-4364664233

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ray <34021803+meikocho1@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ray <cho_meiko@okuribito-funeral.jp>
Co-authored-by: 津铭 <yongtingzhang@gmail.com>
Co-authored-by: Anton Okhontsev <anton.ohontsev@gmail.com>
Co-authored-by: Marcus Neufeldt <MarcusNeufeldt@users.noreply.github.com>
Co-authored-by: btxbtxbtx <btxbtxbtx@users.noreply.github.com>
Co-authored-by: Mickey Lazarevic <mikij@users.noreply.github.com>
Co-authored-by: VrianCao <45995071+VrianCao@users.noreply.github.com>
Co-authored-by: Tomodad <128800342+Tomodad@users.noreply.github.com>
Co-authored-by: Ousama Ben Younes <ousamabenyounes@users.noreply.github.com>
Co-authored-by: Sebastian Breguel <62109266+sebastianbreguel@users.noreply.github.com>
Co-authored-by: Sebastian Breguel <sebastianbreguel@gmail.com>
Co-authored-by: Mickey Lazarevic <noreply@github.com>
Co-authored-by: Ben Younes <benyounes.ousama@gmail.com>
2026-05-03 00:05:27 +03:00
Mert Koseoglu 36fefe206c feat(insight): session analytics — 90 metrics, 37 insights, 4 composite scores
Insight Dashboard:
- New /api/category-analytics endpoint with 8 sections (categories, errorIntelligence,
  delegation, governance, gitProductivity, contextHealth, fileIntelligence, compositeScores)
- 37 insight patterns (5 KILLER, 19 STRONG, 4 NICE) with severity sort + "Show all" toggle
- 4 composite scores (0-100): Productivity, Quality, Delegation, Context Health
- 5-min TTL response cache prevents double DB opens
- insufficientData guard (50 events / 3 sessions minimum)

Windows Hook Fixes (#369, #371, #372):
- fix: use process.execPath instead of bare 'node' for hook commands (#369)
- fix: forward-slash paths + quoting prevents MSYS path mangling on non-C: drives (#372)
- fix: ensure-deps no longer skips better-sqlite3 install on Node v24 (#371)
  Only the SIGSEGV-prone probe is skipped; prebuild-install always runs
- buildNodeCommand() utility in src/adapters/types.ts — cross-platform, MSYS-safe
- All 8 hook-building adapters updated

README:
- Updated "What gets captured" section: 14 → 23 event categories
- Updated Session Guide: 5 new sections
- Updated ctx_insight description: 90 metrics, 37 patterns, 4 composite scores

Closes #369, Closes #371, Closes #372
2026-04-28 13:36:18 +03:00
91cd6dfeb2 feat(codex): wire prompt and stop hooks (#360)
Co-authored-by: boederzeng <86715671+boederzeng@users.noreply.github.com>
Co-authored-by: Mert Köseoğlu <bm.ksglu@gmail.com>
2026-04-27 18:49:09 +03:00
Mert KoseogluandClaude Opus 4.6 859b87cac1 fix: replace bare tool shorthands with correct ctx_ prefixed names (#363)
All tool references in descriptions, response messages, and docs now use
registered MCP names (ctx_search, ctx_execute, etc.) instead of bare
shorthands (search, execute) that agents misinterpret as callable functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 18:18:25 +03:00