From 590d0507a533272d59f9badc9f10ab11313b4702 Mon Sep 17 00:00:00 2001 From: Bruno Azoulay Date: Fri, 4 Sep 2026 09:26:19 +0200 Subject: [PATCH] feat(fuse-ai-pilot): wire beforeMCPExecution to the aipilot scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc-cache gate lives in the harness under scope `aipilot`, but `beforeMCPExecution` was only wired by core-guards and fuse-design, both in scope `core` — so the gate was never reached. Adds the event to fuse-ai-pilot with a matcher covering the four documentation tools. Also records the measured Cursor adapter defects and the work-isolation requirement in the harness fix prompt. --- .cursor-plugin/marketplace.json | 2 +- .../fuse-ai-pilot/.cursor-plugin/plugin.json | 2 +- .../plugins/fuse-ai-pilot/hooks/hooks.json | 7 +++ docs/harness-cursor-fix-prompt.md | 53 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 99d3816..2dc1b24 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -36,7 +36,7 @@ "name": "fuse-ai-pilot", "source": ".cursor-plugin/plugins/fuse-ai-pilot", "description": "AI Pilot workflow agents: sniper (code quality), research-expert, explore-codebase, websearch", - "version": "1.2.40", + "version": "1.2.41", "author": { "name": "Fusengine", "email": "hello@fusengine.ch" diff --git a/.cursor-plugin/plugins/fuse-ai-pilot/.cursor-plugin/plugin.json b/.cursor-plugin/plugins/fuse-ai-pilot/.cursor-plugin/plugin.json index e8beb7f..e81eb7b 100644 --- a/.cursor-plugin/plugins/fuse-ai-pilot/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugins/fuse-ai-pilot/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "fuse-ai-pilot", - "version": "1.2.40", + "version": "1.2.41", "description": "AI Pilot workflow agents: sniper (code quality), research-expert, explore-codebase, websearch", "author": { "name": "Fusengine", diff --git a/.cursor-plugin/plugins/fuse-ai-pilot/hooks/hooks.json b/.cursor-plugin/plugins/fuse-ai-pilot/hooks/hooks.json index 74bf14b..53ce9ac 100644 --- a/.cursor-plugin/plugins/fuse-ai-pilot/hooks/hooks.json +++ b/.cursor-plugin/plugins/fuse-ai-pilot/hooks/hooks.json @@ -27,6 +27,13 @@ "command": "./scripts/hook.sh aipilot", "matcher": "Write" } + ], + "beforeMCPExecution": [ + { + "matcher": "MCP:(query-docs|resolve-library-id|web_search_exa|get_code_context_exa)", + "command": "./scripts/hook.sh aipilot", + "failClosed": false + } ] } } diff --git a/docs/harness-cursor-fix-prompt.md b/docs/harness-cursor-fix-prompt.md index c1f5501..610ff37 100644 --- a/docs/harness-cursor-fix-prompt.md +++ b/docs/harness-cursor-fix-prompt.md @@ -168,6 +168,59 @@ Include regression tests for the `0.1.90` tarball defects so a source-only fix c Require **version >0.1.90 before publishing or pinning**. Select the minimum valid next version only after packed parity is proven. This task must not publish or pin it. +## Measured defects — RED commands that must flip + +These three were obtained by executing the CLI against payloads built from `cursor.com/docs/hooks`, +never from a repository fixture. A forged fixture is what produced the original false green. Each +command below gives the wrong answer today; the task is done when each gives the right one. + +```bash +cd + +# 1. BLOCKING — must become allow, returns deny today. +# Phase is derived from `filePath && edits.length > 0` instead of `hook_event_name` +# (src/adapters/cursor/normalize.ts:33), so a degenerate afterFileEdit falls into the PRE +# pipeline and blocks. This is the "must not accidentally enter a different lifecycle branch" +# clause above, and it contradicts the module's own contract in +# src/adapters/cursor/index.ts:36-44 — "We deliberately never emit permission:\"deny\" here". +# Same cause, same fix: postToolUse, afterShellExecution and beforeReadFile also return deny +# on real payloads today. postToolUse has no `permission` field in its documented schema at all. +echo '{"hook_event_name":"afterFileEdit","file_path":"/tmp/x.ts","edits":[]}' | bun src/cli/bin.ts hook cursor core + +# 2. Must become deny, returns allow today. +# cursorToolName() (src/adapters/cursor/normalize.ts:14-18) does `tool === "Shell" || (!tool && hasCommand)`. +# Any tool_name present and different from "Shell" that still carries a command disarms every +# Bash guard: "Terminal", lowercase "shell", and the documented "MCP:" form. +# Exact-match allowlisting is literally the logic that produced the original P0. +# Robust criterion: a `command` is present => arm the guards, whatever the tool is called. +echo '{"hook_event_name":"preToolUse","tool_name":"Terminal","tool_input":{"command":"rm -rf /"}}' | bun src/cli/bin.ts hook cursor core + +# 3. Three consecutive runs must give identical counts and identical failing test names. +bun test 2>&1 | tail -3 +``` + +Also verify, and decide explicitly: the `afterFileEdit` size/DRY verdict is currently computed +from the fanned-out `edits[]` and then discarded (`src/runtime/post-outcome.ts:30`, plus the +`&& !cursorAfterFileEdit` guards in `handle-post.ts`). Net effect for the user is identical to +before the fix. Either emit it as `permission:"allow"` + `user_message` — the only channel +`afterFileEdit` exposes — or record it as a real loss. What is not acceptable is the current +state, where `test/cursor-cli-p0.test.ts` locks the silence in with `toEqual({exit:0, stdout:""})`: +a test that freezes a defect makes it permanent. + +## Work isolation — required before any "no regression" claim + +At last measurement the working tree held 25 modified files plus 6 untracked, including +`src/policy/guards/bash-write.ts`, `bash-write-safe-paths.ts`, `bash-write-redirects.ts`, +`protected-path.ts`, `src/policy/shell-read-refs.ts`, `src/prompt/types.ts`, +`src/runtime/confirm/confirm-gate.ts` and `codex-confirm.ts` — none of them Cursor adapter files. +The tree also changed *during* verification (12 modified files at the start, 25 at the end). + +Separate the `bash-write` work from the Cursor fix, then re-run the suite on a frozen tree. +While both live in the same tree, "0 failures" is unfalsifiable and the one deterministic +failure — `test/approval-never.test.ts:67`, where `evaluate(never(...))` returns `deny` instead +of `warn`, reproducible in the repo and absent from a pristine HEAD export — cannot be attributed +to either effort. + ## Verification report Report: