vexp-cli 2.7.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ import * as fs from "fs";
10
10
  import * as path from "path";
11
11
  import * as os from "os";
12
12
  import * as crypto from "crypto";
13
- import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpSearchHookScript, vexpHintHookCmdScript, vexpStopGateHookScript, vexpSessionContextHookScript, vexpOpencodeHintPlugin } from "./hook-template.js";
13
+ import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpSearchHookScript, vexpHintHookCmdScript, vexpStopGateHookScript, vexpSessionContextHookScript, vexpOpencodeHintPlugin, vexpOpencodeCompressPlugin, bakeEditHintHook, bakeReadHintHook, bakeBashCapHook } from "./hook-template.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // Constants
16
16
  // ---------------------------------------------------------------------------
@@ -102,6 +102,13 @@ export function setGuardMode(mode) {
102
102
  export function guardMode() {
103
103
  return activeGuardMode;
104
104
  }
105
+ let activeInterventionMode = "off";
106
+ export function setInterventionMode(mode) {
107
+ activeInterventionMode = mode;
108
+ }
109
+ export function interventionMode() {
110
+ return activeInterventionMode;
111
+ }
105
112
  // ---------------------------------------------------------------------------
106
113
  // Agent detectors - mirrors VS Code extension's AGENT_DETECTORS
107
114
  // ---------------------------------------------------------------------------
@@ -123,7 +130,7 @@ const AGENT_DETECTORS = [
123
130
  {
124
131
  agent: "Windsurf",
125
132
  detectPath: ".windsurf",
126
- configFile: ".windsurf/rules.md",
133
+ configFile: ".windsurf/rules/vexp.md",
127
134
  templateName: "windsurf",
128
135
  mcpConfigFile: ".windsurf/mcp.json",
129
136
  },
@@ -244,7 +251,7 @@ const AGENT_DETECTORS = [
244
251
  * true but badly incomplete story. Kept next to the installers, with a
245
252
  * lockstep test asserting every detector has an entry.
246
253
  */
247
- export function plannedWrites(agent, guard = guardMode()) {
254
+ export function plannedWrites(agent, guard = guardMode(), interventions = interventionMode()) {
248
255
  const det = AGENT_DETECTORS.find((d) => d.agent === agent);
249
256
  if (!det)
250
257
  return [];
@@ -255,8 +262,11 @@ export function plannedWrites(agent, guard = guardMode()) {
255
262
  case "Claude Code":
256
263
  out.push("~/.claude.json (MCP server entry, user scope)");
257
264
  out.push(".claude/hooks/vexp-hint.sh + settings.json hooks.UserPromptSubmit (orientation)");
258
- out.push(".claude/hooks/vexp-verify.sh + settings.json hooks.Stop (verification gate)");
259
265
  out.push(".claude/hooks/vexp-restore.sh + settings.json hooks.SessionStart (context restore)");
266
+ if (interventions === "on") {
267
+ out.push(".claude/hooks/vexp-verify.sh + settings.json hooks.Stop (verification gate, opt-in)");
268
+ out.push(".claude/hooks/vexp-edit-hint.sh + settings.json hooks.PostToolUse (coupling, opt-in)");
269
+ }
260
270
  if (guard === "strict") {
261
271
  out.push(".claude/hooks/vexp-guard.sh + settings.json hooks.PreToolUse (guard, opt-in)");
262
272
  }
@@ -334,6 +344,14 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
334
344
  for (const detector of agents) {
335
345
  const configFilePath = path.join(workspaceRoot, detector.configFile);
336
346
  const alreadyExists = fs.existsSync(configFilePath);
347
+ // Sweep the location we used to write to. For several releases the
348
+ // Windsurf rules went to `.windsurf/rules.md`, which Cascade does not
349
+ // read: its rules are `.windsurf/rules/*.md`, `.devin/rules/*.md`, or the
350
+ // legacy root `.windsurfrules`. Upgrading users would otherwise keep a
351
+ // stale, unread copy of our instructions beside the live one.
352
+ if (detector.agent === "Windsurf") {
353
+ removeVexpSection(path.join(workspaceRoot, ".windsurf", "rules.md"));
354
+ }
337
355
  const content = generateAgentConfig(detector.templateName, {
338
356
  workspaceRoot,
339
357
  binaryPath,
@@ -404,6 +422,15 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
404
422
  // 2.3 A2 opt-in applies to DENY hooks, this one cannot block).
405
423
  const hintResult = installClaudeCodeHintHook(workspaceRoot, binaryPath);
406
424
  installClaudeCodeSearchHook(workspaceRoot, binaryPath);
425
+ // Both doors. Bounding either alone measured as no change at all: the
426
+ // work simply moves to the other one.
427
+ if (interventionMode() === "on") {
428
+ installClaudeCodeReadHint(workspaceRoot, binaryPath);
429
+ installClaudeCodeBashCap(workspaceRoot, binaryPath);
430
+ }
431
+ else {
432
+ removeClaudeCodePreToolHooks(workspaceRoot);
433
+ }
407
434
  installClaudeCodeStopGate(workspaceRoot, binaryPath);
408
435
  installClaudeCodeSessionContext(workspaceRoot, binaryPath);
409
436
  if (hintResult) {
@@ -473,6 +500,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
473
500
  if (wrote)
474
501
  mcpConfigs.push(wrote);
475
502
  installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
503
+ installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
476
504
  // Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
477
505
  // markdown alone was demonstrably not enough (a reported session loaded
478
506
  // vexp.md, quoted it back, and still read five files by hand).
@@ -526,6 +554,18 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
526
554
  action: "removed",
527
555
  });
528
556
  }
557
+ // v5: the coupling on edit. Default-ON and independent of the guard —
558
+ // it blocks nothing, so there is no reason to make it opt-in.
559
+ const edit = installCursorEditHint(workspaceRoot, binaryPath);
560
+ if (edit) {
561
+ results.push({
562
+ agent: "Cursor Edit Hint",
563
+ configFile: path.join(".cursor", "hooks", "vexp-edit-hint.sh"),
564
+ content: "",
565
+ alreadyExists: edit === "updated",
566
+ action: edit,
567
+ });
568
+ }
529
569
  }
530
570
  // Cline: rules in .clinerules (generic template, handled by the writer
531
571
  // above); MCP registry is machine-global VS Code storage.
@@ -545,6 +585,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
545
585
  if (detector.agent === "Opencode") {
546
586
  const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
547
587
  installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
588
+ installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
548
589
  if (wroteMcp)
549
590
  mcpConfigs.push(wroteMcp);
550
591
  if (guardMode() === "strict") {
@@ -674,6 +715,11 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
674
715
  fs.mkdirSync(mcpDir, { recursive: true });
675
716
  }
676
717
  }
718
+ // Same sweep as the other writer: the Windsurf rules moved out of
719
+ // `.windsurf/rules.md`, which Cascade never read.
720
+ if (detector.agent === "Windsurf") {
721
+ removeVexpSection(path.join(workspaceRoot, ".windsurf", "rules.md"));
722
+ }
677
723
  const alreadyExists = fs.existsSync(configFilePath);
678
724
  const content = generateAgentConfig(detector.templateName, {
679
725
  workspaceRoot,
@@ -721,6 +767,13 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
721
767
  if (wrote)
722
768
  mcpConfigs.push("~/.claude.json");
723
769
  installClaudeCodeHintHook(workspaceRoot, binaryPath);
770
+ if (interventionMode() === "on") {
771
+ installClaudeCodeReadHint(workspaceRoot, binaryPath);
772
+ installClaudeCodeBashCap(workspaceRoot, binaryPath);
773
+ }
774
+ else {
775
+ removeClaudeCodePreToolHooks(workspaceRoot);
776
+ }
724
777
  installClaudeCodeStopGate(workspaceRoot, binaryPath);
725
778
  installClaudeCodeSessionContext(workspaceRoot, binaryPath);
726
779
  if (guardMode() === "strict")
@@ -761,12 +814,17 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
761
814
  if (wrote)
762
815
  mcpConfigs.push(wrote);
763
816
  installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
817
+ installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
764
818
  if (guardMode() === "strict")
765
819
  installOpencodePlugin(workspaceRoot);
766
820
  else
767
821
  uninstallOpencodePlugin(workspaceRoot);
768
822
  }
769
823
  if (detector.agent === "Cursor") {
824
+ if (interventionMode() === "on") {
825
+ installCursorReadHint(workspaceRoot, binaryPath);
826
+ installCursorBashCap(workspaceRoot, binaryPath);
827
+ }
770
828
  if (guardMode() === "strict")
771
829
  installCursorHook(workspaceRoot);
772
830
  else
@@ -781,6 +839,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
781
839
  if (wrote)
782
840
  mcpConfigs.push(wrote);
783
841
  installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
842
+ installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
784
843
  if (guardMode() === "strict")
785
844
  installKiloPlugin(workspaceRoot);
786
845
  else
@@ -1010,13 +1069,68 @@ function warnUnparseable(filePath) {
1010
1069
  }
1011
1070
  process.stderr.write(` [!] ${filePath} could not be parsed - leaving it untouched. Fix the file or add vexp manually, then re-run setup.\n`);
1012
1071
  }
1072
+ /**
1073
+ * Remove a vexp section from a file we no longer write to, and delete the file
1074
+ * when nothing of the user's is left in it.
1075
+ *
1076
+ * Needed because vexp wrote its Windsurf rules to `.windsurf/rules.md` for
1077
+ * several releases. Cascade reads `.windsurf/rules/*.md` (a directory),
1078
+ * `.devin/rules/*.md`, or the legacy root `.windsurfrules` — never that path.
1079
+ * Leaving the old file behind would put a stale, unread copy of our
1080
+ * instructions next to the live one.
1081
+ */
1082
+ export function removeVexpSection(filePath) {
1083
+ if (!fs.existsSync(filePath))
1084
+ return "absent";
1085
+ let existing;
1086
+ try {
1087
+ existing = fs.readFileSync(filePath, "utf-8");
1088
+ }
1089
+ catch {
1090
+ return "absent";
1091
+ }
1092
+ const marker = existing.match(VEXP_MARKER_RE);
1093
+ const endIdx = existing.indexOf(VEXP_MARKER_END);
1094
+ if (!marker || endIdx === -1)
1095
+ return "absent";
1096
+ const markerIdx = existing.indexOf(marker[0]);
1097
+ let startIdx = markerIdx;
1098
+ while (startIdx > 0 && existing[startIdx - 1] !== "\n")
1099
+ startIdx--;
1100
+ const rest = (existing.slice(0, startIdx) + existing.slice(endIdx + VEXP_MARKER_END.length)).trim();
1101
+ if (rest === "") {
1102
+ try {
1103
+ fs.unlinkSync(filePath);
1104
+ return "deleted";
1105
+ }
1106
+ catch {
1107
+ return "absent";
1108
+ }
1109
+ }
1110
+ try {
1111
+ fs.writeFileSync(filePath, rest + "\n", "utf-8");
1112
+ return "stripped";
1113
+ }
1114
+ catch {
1115
+ return "absent";
1116
+ }
1117
+ }
1013
1118
  function appendOrCreate(filePath, content, version) {
1014
1119
  if (!fs.existsSync(filePath)) {
1015
1120
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
1016
1121
  fs.writeFileSync(filePath, content, "utf-8");
1017
1122
  return "created";
1018
1123
  }
1019
- const existing = fs.readFileSync(filePath, "utf-8");
1124
+ let existing = fs.readFileSync(filePath, "utf-8");
1125
+ // Front matter has to stay the FIRST thing in the file, and the splice below
1126
+ // preserves whatever sits above our marker — so a rewrite would leave the
1127
+ // old block above and insert a second one. One rule file, one front matter.
1128
+ if (content.startsWith("---\n")) {
1129
+ const lead = existing.match(/^---\n[\s\S]*?\n---\n/);
1130
+ if (lead) {
1131
+ existing = existing.slice(lead[0].length).replace(/^\s*\n/, "");
1132
+ }
1133
+ }
1020
1134
  const markerMatch = existing.match(VEXP_MARKER_RE);
1021
1135
  if (!markerMatch) {
1022
1136
  fs.appendFileSync(filePath, "\n\n" + content);
@@ -1290,7 +1404,15 @@ function buildCodexSection(opts) {
1290
1404
  const wsHash = opts.workspaceRoot ? workspaceHash(opts.workspaceRoot) : "";
1291
1405
  const urlPath = wsHash ? `/ws/${wsHash}/mcp` : "/mcp";
1292
1406
  const desiredUrl = `http://127.0.0.1:${opts.mcpPort}${urlPath}`;
1293
- return `\n[mcp_servers.vexp]\n${CODEX_MANAGED_MARKER}: http transport (set VEXP_CODEX_TRANSPORT=direct for stdio)\nurl = "${desiredUrl}"\ntool_timeout_sec = 120\n\n[mcp_servers.vexp.http_headers]\nAuthorization = "Bearer ${opts.token}"\n`;
1407
+ return `
1408
+ [mcp_servers.vexp]
1409
+ ${CODEX_MANAGED_MARKER}: http transport (set VEXP_CODEX_TRANSPORT=direct for stdio)
1410
+ url = "${desiredUrl}"
1411
+ tool_timeout_sec = 120
1412
+
1413
+ [mcp_servers.vexp.http_headers]
1414
+ Authorization = "Bearer ${opts.token}"
1415
+ `;
1294
1416
  }
1295
1417
  /**
1296
1418
  * Configure MCP in ~/.codex/config.toml (global).
@@ -1965,11 +2087,27 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
1965
2087
  const hookPath = path.join(hookDir, "vexp-verify.sh");
1966
2088
  const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
1967
2089
  fs.mkdirSync(hookDir, { recursive: true });
1968
- const script = vexpStopGateHookScript(binaryPath);
2090
+ // Opt-in since the measurement: the gate bought 15 to 32 extra turns per 13
2091
+ // bench sessions, and they land at the END of a session where the
2092
+ // transcript is heaviest, so one of them costs more than a session's entire
2093
+ // tool output. See InterventionMode above. When off, a previously installed
2094
+ // gate is removed and de-registered so upgrades converge.
2095
+ const gateOn = interventionMode() === "on";
1969
2096
  const existed = fs.existsSync(hookPath);
1970
- const scriptIdentical = existed && fs.readFileSync(hookPath, "utf8") === script;
1971
- if (!scriptIdentical) {
1972
- fs.writeFileSync(hookPath, script, { mode: 0o755 });
2097
+ const script = vexpStopGateHookScript(binaryPath);
2098
+ const scriptIdentical = gateOn && existed && fs.readFileSync(hookPath, "utf8") === script;
2099
+ if (gateOn) {
2100
+ if (!scriptIdentical) {
2101
+ fs.writeFileSync(hookPath, script, { mode: 0o755 });
2102
+ }
2103
+ }
2104
+ else if (existed) {
2105
+ try {
2106
+ fs.unlinkSync(hookPath);
2107
+ }
2108
+ catch {
2109
+ /* a hook we cannot remove is still de-registered below */
2110
+ }
1973
2111
  }
1974
2112
  const read = readJsonConfigSafe(settingsPath);
1975
2113
  if (!read.ok) {
@@ -1981,16 +2119,20 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
1981
2119
  const existing = Array.isArray(hooks.Stop) ? hooks.Stop : [];
1982
2120
  const filtered = existing.filter((h) => !isVexpHookEntry(h, "vexp-verify"));
1983
2121
  // Cross-OS shape as everywhere: bash + QUOTED path; timeout in SECONDS.
1984
- filtered.push({
1985
- hooks: [
1986
- {
1987
- type: "command",
1988
- command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-verify.sh"',
1989
- timeout: 15,
1990
- },
1991
- ],
1992
- });
2122
+ if (gateOn) {
2123
+ filtered.push({
2124
+ hooks: [
2125
+ {
2126
+ type: "command",
2127
+ command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-verify.sh"',
2128
+ timeout: 15,
2129
+ },
2130
+ ],
2131
+ });
2132
+ }
1993
2133
  const merged = { ...hooks, Stop: filtered };
2134
+ if (filtered.length === 0)
2135
+ delete merged.Stop;
1994
2136
  const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
1995
2137
  if (scriptIdentical && settingsIdentical)
1996
2138
  return null;
@@ -2002,6 +2144,10 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
2002
2144
  }
2003
2145
  return existed ? "updated" : "created";
2004
2146
  }
2147
+ function isVexpEditHintHookEntry(entry) {
2148
+ const cmds = JSON.stringify(entry ?? "");
2149
+ return cmds.includes("vexp-edit-hint");
2150
+ }
2005
2151
  function isVexpHintHookEntry(h) {
2006
2152
  if (!h || typeof h !== "object")
2007
2153
  return false;
@@ -2046,6 +2192,99 @@ function isVexpSearchHookEntry(h) {
2046
2192
  * Why a hook rather than a tool: 9 of 227 measured agent sessions called the
2047
2193
  * MCP tools. All 227 called Bash.
2048
2194
  */
2195
+ /**
2196
+ * Install one of the compression hooks into Claude Code's PreToolUse.
2197
+ *
2198
+ * Shared because the two are one mechanism seen from two sides. Bounding reads
2199
+ * alone did nothing measurable: Read-tool tokens fell 54% and shell output rose
2200
+ * 39%, and the total sat inside the band between two identical stock runs. An
2201
+ * agent takes what it needs through whichever door is open.
2202
+ */
2203
+ /**
2204
+ * Undo the two compression hooks, so a workspace configured by an older
2205
+ * version converges on the next setup instead of quietly keeping a default
2206
+ * that was measured and withdrawn.
2207
+ *
2208
+ * Same contract as the guard's removal: take the files out AND de-register
2209
+ * them. A hook left in settings.json pointing at a script that no longer
2210
+ * exists is worse than either state on its own — Claude Code runs it, it
2211
+ * fails, and every Read or Bash call carries the failure.
2212
+ */
2213
+ function removeClaudeCodePreToolHooks(workspaceRoot) {
2214
+ const hookDir = path.join(workspaceRoot, ".claude", "hooks");
2215
+ for (const name of ["read-hint", "bash-cap"]) {
2216
+ const p = path.join(hookDir, `vexp-${name}.sh`);
2217
+ if (fs.existsSync(p)) {
2218
+ try {
2219
+ fs.unlinkSync(p);
2220
+ }
2221
+ catch {
2222
+ /* still de-registered below */
2223
+ }
2224
+ }
2225
+ }
2226
+ const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
2227
+ const read = readJsonConfigSafe(settingsPath);
2228
+ if (!read.ok || !read.existed)
2229
+ return;
2230
+ const settings = read.data;
2231
+ const hooks = (settings.hooks ?? {});
2232
+ const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
2233
+ const filtered = existing.filter((h) => !/vexp-(read-hint|bash-cap)/.test(JSON.stringify(h)));
2234
+ if (filtered.length === existing.length)
2235
+ return;
2236
+ const merged = { ...hooks, PreToolUse: filtered };
2237
+ if (filtered.length === 0)
2238
+ delete merged.PreToolUse;
2239
+ settings.hooks = merged;
2240
+ backupConfig(settingsPath);
2241
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2242
+ }
2243
+ function installClaudeCodePreToolHook(workspaceRoot, binaryPath, name, matcher, script) {
2244
+ const hookDir = path.join(workspaceRoot, ".claude", "hooks");
2245
+ const hookPath = path.join(hookDir, `vexp-${name}.sh`);
2246
+ const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
2247
+ fs.mkdirSync(hookDir, { recursive: true });
2248
+ const existed = fs.existsSync(hookPath);
2249
+ const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
2250
+ if (!scriptIdentical)
2251
+ fs.writeFileSync(hookPath, script, { mode: 0o755 });
2252
+ const read = readJsonConfigSafe(settingsPath);
2253
+ if (!read.ok) {
2254
+ warnUnparseable(settingsPath);
2255
+ return scriptIdentical ? null : existed ? "updated" : "created";
2256
+ }
2257
+ const settings = read.data;
2258
+ const hooks = (settings.hooks ?? {});
2259
+ const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
2260
+ const filtered = existing.filter((h) => !JSON.stringify(h).includes(`vexp-${name}`));
2261
+ // Shell form with a quoted path: exec form cannot run a .sh on Windows, and
2262
+ // an unquoted path word-splits on "C:\\Program Files" (c4a0b9e).
2263
+ filtered.push({
2264
+ matcher,
2265
+ hooks: [
2266
+ {
2267
+ type: "command",
2268
+ command: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-${name}.sh"`,
2269
+ timeout: 10,
2270
+ },
2271
+ ],
2272
+ });
2273
+ const merged = { ...hooks, PreToolUse: filtered };
2274
+ const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
2275
+ if (scriptIdentical && settingsIdentical)
2276
+ return null;
2277
+ settings.hooks = merged;
2278
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
2279
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2280
+ return existed ? "updated" : "created";
2281
+ }
2282
+ export function installClaudeCodeReadHint(workspaceRoot, binaryPath) {
2283
+ return installClaudeCodePreToolHook(workspaceRoot, binaryPath, "read-hint", "Read", bakeReadHintHook(binaryPath));
2284
+ }
2285
+ export function installClaudeCodeBashCap(workspaceRoot, binaryPath) {
2286
+ return installClaudeCodePreToolHook(workspaceRoot, binaryPath, "bash-cap", "Bash", bakeBashCapHook(binaryPath));
2287
+ }
2049
2288
  export function installClaudeCodeSearchHook(workspaceRoot, binaryPath) {
2050
2289
  const hookDir = path.join(workspaceRoot, ".claude", "hooks");
2051
2290
  const hookPath = path.join(hookDir, "vexp-search.sh");
@@ -2096,6 +2335,34 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
2096
2335
  const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
2097
2336
  const script = vexpHintHookScript(binaryPath);
2098
2337
  fs.mkdirSync(hookDir, { recursive: true });
2338
+ // v5: the edit-time coupling hook, written beside the prompt-time one -
2339
+ // but only when interventions are on. It is one of the two mechanisms that
2340
+ // ask the agent to go do something, and a turn costs 65,521 tokens. See
2341
+ // InterventionMode above for the measurement.
2342
+ const editHookPath = path.join(hookDir, "vexp-edit-hint.sh");
2343
+ const couplingOn = interventionMode() === "on";
2344
+ if (couplingOn) {
2345
+ const editScript = bakeEditHintHook(binaryPath);
2346
+ const editExisted = fs.existsSync(editHookPath);
2347
+ if (!editExisted || fs.readFileSync(editHookPath, "utf-8") !== editScript) {
2348
+ fs.writeFileSync(editHookPath, editScript, { mode: 0o755 });
2349
+ }
2350
+ try {
2351
+ fs.chmodSync(editHookPath, 0o755);
2352
+ }
2353
+ catch {
2354
+ /* Windows has no exec bit; Git Bash runs it anyway. */
2355
+ }
2356
+ }
2357
+ else if (fs.existsSync(editHookPath)) {
2358
+ // Converge an upgraded workspace without asking anyone to clean up.
2359
+ try {
2360
+ fs.unlinkSync(editHookPath);
2361
+ }
2362
+ catch {
2363
+ /* a hook we cannot remove is still de-registered below */
2364
+ }
2365
+ }
2099
2366
  const existed = fs.existsSync(hookPath);
2100
2367
  const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
2101
2368
  if (!scriptIdentical) {
@@ -2123,7 +2390,35 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
2123
2390
  },
2124
2391
  ],
2125
2392
  });
2126
- const merged = { ...hooks, UserPromptSubmit: filtered };
2393
+ // v5: the coupling on the edit that needs it. PostToolUse rather than
2394
+ // PreToolUse because a deny on an edit stops real work, and this must never
2395
+ // block — it adds no turn and no permission decision, only a line the model
2396
+ // reads as a system-reminder.
2397
+ const existingPost = Array.isArray(hooks.PostToolUse)
2398
+ ? hooks.PostToolUse
2399
+ : [];
2400
+ const filteredPost = existingPost.filter((h) => !isVexpEditHintHookEntry(h));
2401
+ if (couplingOn) {
2402
+ filteredPost.push({
2403
+ matcher: "Edit|Write|NotebookEdit",
2404
+ hooks: [
2405
+ {
2406
+ type: "command",
2407
+ command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-edit-hint.sh"',
2408
+ timeout: 5,
2409
+ },
2410
+ ],
2411
+ });
2412
+ }
2413
+ const merged = {
2414
+ ...hooks,
2415
+ UserPromptSubmit: filtered,
2416
+ PostToolUse: filteredPost,
2417
+ };
2418
+ // An empty array is not "no hook": it leaves a key behind that reads like a
2419
+ // configured-but-broken event. Drop it, as the extension copy does.
2420
+ if (filteredPost.length === 0)
2421
+ delete merged.PostToolUse;
2127
2422
  const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
2128
2423
  if (scriptIdentical && settingsIdentical)
2129
2424
  return null;
@@ -2140,6 +2435,21 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
2140
2435
  * unlike the deny guard (strict-only), this cannot break a session; it only
2141
2436
  * ever appends an orientation line the daemon judged useful.
2142
2437
  */
2438
+ /**
2439
+ * opencode / Kilo compression plugin: both doors, mutating rather than
2440
+ * blocking. Their `tool.execute.before` can rewrite `output.args`, which is
2441
+ * the whole mechanism.
2442
+ */
2443
+ export function installOpencodeCompressPlugin(workspaceRoot, binaryPath, pluginDir) {
2444
+ const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-compress.js");
2445
+ const content = vexpOpencodeCompressPlugin(binaryPath);
2446
+ fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
2447
+ const existed = fs.existsSync(pluginPath);
2448
+ if (existed && fs.readFileSync(pluginPath, "utf-8") === content)
2449
+ return null;
2450
+ fs.writeFileSync(pluginPath, content);
2451
+ return existed ? "updated" : "created";
2452
+ }
2143
2453
  export function installOpencodeHintPlugin(workspaceRoot, binaryPath, pluginDir) {
2144
2454
  const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-hint.js");
2145
2455
  const content = vexpOpencodeHintPlugin(binaryPath);
@@ -2317,6 +2627,58 @@ export function uninstallOpencodePlugin(workspaceRoot) {
2317
2627
  * `failClosed` is left at its default (false): a crashing guard must let the
2318
2628
  * agent work, not lock it out.
2319
2629
  */
2630
+ /**
2631
+ * Register a compression hook with Cursor.
2632
+ *
2633
+ * Cursor's `preToolUse` takes the SAME input as Claude Code's — `tool_name`,
2634
+ * `tool_input` — and answers in a different shape: `permission` plus a
2635
+ * snake_case `updated_input`. The binary emits either on request, so nothing
2636
+ * here has to know the difference beyond passing `--protocol cursor`.
2637
+ *
2638
+ * The binary is registered directly rather than through a shell script: there
2639
+ * is no `$CLAUDE_PROJECT_DIR` to expand here and no bash to assume on Windows.
2640
+ */
2641
+ function installCursorCompressionHook(workspaceRoot, binaryPath, sub, matcher) {
2642
+ const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
2643
+ const command = `${binaryPath} ${sub} --protocol cursor`;
2644
+ const read = readJsonConfigSafe(cfgPath);
2645
+ if (!read.ok) {
2646
+ warnUnparseable(cfgPath);
2647
+ return false;
2648
+ }
2649
+ const cfg = read.data;
2650
+ const hooks = cfg.hooks ?? {};
2651
+ const preToolUse = Array.isArray(hooks.preToolUse) ? [...hooks.preToolUse] : [];
2652
+ // Idempotent by SUBCOMMAND, not by exact command: the binary path changes
2653
+ // between installs and must replace its own entry rather than add one.
2654
+ const mine = (e) => !!e &&
2655
+ typeof e === "object" &&
2656
+ typeof e.command === "string" &&
2657
+ (e.command).includes(` ${sub} --protocol cursor`);
2658
+ const entry = { command, matcher };
2659
+ const at = preToolUse.findIndex(mine);
2660
+ if (at >= 0 && JSON.stringify(preToolUse[at]) === JSON.stringify(entry))
2661
+ return false;
2662
+ if (at >= 0)
2663
+ preToolUse[at] = entry;
2664
+ else
2665
+ preToolUse.push(entry);
2666
+ hooks.preToolUse = preToolUse;
2667
+ cfg.hooks = hooks;
2668
+ if (cfg.version === undefined)
2669
+ cfg.version = 1;
2670
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
2671
+ if (read.existed)
2672
+ backupConfig(cfgPath);
2673
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf-8");
2674
+ return true;
2675
+ }
2676
+ export function installCursorReadHint(workspaceRoot, binaryPath) {
2677
+ return installCursorCompressionHook(workspaceRoot, binaryPath, "read-hint", "Read");
2678
+ }
2679
+ export function installCursorBashCap(workspaceRoot, binaryPath) {
2680
+ return installCursorCompressionHook(workspaceRoot, binaryPath, "bash-cap", "Bash");
2681
+ }
2320
2682
  export function installCursorHook(workspaceRoot) {
2321
2683
  const rel = path.join(".cursor", "hooks", "vexp-guard.js");
2322
2684
  const hookPath = path.join(workspaceRoot, rel);
@@ -2424,6 +2786,58 @@ export function uninstallKiloPlugin(workspaceRoot) {
2424
2786
  }
2425
2787
  return removed;
2426
2788
  }
2789
+ /**
2790
+ * v5: the edit-time coupling for Cursor.
2791
+ *
2792
+ * Cursor sees a prompt but cannot inject at prompt time — `beforeSubmitPrompt`
2793
+ * is read-only. Its injectable channel is `postToolUse`, which is exactly
2794
+ * where this signal belongs anyway: the file has just been edited and the
2795
+ * coupling is what the plan is about to miss.
2796
+ *
2797
+ * Same binary, same daemon op, same fail-open contract as the Claude Code
2798
+ * hook. The point of doing both is that a signal only one agent receives is a
2799
+ * feature of that agent, not of the product.
2800
+ */
2801
+ export function installCursorEditHint(workspaceRoot, binaryPath) {
2802
+ const dir = path.join(workspaceRoot, ".cursor", "hooks");
2803
+ const scriptPath = path.join(dir, "vexp-edit-hint.sh");
2804
+ const script = bakeEditHintHook(binaryPath);
2805
+ fs.mkdirSync(dir, { recursive: true });
2806
+ const existed = fs.existsSync(scriptPath);
2807
+ if (!existed || fs.readFileSync(scriptPath, "utf-8") !== script) {
2808
+ fs.writeFileSync(scriptPath, script, { mode: 0o755 });
2809
+ }
2810
+ try {
2811
+ fs.chmodSync(scriptPath, 0o755);
2812
+ }
2813
+ catch {
2814
+ /* no exec bit on Windows */
2815
+ }
2816
+ const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
2817
+ const read = readJsonConfigSafe(cfgPath);
2818
+ if (!read.ok) {
2819
+ warnUnparseable(cfgPath);
2820
+ return null;
2821
+ }
2822
+ const cfg = read.data;
2823
+ const hooks = cfg.hooks ?? {};
2824
+ const post = Array.isArray(hooks.postToolUse) ? hooks.postToolUse : [];
2825
+ const filtered = post.filter((e) => {
2826
+ if (!e || typeof e !== "object")
2827
+ return true;
2828
+ const cmd = e.command;
2829
+ return typeof cmd !== "string" || !cmd.includes("vexp-edit-hint");
2830
+ });
2831
+ filtered.push({ command: `bash "${scriptPath}"` });
2832
+ hooks.postToolUse = filtered;
2833
+ const merged = { ...cfg, hooks };
2834
+ if (JSON.stringify(merged) === JSON.stringify(cfg) && existed)
2835
+ return null;
2836
+ if (read.existed)
2837
+ backupConfig(cfgPath);
2838
+ fs.writeFileSync(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
2839
+ return read.existed ? "updated" : "created";
2840
+ }
2427
2841
  /** Remove the Cursor guard (2.3 A2 default): delete the hook script and drop
2428
2842
  * our preToolUse entry from .cursor/hooks.json. */
2429
2843
  export function uninstallCursorHook(workspaceRoot) {
@@ -2472,12 +2886,47 @@ export function uninstallCursorHook(workspaceRoot) {
2472
2886
  * Shared by every prose template so the wording cannot drift again. ASCII only
2473
2887
  * (these land in files that get read on Windows).
2474
2888
  */
2475
- const QUERY_SHAPE = `### Query shape (do this)
2476
- - Anchor the task on real identifiers (ClassName, functionName) or file paths:
2477
- \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2478
- - A pure natural-language question ("why does login fail?") falls back to text
2479
- ranking and is much less reliable - name the symbols/files you want, not the question.`;
2480
- function generateAgentConfig(template, vars) {
2889
+ /**
2890
+ * The standing mandate, and it is deliberately short.
2891
+ *
2892
+ * Whatever this file says is resident in the agent's context on EVERY turn,
2893
+ * while a tool call happens at most once. Measured over 13 full bench
2894
+ * sessions: 37.1 API turns against a transcript averaging 65,521 tokens, so
2895
+ * a line here is billed 37 times and a call is billed once. The templates
2896
+ * this replaces ran 282 tokens (Claude Code) to 959 (generic) - up to 35,000
2897
+ * replayed tokens per session - and most of what they carried was a second
2898
+ * copy of the tool descriptions the MCP schemas already send.
2899
+ *
2900
+ * So this keeps only the three things a schema cannot carry:
2901
+ *
2902
+ * 1. WHEN to call, and when not to - the schema says what a tool returns,
2903
+ * not whether this task wants it.
2904
+ * 2. The two guardrails that stop wasted calls: text sweeps belong to
2905
+ * native search, and the index covers repo source only (logs, dist/,
2906
+ * node_modules are not in it - read those directly).
2907
+ * 3. The privacy line, because users ask and compliance reviews ask.
2908
+ *
2909
+ * Everything else was deleted, not shortened. See the catalog diet in
2910
+ * mcp_stdio.rs for the same argument applied one layer down.
2911
+ */
2912
+ const MANDATE_CORE = `### Context strategy: call run_pipeline ONCE at task start
2913
+ If the task already names the files/symbols to touch, SKIP vexp. Otherwise one
2914
+ \`run_pipeline({ "task": "..." })\` returns ranked pivot files with line ranges and
2915
+ blast radius. Do NOT open files one by one to find your way around - every extra
2916
+ tool call costs a turn. Call it again ONLY when the task moves to a new area.
2917
+ \`get_skeleton\` for files to understand, not edit. \`verify_done\` before calling a
2918
+ multi-file task complete, then RUN the tests it names.
2919
+
2920
+ ### Query shape (do this)
2921
+ Anchor the task on real identifiers (ClassName, functionName) or file paths:
2922
+ \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2923
+
2924
+ vexp runs entirely on this machine, index in \`.vexp/\`;
2925
+ \`run_pipeline\` transmits nothing to any external service.
2926
+ On \`status: "degraded"\` or 0 pivots the index is still building - use your own tools.
2927
+ For literal string sweeps use your native search - do NOT route text sweeps through vexp.
2928
+ Repo SOURCE only: logs, dist/, node_modules/ and files outside the repo are NOT indexed.`;
2929
+ export function generateAgentConfig(template, vars) {
2481
2930
  switch (template) {
2482
2931
  case "claude-code":
2483
2932
  return claudeCodeTemplate(vars);
@@ -2506,469 +2955,72 @@ function generateAgentConfig(template, vars) {
2506
2955
  // ---------------------------------------------------------------------------
2507
2956
  function claudeCodeTemplate(vars) {
2508
2957
  return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
2509
- vexp runs entirely on this machine: local daemon, index in \`.vexp/\`.
2510
- \`run_pipeline\` transmits nothing to any external service.
2511
- - \`run_pipeline({ "task": "..." })\` - orientation in one call (ranked pivot
2512
- files with line ranges + blast radius + session notes) when a task does NOT
2513
- name the files/symbols to touch. If it does, SKIP vexp - use your normal tools.
2514
- - \`get_skeleton\` - file structure at 70-90% token savings for files you only
2515
- need to understand, not edit.
2516
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2517
- returns mechanically broken references (imports of removed names, parse
2518
- errors), untouched dependents of the files you changed (file:line), and
2519
- the impacted tests - RUN those tests before declaring done.
2520
- - vexp may append a one-line hint to a prompt when orientation would help;
2521
- otherwise it stays silent.
2522
2958
 
2523
- ### Query shape (do this)
2524
- Anchor the task on real identifiers (ClassName, functionName) or file paths:
2525
- \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2959
+ ${MANDATE_CORE}
2526
2960
  <!-- /vexp -->`;
2527
2961
  }
2528
2962
  function cursorTemplate(vars) {
2529
2963
  return `## vexp rules for Cursor <!-- vexp v${vars.version} -->
2530
2964
 
2531
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2532
- If the task already names the files/symbols to touch, skip vexp and work
2533
- with your normal tools - a skipped call costs nothing.
2534
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2535
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2536
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2537
- For literal string sweeps (constants, log messages, config keys) use native search
2538
- directly - do NOT route text sweeps through vexp.
2539
- vexp runs entirely on this machine: local daemon, local index stored inside the
2540
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2541
- treat it like a local build tool; no data-sharing consent is needed to call it.
2542
-
2543
- ### Workflow
2544
- 1. \`run_pipeline\` with your task description - ONCE at task start
2545
- 2. Literal text sweeps with native search; Read the files you will edit
2546
- 3. Make targeted changes based on the context returned
2547
- 4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
2548
-
2549
- ### Available MCP tools
2550
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2551
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2552
- - \`get_skeleton\` - compact file structure
2553
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2554
- mechanically broken references, untouched dependents, and impacted tests
2555
- to RUN before declaring done, with file:line.
2556
- - \`index_status\` - indexing status
2557
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2558
-
2559
- (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2560
- capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2561
- those server-side, so the four above cover the normal workflow.)
2562
-
2563
- ${QUERY_SHAPE}
2564
-
2565
- ### Agentic search
2566
- - Ask vexp first for architecture/impact questions; native search remains the right
2567
- tool for literal text sweeps
2568
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2569
- so they do not re-explore from scratch
2570
-
2571
- ### Tips
2572
- - Add \`include_tests: true\` when debugging
2573
- - Use \`preset: "refactor"\` for deep impact analysis
2574
-
2575
- ### Fallback
2576
- If \`run_pipeline\` returns 0 pivots with an INDEX EMPTY warning, the index is rebuilding.
2577
- Use file search and read tools directly until the index is ready.
2578
-
2579
- ### Multi-Repo
2580
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
2581
-
2582
- > **Tip:** To avoid confirmation prompts on every tool call, enable Yolo mode:
2583
- > Cursor Settings -> Features -> Agent -> Enable Yolo Mode.
2965
+ ${MANDATE_CORE}
2584
2966
  <!-- /vexp -->`;
2585
2967
  }
2586
2968
  function windsurfTemplate(vars) {
2587
- return `## vexp for Windsurf <!-- vexp v${vars.version} -->
2588
-
2589
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2590
- If the task already names the files/symbols to touch, skip vexp and work
2591
- with your normal tools - a skipped call costs nothing.
2592
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2593
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2594
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2595
- For literal string sweeps (constants, log messages, config keys) use native search
2596
- directly - do NOT route text sweeps through vexp.
2597
- vexp runs entirely on this machine: local daemon, local index stored inside the
2598
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2599
- treat it like a local build tool; no data-sharing consent is needed to call it.
2600
-
2601
- ### Workflow
2602
- 1. \`run_pipeline\` with your task description - ONCE at task start
2603
- 2. Literal text sweeps with native search; Read the files you will edit
2604
- 3. Make targeted changes based on the context returned
2605
- 4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
2606
-
2607
- ### Available MCP tools
2608
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2609
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2610
- - \`get_skeleton\` - compact file structure
2611
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2612
- mechanically broken references, untouched dependents, and impacted tests
2613
- to RUN before declaring done, with file:line.
2614
- - \`index_status\` - indexing status
2615
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2616
-
2617
- (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2618
- capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2619
- those server-side, so the four above cover the normal workflow.)
2620
-
2621
- ${QUERY_SHAPE}
2622
-
2623
- ### Agentic search
2624
- - Ask vexp first for architecture/impact questions; native search remains the right
2625
- tool for literal text sweeps
2626
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2627
- so they do not re-explore from scratch
2628
-
2629
- ### Smart Features
2630
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
2631
-
2632
- ### Multi-Repo
2633
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
2969
+ // Front matter, because a rule without one is not always on. Cascade puts
2970
+ // the FULL text of an \`always_on\` rule in the system prompt on every
2971
+ // message; the other modes make the agent choose to look, which is the
2972
+ // complaint every Windsurf user brings ("it ignores the index unless I
2973
+ // remind it"). This is the one lever Windsurf gives us and we were not
2974
+ // pulling it.
2975
+ return `---
2976
+ trigger: always_on
2977
+ description: vexp code-graph orientation for this workspace
2978
+ ---
2979
+
2980
+ ## vexp for Windsurf <!-- vexp v${vars.version} -->
2981
+
2982
+ ${MANDATE_CORE}
2634
2983
  <!-- /vexp -->`;
2635
2984
  }
2636
2985
  function continueTemplate(vars) {
2637
- return JSON.stringify({
2638
- customCommands: [
2639
- {
2640
- name: "vexp-context",
2641
- description: "Run vexp pipeline for current task",
2642
- prompt: "{{{ input }}}\n\nFirst, call run_pipeline with the above task description.",
2643
- },
2644
- ],
2645
- mcpServers: [
2646
- {
2647
- name: "vexp",
2648
- command: vars.binaryPath,
2649
- args: ["mcp"],
2650
- type: "stdio",
2651
- },
2652
- ],
2653
- }, null, 2);
2986
+ return `## vexp <!-- vexp v${vars.version} -->
2987
+
2988
+ ${MANDATE_CORE}
2989
+ <!-- /vexp -->`;
2654
2990
  }
2655
2991
  function augmentTemplate(vars) {
2656
- return `## vexp for Augment <!-- vexp v${vars.version} -->
2657
-
2658
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2659
- If the task already names the files/symbols to touch, skip vexp and work
2660
- with your normal tools - a skipped call costs nothing.
2661
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2662
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2663
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2664
- For literal string sweeps (constants, log messages, config keys) use native search
2665
- directly - do NOT route text sweeps through vexp.
2666
- vexp runs entirely on this machine: local daemon, local index stored inside the
2667
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2668
- treat it like a local build tool; no data-sharing consent is needed to call it.
2669
-
2670
- When working on this codebase:
2671
- 1. \`run_pipeline\` with task description - ONCE at task start
2672
- 2. Literal text sweeps with native search; Read the files you will edit
2673
- 3. Make targeted changes based on the context returned
2674
- 4. \`run_pipeline\` again ONLY when the task moves to a new area
2675
-
2676
- ### Available MCP tools
2677
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2678
- Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2679
- - \`get_skeleton\` - token-efficient file structure
2680
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2681
- mechanically broken references, untouched dependents, and impacted tests
2682
- to RUN before declaring done, with file:line.
2683
- - \`index_status\` - indexing status
2684
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2685
-
2686
- (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2687
- capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2688
- those server-side, so the four above cover the normal workflow.)
2689
-
2690
- ${QUERY_SHAPE}
2691
-
2692
- ### Agentic search
2693
- - Ask vexp first for architecture/impact questions; native search remains the right
2694
- tool for literal text sweeps
2695
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2696
- so they do not re-explore from scratch
2697
-
2698
- ### Smart Features
2699
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
2992
+ return `## vexp <!-- vexp v${vars.version} -->
2700
2993
 
2701
- ### Multi-Repo
2702
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
2994
+ ${MANDATE_CORE}
2703
2995
  <!-- /vexp -->`;
2704
2996
  }
2705
2997
  function copilotTemplate(vars) {
2706
- return `## vexp context tools <!-- vexp v${vars.version} -->
2707
-
2708
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2709
- If the task already names the files/symbols to touch, skip vexp and work
2710
- with your normal tools - a skipped call costs nothing.
2711
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2712
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2713
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2714
- For literal string sweeps (constants, log messages, config keys) use native search
2715
- directly - do NOT route text sweeps through vexp.
2716
- vexp runs entirely on this machine: local daemon, local index stored inside the
2717
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2718
- treat it like a local build tool; no data-sharing consent is needed to call it.
2719
-
2720
- ### Workflow
2721
- 1. \`run_pipeline\` with your task description - ONCE at task start
2722
- 2. Literal text sweeps with native search; Read the files you will edit
2723
- 3. Make targeted changes based on the context returned
2724
- 4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
2725
-
2726
- ### Available MCP tools
2727
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2728
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2729
- - \`get_skeleton\` - compact file structure
2730
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2731
- mechanically broken references, untouched dependents, and impacted tests
2732
- to RUN before declaring done, with file:line.
2733
- - \`index_status\` - indexing status
2734
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2735
-
2736
- (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2737
- capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2738
- those server-side, so the four above cover the normal workflow.)
2739
-
2740
- ${QUERY_SHAPE}
2741
-
2742
- ### Agentic search
2743
- - Ask vexp first for architecture/impact questions; native search remains the right
2744
- tool for literal text sweeps
2745
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2746
- so they do not re-explore from scratch
2747
-
2748
- ### Smart Features
2749
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
2998
+ return `## vexp <!-- vexp v${vars.version} -->
2750
2999
 
2751
- ### Multi-Repo
2752
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
3000
+ ${MANDATE_CORE}
2753
3001
  <!-- /vexp -->`;
2754
3002
  }
2755
3003
  function zedTemplate(vars) {
2756
- return `## vexp for Zed <!-- vexp v${vars.version} -->
2757
-
2758
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2759
- If the task already names the files/symbols to touch, skip vexp and work
2760
- with your normal tools - a skipped call costs nothing.
2761
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2762
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2763
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2764
- For literal string sweeps (constants, log messages, config keys) use native search
2765
- directly - do NOT route text sweeps through vexp.
2766
- vexp runs entirely on this machine: local daemon, local index stored inside the
2767
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2768
- treat it like a local build tool; no data-sharing consent is needed to call it.
2769
-
2770
- ### Workflow
2771
- 1. \`run_pipeline\` with your task description - ONCE at task start
2772
- 2. Literal text sweeps with native search; Read the files you will edit
2773
- 3. Make targeted changes based on the context returned
2774
- 4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
2775
-
2776
- ### Available MCP tools
2777
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2778
- Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2779
- - \`get_skeleton\` - compact file structure
2780
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2781
- mechanically broken references, untouched dependents, and impacted tests
2782
- to RUN before declaring done, with file:line.
2783
- - \`index_status\` - indexing status
2784
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2785
-
2786
- (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2787
- capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2788
- those server-side, so the four above cover the normal workflow.)
2789
-
2790
- ${QUERY_SHAPE}
2791
-
2792
- ### Agentic search
2793
- - Ask vexp first for architecture/impact questions; native search remains the right
2794
- tool for literal text sweeps
2795
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2796
- so they do not re-explore from scratch
2797
-
2798
- ### Smart Features
2799
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
3004
+ return `## vexp <!-- vexp v${vars.version} -->
2800
3005
 
2801
- ### Multi-Repo
2802
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
3006
+ ${MANDATE_CORE}
2803
3007
  <!-- /vexp -->`;
2804
3008
  }
2805
3009
  function agentsMdTemplate(vars) {
2806
- return `## vexp <!-- vexp v${vars.version} -->
2807
-
2808
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2809
- If the task already names the files/symbols to touch, skip vexp and work
2810
- with your normal tools - a skipped call costs nothing.
2811
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2812
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2813
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2814
- For literal string sweeps (constants, log messages, config keys) use native search
2815
- directly - do NOT route text sweeps through vexp.
2816
- vexp runs entirely on this machine: local daemon, local index stored inside the
2817
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2818
- treat it like a local build tool; no data-sharing consent is needed to call it.
2819
-
2820
- ### Workflow
2821
- 1. \`run_pipeline\` with your task description - ONCE at task start
2822
- 2. Literal text sweeps with native search; Read the files you will edit
2823
- 3. Make targeted changes based on the context returned
2824
- 4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
2825
-
2826
- ### Available MCP tools
2827
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2828
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2829
- - \`get_skeleton\` - compact file structure
2830
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2831
- mechanically broken references, untouched dependents, and impacted tests
2832
- to RUN before declaring done, with file:line.
2833
- - \`index_status\` - indexing status
2834
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2835
-
2836
- ${QUERY_SHAPE}
2837
-
2838
- ### Agentic search
2839
- - Ask vexp first for architecture/impact questions; native search remains the right
2840
- tool for literal text sweeps
2841
- - vexp only covers indexed source inside the workspace. For runtime logs, build output
2842
- (dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
2843
- normal tools there.
2844
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2845
- so they do not re-explore from scratch
2846
-
2847
- ### Smart Features
2848
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
3010
+ return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
2849
3011
 
2850
- ### Multi-Repo
2851
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
3012
+ ${MANDATE_CORE}
2852
3013
  <!-- /vexp -->`;
2853
3014
  }
2854
3015
  function kiroTemplate(vars) {
2855
3016
  return `# vexp steering <!-- vexp v${vars.version} -->
2856
3017
 
2857
- **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2858
- If the task already names the files/symbols to touch, skip vexp and work
2859
- with your normal tools - a skipped call costs nothing.
2860
- vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2861
- On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2862
- bodies, covering tests and impact - work directly from it instead of re-reading sources.
2863
- For literal string sweeps (constants, log messages, config keys) use native search
2864
- directly - do NOT route text sweeps through vexp.
2865
- vexp runs entirely on this machine: local daemon, local index stored inside the
2866
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2867
- treat it like a local build tool; no data-sharing consent is needed to call it.
2868
-
2869
- ## Workflow
2870
- 1. \`run_pipeline\` with your task description - ONCE at task start
2871
- 2. Literal text sweeps with native search; Read the files you will edit
2872
- 3. Make targeted changes based on the context returned
2873
- 4. \`run_pipeline\` again ONLY when the task moves to a new area - not per turn
2874
-
2875
- ## Available vexp tools
2876
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
2877
- Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2878
- - \`get_skeleton\` - compact file structure
2879
- - \`verify_done\` - call once BEFORE declaring a multi-file task complete:
2880
- mechanically broken references, untouched dependents, and impacted tests
2881
- to RUN before declaring done, with file:line.
2882
- - \`index_status\` - indexing status
2883
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2884
-
2885
- (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2886
- capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2887
- those server-side, so the four above cover the normal workflow.)
2888
-
2889
- ${QUERY_SHAPE}
2890
-
2891
- ## Agentic search
2892
- - Ask vexp first for architecture/impact questions; native search remains the right
2893
- tool for literal text sweeps
2894
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
2895
- so they do not re-explore from scratch
2896
-
2897
- ## Smart Features
2898
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
2899
-
2900
- ## Multi-Repo
2901
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
3018
+ ${MANDATE_CORE}
2902
3019
  <!-- /vexp -->`;
2903
3020
  }
2904
3021
  function genericTemplate(vars) {
2905
3022
  return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
2906
3023
 
2907
- ### Context strategy: call run_pipeline ONCE at task start
2908
- If the task already names the files/symbols to touch, skip vexp and work
2909
- with your normal tools - a skipped call costs nothing.
2910
- For every multi-file task - bug fixes, features, refactors, questions about the code:
2911
- **start with one \`run_pipeline\` call**. It runs context search + impact analysis +
2912
- memory recall in a single call and returns compressed, graph-ranked results: the
2913
- callers, the blast radius and the co-changed files that opening a file cannot show.
2914
-
2915
- Then implement with your normal tools:
2916
- - Literal text sweeps (string constants, log messages, config keys): use your
2917
- native search directly - do NOT route text sweeps through vexp.
2918
- - Read the files you are about to edit; use \`get_skeleton\`
2919
- (detail: minimal/standard/detailed, 70-90% fewer tokens) for files you only
2920
- need to understand, not edit.
2921
- - Do NOT open files one by one to find your way around - one good pipeline call
2922
- beats five exploratory reads; every extra tool call costs a turn.
2923
- - Before declaring a multi-file task complete, call \`verify_done\` once:
2924
- it returns mechanically broken references (imports of removed names,
2925
- parse errors), untouched dependents of your changes (file:line), and the
2926
- impacted tests - RUN those tests before declaring done.
2927
-
2928
- vexp runs entirely on this machine: local daemon, local index stored inside the
2929
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
2930
- treat it like a local build tool; no data-sharing consent is needed to call it.
2931
-
2932
- vexp indexes source inside the workspace and nothing else. Runtime logs, build
2933
- output (dist/, .vite/, node_modules/) and files outside the repo are NOT indexed -
2934
- read those directly, this rule does not cover them.
2935
-
2936
- ### Primary tool
2937
- - \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Auto-detects intent
2938
- (debug/modify/refactor/explore) from your task. Includes file content for pivots.
2939
- - \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2940
- - \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\`
2941
- - \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - saves an insight in the same call
2942
-
2943
- ### Other MCP tools (only when run_pipeline is not enough)
2944
- - \`get_skeleton\` - **preferred over reading a file**: signatures and structure, 3 detail levels
2945
- - \`index_status\` - indexing status and health check
2946
- - \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
2947
-
2948
- ${QUERY_SHAPE}
2949
-
2950
- ### Workflow
2951
- 1. \`run_pipeline("your task")\` - ONCE at task start. Returns pivots + impact + memories in 1 call
2952
- 2. Literal string sweeps with native search; Read the files you will edit
2953
- 3. Structural overview without editing? \`get_skeleton({ files: [...], detail: "detailed" })\`
2954
- 4. Make targeted changes based on the context returned
2955
- 5. \`run_pipeline\` again ONLY when the task moves to a new area - do NOT chain vexp calls
2956
-
2957
- ### Sub-agents and background tasks
2958
- - Sub-agents CAN call \`run_pipeline\` - always give them the task description
2959
- - For architecture exploration, call \`run_pipeline\` first and pass the returned
2960
- context into the agent prompt - it usually replaces the exploration entirely
2961
-
2962
- ### Fallback
2963
- If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
2964
- the index is empty or still building. Use the built-in search and read tools directly
2965
- until it is ready - do not stall waiting for vexp.
2966
-
2967
- ### Smart features (automatic - no action needed)
2968
- Intent detection, hybrid keyword+semantic+graph ranking, session memory,
2969
- change coupling, auto-expanding budget.
2970
-
2971
- ### Multi-repo
2972
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
3024
+ ${MANDATE_CORE}
2973
3025
  <!-- /vexp -->`;
2974
3026
  }