vexp-cli 2.7.0 → 3.1.0
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.
- package/README.md +1 -0
- package/dist/agent-config.js +640 -468
- package/dist/cli.js +39 -13
- package/dist/doctor.js +169 -4
- package/dist/hook-template.js +177 -1
- package/dist/license.js +10 -6
- package/dist/secret-prompt.js +62 -0
- package/mcp/mcp-server.cjs +40 -38
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -116,14 +123,16 @@ const AGENT_DETECTORS = [
|
|
|
116
123
|
{
|
|
117
124
|
agent: "Cursor",
|
|
118
125
|
detectPath: ".cursor",
|
|
119
|
-
|
|
126
|
+
// Cursor reads a DIRECTORY of rules here (`.cursor/rules/*.mdc`); the
|
|
127
|
+
// file inside is ours. See `cursorRulesTarget` for the one exception.
|
|
128
|
+
configFile: ".cursor/rules/vexp.mdc",
|
|
120
129
|
templateName: "cursor",
|
|
121
130
|
mcpConfigFile: ".cursor/mcp.json",
|
|
122
131
|
},
|
|
123
132
|
{
|
|
124
133
|
agent: "Windsurf",
|
|
125
134
|
detectPath: ".windsurf",
|
|
126
|
-
configFile: ".windsurf/rules.md",
|
|
135
|
+
configFile: ".windsurf/rules/vexp.md",
|
|
127
136
|
templateName: "windsurf",
|
|
128
137
|
mcpConfigFile: ".windsurf/mcp.json",
|
|
129
138
|
},
|
|
@@ -244,7 +253,7 @@ const AGENT_DETECTORS = [
|
|
|
244
253
|
* true but badly incomplete story. Kept next to the installers, with a
|
|
245
254
|
* lockstep test asserting every detector has an entry.
|
|
246
255
|
*/
|
|
247
|
-
export function plannedWrites(agent, guard = guardMode()) {
|
|
256
|
+
export function plannedWrites(agent, guard = guardMode(), interventions = interventionMode()) {
|
|
248
257
|
const det = AGENT_DETECTORS.find((d) => d.agent === agent);
|
|
249
258
|
if (!det)
|
|
250
259
|
return [];
|
|
@@ -255,8 +264,11 @@ export function plannedWrites(agent, guard = guardMode()) {
|
|
|
255
264
|
case "Claude Code":
|
|
256
265
|
out.push("~/.claude.json (MCP server entry, user scope)");
|
|
257
266
|
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
267
|
out.push(".claude/hooks/vexp-restore.sh + settings.json hooks.SessionStart (context restore)");
|
|
268
|
+
if (interventions === "on") {
|
|
269
|
+
out.push(".claude/hooks/vexp-verify.sh + settings.json hooks.Stop (verification gate, opt-in)");
|
|
270
|
+
out.push(".claude/hooks/vexp-edit-hint.sh + settings.json hooks.PostToolUse (coupling, opt-in)");
|
|
271
|
+
}
|
|
260
272
|
if (guard === "strict") {
|
|
261
273
|
out.push(".claude/hooks/vexp-guard.sh + settings.json hooks.PreToolUse (guard, opt-in)");
|
|
262
274
|
}
|
|
@@ -331,9 +343,18 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
331
343
|
const filter = agentFilter.map((a) => a.toLowerCase());
|
|
332
344
|
agents = agents.filter((d) => filter.includes(d.agent.toLowerCase()));
|
|
333
345
|
}
|
|
334
|
-
for (const
|
|
346
|
+
for (const detected of agents) {
|
|
347
|
+
const detector = withCursorTarget(detected, workspaceRoot);
|
|
335
348
|
const configFilePath = path.join(workspaceRoot, detector.configFile);
|
|
336
349
|
const alreadyExists = fs.existsSync(configFilePath);
|
|
350
|
+
// Sweep the location we used to write to. For several releases the
|
|
351
|
+
// Windsurf rules went to `.windsurf/rules.md`, which Cascade does not
|
|
352
|
+
// read: its rules are `.windsurf/rules/*.md`, `.devin/rules/*.md`, or the
|
|
353
|
+
// legacy root `.windsurfrules`. Upgrading users would otherwise keep a
|
|
354
|
+
// stale, unread copy of our instructions beside the live one.
|
|
355
|
+
if (detector.agent === "Windsurf") {
|
|
356
|
+
removeVexpSection(path.join(workspaceRoot, ".windsurf", "rules.md"));
|
|
357
|
+
}
|
|
337
358
|
const content = generateAgentConfig(detector.templateName, {
|
|
338
359
|
workspaceRoot,
|
|
339
360
|
binaryPath,
|
|
@@ -404,6 +425,15 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
404
425
|
// 2.3 A2 opt-in applies to DENY hooks, this one cannot block).
|
|
405
426
|
const hintResult = installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
406
427
|
installClaudeCodeSearchHook(workspaceRoot, binaryPath);
|
|
428
|
+
// Both doors. Bounding either alone measured as no change at all: the
|
|
429
|
+
// work simply moves to the other one.
|
|
430
|
+
if (interventionMode() === "on") {
|
|
431
|
+
installClaudeCodeReadHint(workspaceRoot, binaryPath);
|
|
432
|
+
installClaudeCodeBashCap(workspaceRoot, binaryPath);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
removeClaudeCodePreToolHooks(workspaceRoot);
|
|
436
|
+
}
|
|
407
437
|
installClaudeCodeStopGate(workspaceRoot, binaryPath);
|
|
408
438
|
installClaudeCodeSessionContext(workspaceRoot, binaryPath);
|
|
409
439
|
if (hintResult) {
|
|
@@ -473,6 +503,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
473
503
|
if (wrote)
|
|
474
504
|
mcpConfigs.push(wrote);
|
|
475
505
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
506
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
476
507
|
// Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
|
|
477
508
|
// markdown alone was demonstrably not enough (a reported session loaded
|
|
478
509
|
// vexp.md, quoted it back, and still read five files by hand).
|
|
@@ -526,6 +557,18 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
526
557
|
action: "removed",
|
|
527
558
|
});
|
|
528
559
|
}
|
|
560
|
+
// v5: the coupling on edit. Default-ON and independent of the guard —
|
|
561
|
+
// it blocks nothing, so there is no reason to make it opt-in.
|
|
562
|
+
const edit = installCursorEditHint(workspaceRoot, binaryPath);
|
|
563
|
+
if (edit) {
|
|
564
|
+
results.push({
|
|
565
|
+
agent: "Cursor Edit Hint",
|
|
566
|
+
configFile: path.join(".cursor", "hooks", "vexp-edit-hint.sh"),
|
|
567
|
+
content: "",
|
|
568
|
+
alreadyExists: edit === "updated",
|
|
569
|
+
action: edit,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
529
572
|
}
|
|
530
573
|
// Cline: rules in .clinerules (generic template, handled by the writer
|
|
531
574
|
// above); MCP registry is machine-global VS Code storage.
|
|
@@ -545,6 +588,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
|
|
|
545
588
|
if (detector.agent === "Opencode") {
|
|
546
589
|
const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
|
|
547
590
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
591
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
548
592
|
if (wroteMcp)
|
|
549
593
|
mcpConfigs.push(wroteMcp);
|
|
550
594
|
if (guardMode() === "strict") {
|
|
@@ -648,7 +692,8 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
648
692
|
const results = [];
|
|
649
693
|
const mcpConfigs = [];
|
|
650
694
|
const writtenConfigFiles = new Set();
|
|
651
|
-
for (const
|
|
695
|
+
for (const detected of AGENT_DETECTORS) {
|
|
696
|
+
const detector = withCursorTarget(detected, workspaceRoot);
|
|
652
697
|
if (!selectedAgentNames.includes(detector.agent))
|
|
653
698
|
continue;
|
|
654
699
|
// Create agent directory if it doesn't exist. Skip for file-based
|
|
@@ -674,6 +719,11 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
674
719
|
fs.mkdirSync(mcpDir, { recursive: true });
|
|
675
720
|
}
|
|
676
721
|
}
|
|
722
|
+
// Same sweep as the other writer: the Windsurf rules moved out of
|
|
723
|
+
// `.windsurf/rules.md`, which Cascade never read.
|
|
724
|
+
if (detector.agent === "Windsurf") {
|
|
725
|
+
removeVexpSection(path.join(workspaceRoot, ".windsurf", "rules.md"));
|
|
726
|
+
}
|
|
677
727
|
const alreadyExists = fs.existsSync(configFilePath);
|
|
678
728
|
const content = generateAgentConfig(detector.templateName, {
|
|
679
729
|
workspaceRoot,
|
|
@@ -721,6 +771,13 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
721
771
|
if (wrote)
|
|
722
772
|
mcpConfigs.push("~/.claude.json");
|
|
723
773
|
installClaudeCodeHintHook(workspaceRoot, binaryPath);
|
|
774
|
+
if (interventionMode() === "on") {
|
|
775
|
+
installClaudeCodeReadHint(workspaceRoot, binaryPath);
|
|
776
|
+
installClaudeCodeBashCap(workspaceRoot, binaryPath);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
removeClaudeCodePreToolHooks(workspaceRoot);
|
|
780
|
+
}
|
|
724
781
|
installClaudeCodeStopGate(workspaceRoot, binaryPath);
|
|
725
782
|
installClaudeCodeSessionContext(workspaceRoot, binaryPath);
|
|
726
783
|
if (guardMode() === "strict")
|
|
@@ -761,12 +818,17 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
761
818
|
if (wrote)
|
|
762
819
|
mcpConfigs.push(wrote);
|
|
763
820
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
821
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
|
|
764
822
|
if (guardMode() === "strict")
|
|
765
823
|
installOpencodePlugin(workspaceRoot);
|
|
766
824
|
else
|
|
767
825
|
uninstallOpencodePlugin(workspaceRoot);
|
|
768
826
|
}
|
|
769
827
|
if (detector.agent === "Cursor") {
|
|
828
|
+
if (interventionMode() === "on") {
|
|
829
|
+
installCursorReadHint(workspaceRoot, binaryPath);
|
|
830
|
+
installCursorBashCap(workspaceRoot, binaryPath);
|
|
831
|
+
}
|
|
770
832
|
if (guardMode() === "strict")
|
|
771
833
|
installCursorHook(workspaceRoot);
|
|
772
834
|
else
|
|
@@ -781,6 +843,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
|
|
|
781
843
|
if (wrote)
|
|
782
844
|
mcpConfigs.push(wrote);
|
|
783
845
|
installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
846
|
+
installOpencodeCompressPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
|
|
784
847
|
if (guardMode() === "strict")
|
|
785
848
|
installKiloPlugin(workspaceRoot);
|
|
786
849
|
else
|
|
@@ -907,6 +970,70 @@ export function readJsonConfigSafe(filePath) {
|
|
|
907
970
|
* {command: {path, args}} Zed
|
|
908
971
|
* {command: ["node", "…/mcp-server.cjs"]} Kilo Code
|
|
909
972
|
*/
|
|
973
|
+
/**
|
|
974
|
+
* The interpreter to write into an editor's MCP config for `mcp-server.cjs`.
|
|
975
|
+
*
|
|
976
|
+
* A bare `node` is resolved through the PATH of the process that spawns the
|
|
977
|
+
* server — and a VS Code / Cursor / Windsurf launched from the Dock, the Start
|
|
978
|
+
* menu or a desktop shortcut carries the login PATH, which on most developer
|
|
979
|
+
* machines has no nvm, Volta or Homebrew node in it. The server then dies with
|
|
980
|
+
* `spawn node ENOENT`, the editor lists no vexp tools, and nothing on our side
|
|
981
|
+
* logged anything (GitHub Copilot field report, 2026-08). Pin the node that is
|
|
982
|
+
* running us instead; the VS Code extension has done the same
|
|
983
|
+
* (`resolveNodePath`) since 2.x. Same search order, bare `node` as last resort.
|
|
984
|
+
*/
|
|
985
|
+
export function resolveNodeCommand() {
|
|
986
|
+
const execPath = process.execPath;
|
|
987
|
+
if (execPath && !/Code|Electron/i.test(execPath)) {
|
|
988
|
+
try {
|
|
989
|
+
fs.accessSync(execPath, fs.constants.X_OK);
|
|
990
|
+
return execPath;
|
|
991
|
+
}
|
|
992
|
+
catch { /* not executable */ }
|
|
993
|
+
}
|
|
994
|
+
if (process.platform !== "win32" && fs.existsSync("/usr/local/bin/node"))
|
|
995
|
+
return "/usr/local/bin/node";
|
|
996
|
+
const home = os.homedir();
|
|
997
|
+
const nvmDir = path.join(home, ".nvm", "versions", "node");
|
|
998
|
+
if (fs.existsSync(nvmDir)) {
|
|
999
|
+
try {
|
|
1000
|
+
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith("v")).sort().reverse();
|
|
1001
|
+
for (const v of versions) {
|
|
1002
|
+
const candidate = path.join(nvmDir, v, "bin", "node");
|
|
1003
|
+
if (fs.existsSync(candidate))
|
|
1004
|
+
return candidate;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
catch { /* ignore */ }
|
|
1008
|
+
}
|
|
1009
|
+
return "node";
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* An entry whose interpreter is a bare name (`node`, `node.exe`) rather than a
|
|
1013
|
+
* path. Such an entry may well work from a terminal and still fail inside a
|
|
1014
|
+
* GUI editor, so it is never adopted as "already working" — the writer replaces
|
|
1015
|
+
* it with the pinned interpreter on the next setup.
|
|
1016
|
+
*/
|
|
1017
|
+
export function isBareInterpreter(entry) {
|
|
1018
|
+
if (!entry || typeof entry !== "object")
|
|
1019
|
+
return false;
|
|
1020
|
+
const e = entry;
|
|
1021
|
+
let command = e.command;
|
|
1022
|
+
if (Array.isArray(command))
|
|
1023
|
+
command = command[0];
|
|
1024
|
+
else if (command !== null && typeof command === "object")
|
|
1025
|
+
command = command.path;
|
|
1026
|
+
return typeof command === "string" && command.length > 0 && !/[\\/]/.test(command);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Adopt a foreign entry only when it resolves AND pins its interpreter. Used by
|
|
1030
|
+
* the GUI-editor writers (VS Code/Copilot, Cursor/Windsurf/Kiro/Trae, Zed),
|
|
1031
|
+
* where a bare `node` is exactly the entry that fails; terminal agents
|
|
1032
|
+
* (opencode, Claude Code) keep adopting a bare interpreter — it works there.
|
|
1033
|
+
*/
|
|
1034
|
+
function adoptableEntry(entry) {
|
|
1035
|
+
return vexpEntryStillResolves(entry) && !isBareInterpreter(entry);
|
|
1036
|
+
}
|
|
910
1037
|
function vexpEntryStillResolves(entry) {
|
|
911
1038
|
if (!entry || typeof entry !== "object")
|
|
912
1039
|
return false;
|
|
@@ -1010,13 +1137,110 @@ function warnUnparseable(filePath) {
|
|
|
1010
1137
|
}
|
|
1011
1138
|
process.stderr.write(` [!] ${filePath} could not be parsed - leaving it untouched. Fix the file or add vexp manually, then re-run setup.\n`);
|
|
1012
1139
|
}
|
|
1013
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* Remove a vexp section from a file we no longer write to, and delete the file
|
|
1142
|
+
* when nothing of the user's is left in it.
|
|
1143
|
+
*
|
|
1144
|
+
* Needed because vexp wrote its Windsurf rules to `.windsurf/rules.md` for
|
|
1145
|
+
* several releases. Cascade reads `.windsurf/rules/*.md` (a directory),
|
|
1146
|
+
* `.devin/rules/*.md`, or the legacy root `.windsurfrules` — never that path.
|
|
1147
|
+
* Leaving the old file behind would put a stale, unread copy of our
|
|
1148
|
+
* instructions next to the live one.
|
|
1149
|
+
*/
|
|
1150
|
+
export function removeVexpSection(filePath) {
|
|
1151
|
+
if (!fs.existsSync(filePath))
|
|
1152
|
+
return "absent";
|
|
1153
|
+
let existing;
|
|
1154
|
+
try {
|
|
1155
|
+
existing = fs.readFileSync(filePath, "utf-8");
|
|
1156
|
+
}
|
|
1157
|
+
catch {
|
|
1158
|
+
return "absent";
|
|
1159
|
+
}
|
|
1160
|
+
const marker = existing.match(VEXP_MARKER_RE);
|
|
1161
|
+
const endIdx = existing.indexOf(VEXP_MARKER_END);
|
|
1162
|
+
if (!marker || endIdx === -1)
|
|
1163
|
+
return "absent";
|
|
1164
|
+
const markerIdx = existing.indexOf(marker[0]);
|
|
1165
|
+
let startIdx = markerIdx;
|
|
1166
|
+
while (startIdx > 0 && existing[startIdx - 1] !== "\n")
|
|
1167
|
+
startIdx--;
|
|
1168
|
+
const rest = (existing.slice(0, startIdx) + existing.slice(endIdx + VEXP_MARKER_END.length)).trim();
|
|
1169
|
+
if (rest === "") {
|
|
1170
|
+
try {
|
|
1171
|
+
fs.unlinkSync(filePath);
|
|
1172
|
+
return "deleted";
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
return "absent";
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
fs.writeFileSync(filePath, rest + "\n", "utf-8");
|
|
1180
|
+
return "stripped";
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
return "absent";
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Where the Cursor rule goes, after clearing the way.
|
|
1188
|
+
*
|
|
1189
|
+
* Cursor reads `.cursor/rules/` as a directory of `.mdc` rules. vexp wrote a
|
|
1190
|
+
* single FILE at `.cursor/rules` for several releases — read by nothing, and
|
|
1191
|
+
* a file where Cursor wants a folder, so Cursor could not create its own
|
|
1192
|
+
* rules either. On a project where the folder already existed, `vexp setup`
|
|
1193
|
+
* then crashed with EISDIR reading the directory (field report, Windows /
|
|
1194
|
+
* Cursor, 2026-08) after indexing and the MCP start had already succeeded.
|
|
1195
|
+
*
|
|
1196
|
+
* Our stale file is removed when it holds only our section. If a user put
|
|
1197
|
+
* their own text in a `.cursor/rules` FILE we keep it and fall back to the
|
|
1198
|
+
* legacy root `.cursorrules`, which Cursor still reads — the folder cannot
|
|
1199
|
+
* exist while that file does.
|
|
1200
|
+
*/
|
|
1201
|
+
export function cursorRulesTarget(workspaceRoot) {
|
|
1202
|
+
const legacy = path.join(workspaceRoot, ".cursor", "rules");
|
|
1203
|
+
let isFile = false;
|
|
1204
|
+
try {
|
|
1205
|
+
isFile = fs.statSync(legacy).isFile();
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
/* absent, or a directory */
|
|
1209
|
+
}
|
|
1210
|
+
if (isFile) {
|
|
1211
|
+
removeVexpSection(legacy);
|
|
1212
|
+
if (fs.existsSync(legacy))
|
|
1213
|
+
return ".cursorrules";
|
|
1214
|
+
}
|
|
1215
|
+
return path.join(".cursor", "rules", "vexp.mdc");
|
|
1216
|
+
}
|
|
1217
|
+
/** The detector with its Cursor target resolved for THIS workspace. */
|
|
1218
|
+
function withCursorTarget(d, workspaceRoot) {
|
|
1219
|
+
return d.agent === "Cursor" ? { ...d, configFile: cursorRulesTarget(workspaceRoot) } : d;
|
|
1220
|
+
}
|
|
1221
|
+
export function appendOrCreate(filePath, content, version) {
|
|
1222
|
+
// A directory where a file was expected. Reading it is EISDIR, and that
|
|
1223
|
+
// aborted the whole setup once (the `.cursor/rules` folder above). The
|
|
1224
|
+
// detectors no longer point at folders; if one ever does again, write a
|
|
1225
|
+
// file inside it rather than crash on the user's machine.
|
|
1226
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
|
1227
|
+
filePath = path.join(filePath, "vexp.md");
|
|
1228
|
+
}
|
|
1014
1229
|
if (!fs.existsSync(filePath)) {
|
|
1015
1230
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1016
1231
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
1017
1232
|
return "created";
|
|
1018
1233
|
}
|
|
1019
|
-
|
|
1234
|
+
let existing = fs.readFileSync(filePath, "utf-8");
|
|
1235
|
+
// Front matter has to stay the FIRST thing in the file, and the splice below
|
|
1236
|
+
// preserves whatever sits above our marker — so a rewrite would leave the
|
|
1237
|
+
// old block above and insert a second one. One rule file, one front matter.
|
|
1238
|
+
if (content.startsWith("---\n")) {
|
|
1239
|
+
const lead = existing.match(/^---\n[\s\S]*?\n---\n/);
|
|
1240
|
+
if (lead) {
|
|
1241
|
+
existing = existing.slice(lead[0].length).replace(/^\s*\n/, "");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1020
1244
|
const markerMatch = existing.match(VEXP_MARKER_RE);
|
|
1021
1245
|
if (!markerMatch) {
|
|
1022
1246
|
fs.appendFileSync(filePath, "\n\n" + content);
|
|
@@ -1120,7 +1344,7 @@ approveKey = "alwaysAllow") {
|
|
|
1120
1344
|
}
|
|
1121
1345
|
const existing = read.data;
|
|
1122
1346
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1123
|
-
const targetCmd = useNode ?
|
|
1347
|
+
const targetCmd = useNode ? resolveNodeCommand() : binaryPath;
|
|
1124
1348
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1125
1349
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
1126
1350
|
const beforeServers = existing.mcpServers;
|
|
@@ -1144,7 +1368,7 @@ approveKey = "alwaysAllow") {
|
|
|
1144
1368
|
// Another vexp install already left a working entry here — don't fight it.
|
|
1145
1369
|
// Only the install path may differ: `envMatches` still gates on the
|
|
1146
1370
|
// VEXP_WORKSPACE pin, so a moved or renamed project is repinned normally.
|
|
1147
|
-
if (removed.length === 0 && envMatches &&
|
|
1371
|
+
if (removed.length === 0 && envMatches && adoptableEntry(previousVexp))
|
|
1148
1372
|
return false;
|
|
1149
1373
|
const servers = existing.mcpServers ?? {};
|
|
1150
1374
|
servers["vexp"] = {
|
|
@@ -1290,7 +1514,15 @@ function buildCodexSection(opts) {
|
|
|
1290
1514
|
const wsHash = opts.workspaceRoot ? workspaceHash(opts.workspaceRoot) : "";
|
|
1291
1515
|
const urlPath = wsHash ? `/ws/${wsHash}/mcp` : "/mcp";
|
|
1292
1516
|
const desiredUrl = `http://127.0.0.1:${opts.mcpPort}${urlPath}`;
|
|
1293
|
-
return
|
|
1517
|
+
return `
|
|
1518
|
+
[mcp_servers.vexp]
|
|
1519
|
+
${CODEX_MANAGED_MARKER}: http transport (set VEXP_CODEX_TRANSPORT=direct for stdio)
|
|
1520
|
+
url = "${desiredUrl}"
|
|
1521
|
+
tool_timeout_sec = 120
|
|
1522
|
+
|
|
1523
|
+
[mcp_servers.vexp.http_headers]
|
|
1524
|
+
Authorization = "Bearer ${opts.token}"
|
|
1525
|
+
`;
|
|
1294
1526
|
}
|
|
1295
1527
|
/**
|
|
1296
1528
|
* Configure MCP in ~/.codex/config.toml (global).
|
|
@@ -1378,7 +1610,7 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
|
|
|
1378
1610
|
}
|
|
1379
1611
|
const existing = read.data;
|
|
1380
1612
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1381
|
-
const targetCmd = useNode ?
|
|
1613
|
+
const targetCmd = useNode ? resolveNodeCommand() : binaryPath;
|
|
1382
1614
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1383
1615
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
1384
1616
|
const beforeServers = existing.servers;
|
|
@@ -1398,7 +1630,7 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
|
|
|
1398
1630
|
// Copilot branch, which is gated only on `.github/` existing. Only the
|
|
1399
1631
|
// install path may differ — `envMatches` still gates on the VEXP_WORKSPACE
|
|
1400
1632
|
// pin, so a moved or renamed project is repinned normally.
|
|
1401
|
-
if (removed.length === 0 && envMatches &&
|
|
1633
|
+
if (removed.length === 0 && envMatches && adoptableEntry(previousVexp))
|
|
1402
1634
|
return false;
|
|
1403
1635
|
const servers = existing.servers ?? {};
|
|
1404
1636
|
servers["vexp"] = {
|
|
@@ -1536,7 +1768,7 @@ export function configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
|
1536
1768
|
}
|
|
1537
1769
|
const cfg = read.data;
|
|
1538
1770
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1539
|
-
const command = useNode ? [
|
|
1771
|
+
const command = useNode ? [resolveNodeCommand(), mcpServerPath] : [binaryPath, "mcp"];
|
|
1540
1772
|
const env = { VEXP_WORKSPACE: workspaceRoot };
|
|
1541
1773
|
const mcp = cfg.mcp ?? {};
|
|
1542
1774
|
const prev = mcp["vexp"];
|
|
@@ -1577,7 +1809,7 @@ export function configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath) {
|
|
|
1577
1809
|
}
|
|
1578
1810
|
const cfg = read.data;
|
|
1579
1811
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1580
|
-
const command = useNode ? [
|
|
1812
|
+
const command = useNode ? [resolveNodeCommand(), mcpServerPath] : [binaryPath, "mcp"];
|
|
1581
1813
|
const env = { VEXP_WORKSPACE: workspaceRoot };
|
|
1582
1814
|
const mcp = cfg.mcp ?? {};
|
|
1583
1815
|
const prev = mcp["vexp"];
|
|
@@ -1611,7 +1843,7 @@ export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
1611
1843
|
}
|
|
1612
1844
|
const settings = read.data;
|
|
1613
1845
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1614
|
-
const targetCmd = useNode ?
|
|
1846
|
+
const targetCmd = useNode ? resolveNodeCommand() : binaryPath;
|
|
1615
1847
|
const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1616
1848
|
const targetEnv = workspaceRoot ? { VEXP_WORKSPACE: workspaceRoot } : undefined;
|
|
1617
1849
|
const beforeCs = settings.context_servers;
|
|
@@ -1630,7 +1862,7 @@ export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
|
|
|
1630
1862
|
// Another vexp install already left a working entry here — don't fight it.
|
|
1631
1863
|
// Only the install path may differ: `envMatches` still gates on the
|
|
1632
1864
|
// VEXP_WORKSPACE pin, so a moved or renamed project is repinned normally.
|
|
1633
|
-
if (removed.length === 0 && envMatches &&
|
|
1865
|
+
if (removed.length === 0 && envMatches && adoptableEntry(previousVexp))
|
|
1634
1866
|
return false;
|
|
1635
1867
|
const cs = settings.context_servers ?? {};
|
|
1636
1868
|
cs["vexp"] = {
|
|
@@ -1673,7 +1905,7 @@ export function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRo
|
|
|
1673
1905
|
const beforeServers = config.mcpServers;
|
|
1674
1906
|
const existing = beforeServers?.["vexp"];
|
|
1675
1907
|
const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
|
|
1676
|
-
const desiredCommand = useNode ?
|
|
1908
|
+
const desiredCommand = useNode ? resolveNodeCommand() : binaryPath;
|
|
1677
1909
|
const desiredArgs = useNode ? [mcpServerPath] : ["mcp"];
|
|
1678
1910
|
// Multi-session fix: do NOT pin VEXP_WORKSPACE on this user-scope entry.
|
|
1679
1911
|
// Claude Code applies ~/.claude.json mcpServers to EVERY project, so a pinned
|
|
@@ -1965,11 +2197,27 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
|
|
|
1965
2197
|
const hookPath = path.join(hookDir, "vexp-verify.sh");
|
|
1966
2198
|
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
1967
2199
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
1968
|
-
|
|
2200
|
+
// Opt-in since the measurement: the gate bought 15 to 32 extra turns per 13
|
|
2201
|
+
// bench sessions, and they land at the END of a session where the
|
|
2202
|
+
// transcript is heaviest, so one of them costs more than a session's entire
|
|
2203
|
+
// tool output. See InterventionMode above. When off, a previously installed
|
|
2204
|
+
// gate is removed and de-registered so upgrades converge.
|
|
2205
|
+
const gateOn = interventionMode() === "on";
|
|
1969
2206
|
const existed = fs.existsSync(hookPath);
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
2207
|
+
const script = vexpStopGateHookScript(binaryPath);
|
|
2208
|
+
const scriptIdentical = gateOn && existed && fs.readFileSync(hookPath, "utf8") === script;
|
|
2209
|
+
if (gateOn) {
|
|
2210
|
+
if (!scriptIdentical) {
|
|
2211
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
else if (existed) {
|
|
2215
|
+
try {
|
|
2216
|
+
fs.unlinkSync(hookPath);
|
|
2217
|
+
}
|
|
2218
|
+
catch {
|
|
2219
|
+
/* a hook we cannot remove is still de-registered below */
|
|
2220
|
+
}
|
|
1973
2221
|
}
|
|
1974
2222
|
const read = readJsonConfigSafe(settingsPath);
|
|
1975
2223
|
if (!read.ok) {
|
|
@@ -1981,16 +2229,20 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
|
|
|
1981
2229
|
const existing = Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
1982
2230
|
const filtered = existing.filter((h) => !isVexpHookEntry(h, "vexp-verify"));
|
|
1983
2231
|
// Cross-OS shape as everywhere: bash + QUOTED path; timeout in SECONDS.
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2232
|
+
if (gateOn) {
|
|
2233
|
+
filtered.push({
|
|
2234
|
+
hooks: [
|
|
2235
|
+
{
|
|
2236
|
+
type: "command",
|
|
2237
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-verify.sh"',
|
|
2238
|
+
timeout: 15,
|
|
2239
|
+
},
|
|
2240
|
+
],
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
1993
2243
|
const merged = { ...hooks, Stop: filtered };
|
|
2244
|
+
if (filtered.length === 0)
|
|
2245
|
+
delete merged.Stop;
|
|
1994
2246
|
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
1995
2247
|
if (scriptIdentical && settingsIdentical)
|
|
1996
2248
|
return null;
|
|
@@ -2002,6 +2254,10 @@ export function installClaudeCodeStopGate(workspaceRoot, binaryPath) {
|
|
|
2002
2254
|
}
|
|
2003
2255
|
return existed ? "updated" : "created";
|
|
2004
2256
|
}
|
|
2257
|
+
function isVexpEditHintHookEntry(entry) {
|
|
2258
|
+
const cmds = JSON.stringify(entry ?? "");
|
|
2259
|
+
return cmds.includes("vexp-edit-hint");
|
|
2260
|
+
}
|
|
2005
2261
|
function isVexpHintHookEntry(h) {
|
|
2006
2262
|
if (!h || typeof h !== "object")
|
|
2007
2263
|
return false;
|
|
@@ -2046,6 +2302,99 @@ function isVexpSearchHookEntry(h) {
|
|
|
2046
2302
|
* Why a hook rather than a tool: 9 of 227 measured agent sessions called the
|
|
2047
2303
|
* MCP tools. All 227 called Bash.
|
|
2048
2304
|
*/
|
|
2305
|
+
/**
|
|
2306
|
+
* Install one of the compression hooks into Claude Code's PreToolUse.
|
|
2307
|
+
*
|
|
2308
|
+
* Shared because the two are one mechanism seen from two sides. Bounding reads
|
|
2309
|
+
* alone did nothing measurable: Read-tool tokens fell 54% and shell output rose
|
|
2310
|
+
* 39%, and the total sat inside the band between two identical stock runs. An
|
|
2311
|
+
* agent takes what it needs through whichever door is open.
|
|
2312
|
+
*/
|
|
2313
|
+
/**
|
|
2314
|
+
* Undo the two compression hooks, so a workspace configured by an older
|
|
2315
|
+
* version converges on the next setup instead of quietly keeping a default
|
|
2316
|
+
* that was measured and withdrawn.
|
|
2317
|
+
*
|
|
2318
|
+
* Same contract as the guard's removal: take the files out AND de-register
|
|
2319
|
+
* them. A hook left in settings.json pointing at a script that no longer
|
|
2320
|
+
* exists is worse than either state on its own — Claude Code runs it, it
|
|
2321
|
+
* fails, and every Read or Bash call carries the failure.
|
|
2322
|
+
*/
|
|
2323
|
+
function removeClaudeCodePreToolHooks(workspaceRoot) {
|
|
2324
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2325
|
+
for (const name of ["read-hint", "bash-cap"]) {
|
|
2326
|
+
const p = path.join(hookDir, `vexp-${name}.sh`);
|
|
2327
|
+
if (fs.existsSync(p)) {
|
|
2328
|
+
try {
|
|
2329
|
+
fs.unlinkSync(p);
|
|
2330
|
+
}
|
|
2331
|
+
catch {
|
|
2332
|
+
/* still de-registered below */
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2337
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
2338
|
+
if (!read.ok || !read.existed)
|
|
2339
|
+
return;
|
|
2340
|
+
const settings = read.data;
|
|
2341
|
+
const hooks = (settings.hooks ?? {});
|
|
2342
|
+
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
2343
|
+
const filtered = existing.filter((h) => !/vexp-(read-hint|bash-cap)/.test(JSON.stringify(h)));
|
|
2344
|
+
if (filtered.length === existing.length)
|
|
2345
|
+
return;
|
|
2346
|
+
const merged = { ...hooks, PreToolUse: filtered };
|
|
2347
|
+
if (filtered.length === 0)
|
|
2348
|
+
delete merged.PreToolUse;
|
|
2349
|
+
settings.hooks = merged;
|
|
2350
|
+
backupConfig(settingsPath);
|
|
2351
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2352
|
+
}
|
|
2353
|
+
function installClaudeCodePreToolHook(workspaceRoot, binaryPath, name, matcher, script) {
|
|
2354
|
+
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2355
|
+
const hookPath = path.join(hookDir, `vexp-${name}.sh`);
|
|
2356
|
+
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2357
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
2358
|
+
const existed = fs.existsSync(hookPath);
|
|
2359
|
+
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2360
|
+
if (!scriptIdentical)
|
|
2361
|
+
fs.writeFileSync(hookPath, script, { mode: 0o755 });
|
|
2362
|
+
const read = readJsonConfigSafe(settingsPath);
|
|
2363
|
+
if (!read.ok) {
|
|
2364
|
+
warnUnparseable(settingsPath);
|
|
2365
|
+
return scriptIdentical ? null : existed ? "updated" : "created";
|
|
2366
|
+
}
|
|
2367
|
+
const settings = read.data;
|
|
2368
|
+
const hooks = (settings.hooks ?? {});
|
|
2369
|
+
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
2370
|
+
const filtered = existing.filter((h) => !JSON.stringify(h).includes(`vexp-${name}`));
|
|
2371
|
+
// Shell form with a quoted path: exec form cannot run a .sh on Windows, and
|
|
2372
|
+
// an unquoted path word-splits on "C:\\Program Files" (c4a0b9e).
|
|
2373
|
+
filtered.push({
|
|
2374
|
+
matcher,
|
|
2375
|
+
hooks: [
|
|
2376
|
+
{
|
|
2377
|
+
type: "command",
|
|
2378
|
+
command: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-${name}.sh"`,
|
|
2379
|
+
timeout: 10,
|
|
2380
|
+
},
|
|
2381
|
+
],
|
|
2382
|
+
});
|
|
2383
|
+
const merged = { ...hooks, PreToolUse: filtered };
|
|
2384
|
+
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
2385
|
+
if (scriptIdentical && settingsIdentical)
|
|
2386
|
+
return null;
|
|
2387
|
+
settings.hooks = merged;
|
|
2388
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
2389
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2390
|
+
return existed ? "updated" : "created";
|
|
2391
|
+
}
|
|
2392
|
+
export function installClaudeCodeReadHint(workspaceRoot, binaryPath) {
|
|
2393
|
+
return installClaudeCodePreToolHook(workspaceRoot, binaryPath, "read-hint", "Read", bakeReadHintHook(binaryPath));
|
|
2394
|
+
}
|
|
2395
|
+
export function installClaudeCodeBashCap(workspaceRoot, binaryPath) {
|
|
2396
|
+
return installClaudeCodePreToolHook(workspaceRoot, binaryPath, "bash-cap", "Bash", bakeBashCapHook(binaryPath));
|
|
2397
|
+
}
|
|
2049
2398
|
export function installClaudeCodeSearchHook(workspaceRoot, binaryPath) {
|
|
2050
2399
|
const hookDir = path.join(workspaceRoot, ".claude", "hooks");
|
|
2051
2400
|
const hookPath = path.join(hookDir, "vexp-search.sh");
|
|
@@ -2096,6 +2445,34 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
|
2096
2445
|
const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
|
|
2097
2446
|
const script = vexpHintHookScript(binaryPath);
|
|
2098
2447
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
2448
|
+
// v5: the edit-time coupling hook, written beside the prompt-time one -
|
|
2449
|
+
// but only when interventions are on. It is one of the two mechanisms that
|
|
2450
|
+
// ask the agent to go do something, and a turn costs 65,521 tokens. See
|
|
2451
|
+
// InterventionMode above for the measurement.
|
|
2452
|
+
const editHookPath = path.join(hookDir, "vexp-edit-hint.sh");
|
|
2453
|
+
const couplingOn = interventionMode() === "on";
|
|
2454
|
+
if (couplingOn) {
|
|
2455
|
+
const editScript = bakeEditHintHook(binaryPath);
|
|
2456
|
+
const editExisted = fs.existsSync(editHookPath);
|
|
2457
|
+
if (!editExisted || fs.readFileSync(editHookPath, "utf-8") !== editScript) {
|
|
2458
|
+
fs.writeFileSync(editHookPath, editScript, { mode: 0o755 });
|
|
2459
|
+
}
|
|
2460
|
+
try {
|
|
2461
|
+
fs.chmodSync(editHookPath, 0o755);
|
|
2462
|
+
}
|
|
2463
|
+
catch {
|
|
2464
|
+
/* Windows has no exec bit; Git Bash runs it anyway. */
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
else if (fs.existsSync(editHookPath)) {
|
|
2468
|
+
// Converge an upgraded workspace without asking anyone to clean up.
|
|
2469
|
+
try {
|
|
2470
|
+
fs.unlinkSync(editHookPath);
|
|
2471
|
+
}
|
|
2472
|
+
catch {
|
|
2473
|
+
/* a hook we cannot remove is still de-registered below */
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2099
2476
|
const existed = fs.existsSync(hookPath);
|
|
2100
2477
|
const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
|
|
2101
2478
|
if (!scriptIdentical) {
|
|
@@ -2123,7 +2500,35 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
|
2123
2500
|
},
|
|
2124
2501
|
],
|
|
2125
2502
|
});
|
|
2126
|
-
|
|
2503
|
+
// v5: the coupling on the edit that needs it. PostToolUse rather than
|
|
2504
|
+
// PreToolUse because a deny on an edit stops real work, and this must never
|
|
2505
|
+
// block — it adds no turn and no permission decision, only a line the model
|
|
2506
|
+
// reads as a system-reminder.
|
|
2507
|
+
const existingPost = Array.isArray(hooks.PostToolUse)
|
|
2508
|
+
? hooks.PostToolUse
|
|
2509
|
+
: [];
|
|
2510
|
+
const filteredPost = existingPost.filter((h) => !isVexpEditHintHookEntry(h));
|
|
2511
|
+
if (couplingOn) {
|
|
2512
|
+
filteredPost.push({
|
|
2513
|
+
matcher: "Edit|Write|NotebookEdit",
|
|
2514
|
+
hooks: [
|
|
2515
|
+
{
|
|
2516
|
+
type: "command",
|
|
2517
|
+
command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-edit-hint.sh"',
|
|
2518
|
+
timeout: 5,
|
|
2519
|
+
},
|
|
2520
|
+
],
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
const merged = {
|
|
2524
|
+
...hooks,
|
|
2525
|
+
UserPromptSubmit: filtered,
|
|
2526
|
+
PostToolUse: filteredPost,
|
|
2527
|
+
};
|
|
2528
|
+
// An empty array is not "no hook": it leaves a key behind that reads like a
|
|
2529
|
+
// configured-but-broken event. Drop it, as the extension copy does.
|
|
2530
|
+
if (filteredPost.length === 0)
|
|
2531
|
+
delete merged.PostToolUse;
|
|
2127
2532
|
const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
|
|
2128
2533
|
if (scriptIdentical && settingsIdentical)
|
|
2129
2534
|
return null;
|
|
@@ -2140,6 +2545,21 @@ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
|
|
|
2140
2545
|
* unlike the deny guard (strict-only), this cannot break a session; it only
|
|
2141
2546
|
* ever appends an orientation line the daemon judged useful.
|
|
2142
2547
|
*/
|
|
2548
|
+
/**
|
|
2549
|
+
* opencode / Kilo compression plugin: both doors, mutating rather than
|
|
2550
|
+
* blocking. Their `tool.execute.before` can rewrite `output.args`, which is
|
|
2551
|
+
* the whole mechanism.
|
|
2552
|
+
*/
|
|
2553
|
+
export function installOpencodeCompressPlugin(workspaceRoot, binaryPath, pluginDir) {
|
|
2554
|
+
const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-compress.js");
|
|
2555
|
+
const content = vexpOpencodeCompressPlugin(binaryPath);
|
|
2556
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
2557
|
+
const existed = fs.existsSync(pluginPath);
|
|
2558
|
+
if (existed && fs.readFileSync(pluginPath, "utf-8") === content)
|
|
2559
|
+
return null;
|
|
2560
|
+
fs.writeFileSync(pluginPath, content);
|
|
2561
|
+
return existed ? "updated" : "created";
|
|
2562
|
+
}
|
|
2143
2563
|
export function installOpencodeHintPlugin(workspaceRoot, binaryPath, pluginDir) {
|
|
2144
2564
|
const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-hint.js");
|
|
2145
2565
|
const content = vexpOpencodeHintPlugin(binaryPath);
|
|
@@ -2317,6 +2737,58 @@ export function uninstallOpencodePlugin(workspaceRoot) {
|
|
|
2317
2737
|
* `failClosed` is left at its default (false): a crashing guard must let the
|
|
2318
2738
|
* agent work, not lock it out.
|
|
2319
2739
|
*/
|
|
2740
|
+
/**
|
|
2741
|
+
* Register a compression hook with Cursor.
|
|
2742
|
+
*
|
|
2743
|
+
* Cursor's `preToolUse` takes the SAME input as Claude Code's — `tool_name`,
|
|
2744
|
+
* `tool_input` — and answers in a different shape: `permission` plus a
|
|
2745
|
+
* snake_case `updated_input`. The binary emits either on request, so nothing
|
|
2746
|
+
* here has to know the difference beyond passing `--protocol cursor`.
|
|
2747
|
+
*
|
|
2748
|
+
* The binary is registered directly rather than through a shell script: there
|
|
2749
|
+
* is no `$CLAUDE_PROJECT_DIR` to expand here and no bash to assume on Windows.
|
|
2750
|
+
*/
|
|
2751
|
+
function installCursorCompressionHook(workspaceRoot, binaryPath, sub, matcher) {
|
|
2752
|
+
const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
|
|
2753
|
+
const command = `${binaryPath} ${sub} --protocol cursor`;
|
|
2754
|
+
const read = readJsonConfigSafe(cfgPath);
|
|
2755
|
+
if (!read.ok) {
|
|
2756
|
+
warnUnparseable(cfgPath);
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
const cfg = read.data;
|
|
2760
|
+
const hooks = cfg.hooks ?? {};
|
|
2761
|
+
const preToolUse = Array.isArray(hooks.preToolUse) ? [...hooks.preToolUse] : [];
|
|
2762
|
+
// Idempotent by SUBCOMMAND, not by exact command: the binary path changes
|
|
2763
|
+
// between installs and must replace its own entry rather than add one.
|
|
2764
|
+
const mine = (e) => !!e &&
|
|
2765
|
+
typeof e === "object" &&
|
|
2766
|
+
typeof e.command === "string" &&
|
|
2767
|
+
(e.command).includes(` ${sub} --protocol cursor`);
|
|
2768
|
+
const entry = { command, matcher };
|
|
2769
|
+
const at = preToolUse.findIndex(mine);
|
|
2770
|
+
if (at >= 0 && JSON.stringify(preToolUse[at]) === JSON.stringify(entry))
|
|
2771
|
+
return false;
|
|
2772
|
+
if (at >= 0)
|
|
2773
|
+
preToolUse[at] = entry;
|
|
2774
|
+
else
|
|
2775
|
+
preToolUse.push(entry);
|
|
2776
|
+
hooks.preToolUse = preToolUse;
|
|
2777
|
+
cfg.hooks = hooks;
|
|
2778
|
+
if (cfg.version === undefined)
|
|
2779
|
+
cfg.version = 1;
|
|
2780
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
2781
|
+
if (read.existed)
|
|
2782
|
+
backupConfig(cfgPath);
|
|
2783
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
2784
|
+
return true;
|
|
2785
|
+
}
|
|
2786
|
+
export function installCursorReadHint(workspaceRoot, binaryPath) {
|
|
2787
|
+
return installCursorCompressionHook(workspaceRoot, binaryPath, "read-hint", "Read");
|
|
2788
|
+
}
|
|
2789
|
+
export function installCursorBashCap(workspaceRoot, binaryPath) {
|
|
2790
|
+
return installCursorCompressionHook(workspaceRoot, binaryPath, "bash-cap", "Bash");
|
|
2791
|
+
}
|
|
2320
2792
|
export function installCursorHook(workspaceRoot) {
|
|
2321
2793
|
const rel = path.join(".cursor", "hooks", "vexp-guard.js");
|
|
2322
2794
|
const hookPath = path.join(workspaceRoot, rel);
|
|
@@ -2424,6 +2896,58 @@ export function uninstallKiloPlugin(workspaceRoot) {
|
|
|
2424
2896
|
}
|
|
2425
2897
|
return removed;
|
|
2426
2898
|
}
|
|
2899
|
+
/**
|
|
2900
|
+
* v5: the edit-time coupling for Cursor.
|
|
2901
|
+
*
|
|
2902
|
+
* Cursor sees a prompt but cannot inject at prompt time — `beforeSubmitPrompt`
|
|
2903
|
+
* is read-only. Its injectable channel is `postToolUse`, which is exactly
|
|
2904
|
+
* where this signal belongs anyway: the file has just been edited and the
|
|
2905
|
+
* coupling is what the plan is about to miss.
|
|
2906
|
+
*
|
|
2907
|
+
* Same binary, same daemon op, same fail-open contract as the Claude Code
|
|
2908
|
+
* hook. The point of doing both is that a signal only one agent receives is a
|
|
2909
|
+
* feature of that agent, not of the product.
|
|
2910
|
+
*/
|
|
2911
|
+
export function installCursorEditHint(workspaceRoot, binaryPath) {
|
|
2912
|
+
const dir = path.join(workspaceRoot, ".cursor", "hooks");
|
|
2913
|
+
const scriptPath = path.join(dir, "vexp-edit-hint.sh");
|
|
2914
|
+
const script = bakeEditHintHook(binaryPath);
|
|
2915
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2916
|
+
const existed = fs.existsSync(scriptPath);
|
|
2917
|
+
if (!existed || fs.readFileSync(scriptPath, "utf-8") !== script) {
|
|
2918
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
2919
|
+
}
|
|
2920
|
+
try {
|
|
2921
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
2922
|
+
}
|
|
2923
|
+
catch {
|
|
2924
|
+
/* no exec bit on Windows */
|
|
2925
|
+
}
|
|
2926
|
+
const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
|
|
2927
|
+
const read = readJsonConfigSafe(cfgPath);
|
|
2928
|
+
if (!read.ok) {
|
|
2929
|
+
warnUnparseable(cfgPath);
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
const cfg = read.data;
|
|
2933
|
+
const hooks = cfg.hooks ?? {};
|
|
2934
|
+
const post = Array.isArray(hooks.postToolUse) ? hooks.postToolUse : [];
|
|
2935
|
+
const filtered = post.filter((e) => {
|
|
2936
|
+
if (!e || typeof e !== "object")
|
|
2937
|
+
return true;
|
|
2938
|
+
const cmd = e.command;
|
|
2939
|
+
return typeof cmd !== "string" || !cmd.includes("vexp-edit-hint");
|
|
2940
|
+
});
|
|
2941
|
+
filtered.push({ command: `bash "${scriptPath}"` });
|
|
2942
|
+
hooks.postToolUse = filtered;
|
|
2943
|
+
const merged = { ...cfg, hooks };
|
|
2944
|
+
if (JSON.stringify(merged) === JSON.stringify(cfg) && existed)
|
|
2945
|
+
return null;
|
|
2946
|
+
if (read.existed)
|
|
2947
|
+
backupConfig(cfgPath);
|
|
2948
|
+
fs.writeFileSync(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2949
|
+
return read.existed ? "updated" : "created";
|
|
2950
|
+
}
|
|
2427
2951
|
/** Remove the Cursor guard (2.3 A2 default): delete the hook script and drop
|
|
2428
2952
|
* our preToolUse entry from .cursor/hooks.json. */
|
|
2429
2953
|
export function uninstallCursorHook(workspaceRoot) {
|
|
@@ -2472,12 +2996,47 @@ export function uninstallCursorHook(workspaceRoot) {
|
|
|
2472
2996
|
* Shared by every prose template so the wording cannot drift again. ASCII only
|
|
2473
2997
|
* (these land in files that get read on Windows).
|
|
2474
2998
|
*/
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2999
|
+
/**
|
|
3000
|
+
* The standing mandate, and it is deliberately short.
|
|
3001
|
+
*
|
|
3002
|
+
* Whatever this file says is resident in the agent's context on EVERY turn,
|
|
3003
|
+
* while a tool call happens at most once. Measured over 13 full bench
|
|
3004
|
+
* sessions: 37.1 API turns against a transcript averaging 65,521 tokens, so
|
|
3005
|
+
* a line here is billed 37 times and a call is billed once. The templates
|
|
3006
|
+
* this replaces ran 282 tokens (Claude Code) to 959 (generic) - up to 35,000
|
|
3007
|
+
* replayed tokens per session - and most of what they carried was a second
|
|
3008
|
+
* copy of the tool descriptions the MCP schemas already send.
|
|
3009
|
+
*
|
|
3010
|
+
* So this keeps only the three things a schema cannot carry:
|
|
3011
|
+
*
|
|
3012
|
+
* 1. WHEN to call, and when not to - the schema says what a tool returns,
|
|
3013
|
+
* not whether this task wants it.
|
|
3014
|
+
* 2. The two guardrails that stop wasted calls: text sweeps belong to
|
|
3015
|
+
* native search, and the index covers repo source only (logs, dist/,
|
|
3016
|
+
* node_modules are not in it - read those directly).
|
|
3017
|
+
* 3. The privacy line, because users ask and compliance reviews ask.
|
|
3018
|
+
*
|
|
3019
|
+
* Everything else was deleted, not shortened. See the catalog diet in
|
|
3020
|
+
* mcp_stdio.rs for the same argument applied one layer down.
|
|
3021
|
+
*/
|
|
3022
|
+
const MANDATE_CORE = `### Context strategy: call run_pipeline ONCE at task start
|
|
3023
|
+
If the task already names the files/symbols to touch, SKIP vexp. Otherwise one
|
|
3024
|
+
\`run_pipeline({ "task": "..." })\` returns ranked pivot files with line ranges and
|
|
3025
|
+
blast radius. Do NOT open files one by one to find your way around - every extra
|
|
3026
|
+
tool call costs a turn. Call it again ONLY when the task moves to a new area.
|
|
3027
|
+
\`get_skeleton\` for files to understand, not edit. \`verify_done\` before calling a
|
|
3028
|
+
multi-file task complete, then RUN the tests it names.
|
|
3029
|
+
|
|
3030
|
+
### Query shape (do this)
|
|
3031
|
+
Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
3032
|
+
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
3033
|
+
|
|
3034
|
+
vexp runs entirely on this machine, index in \`.vexp/\`;
|
|
3035
|
+
\`run_pipeline\` transmits nothing to any external service.
|
|
3036
|
+
On \`status: "degraded"\` or 0 pivots the index is still building - use your own tools.
|
|
3037
|
+
For literal string sweeps use your native search - do NOT route text sweeps through vexp.
|
|
3038
|
+
Repo SOURCE only: logs, dist/, node_modules/ and files outside the repo are NOT indexed.`;
|
|
3039
|
+
export function generateAgentConfig(template, vars) {
|
|
2481
3040
|
switch (template) {
|
|
2482
3041
|
case "claude-code":
|
|
2483
3042
|
return claudeCodeTemplate(vars);
|
|
@@ -2506,469 +3065,82 @@ function generateAgentConfig(template, vars) {
|
|
|
2506
3065
|
// ---------------------------------------------------------------------------
|
|
2507
3066
|
function claudeCodeTemplate(vars) {
|
|
2508
3067
|
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
3068
|
|
|
2523
|
-
|
|
2524
|
-
Anchor the task on real identifiers (ClassName, functionName) or file paths:
|
|
2525
|
-
\`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
|
|
3069
|
+
${MANDATE_CORE}
|
|
2526
3070
|
<!-- /vexp -->`;
|
|
2527
3071
|
}
|
|
2528
3072
|
function cursorTemplate(vars) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
vexp
|
|
2540
|
-
|
|
2541
|
-
|
|
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.
|
|
3073
|
+
// Cursor project rules are `.cursor/rules/*.mdc` with front matter;
|
|
3074
|
+
// `alwaysApply: true` puts the rule in every chat without the agent having
|
|
3075
|
+
// to pick it. A rule file that Cursor reads at all is new: for several
|
|
3076
|
+
// releases vexp wrote a single FILE at `.cursor/rules`, a path Cursor never
|
|
3077
|
+
// reads, and one that stopped Cursor from creating its rules folder.
|
|
3078
|
+
return `---
|
|
3079
|
+
description: vexp code-graph orientation for this workspace
|
|
3080
|
+
alwaysApply: true
|
|
3081
|
+
---
|
|
3082
|
+
|
|
3083
|
+
## vexp rules for Cursor <!-- vexp v${vars.version} -->
|
|
3084
|
+
|
|
3085
|
+
${MANDATE_CORE}
|
|
2584
3086
|
<!-- /vexp -->`;
|
|
2585
3087
|
}
|
|
2586
3088
|
function windsurfTemplate(vars) {
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
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.
|
|
3089
|
+
// Front matter, because a rule without one is not always on. Cascade puts
|
|
3090
|
+
// the FULL text of an \`always_on\` rule in the system prompt on every
|
|
3091
|
+
// message; the other modes make the agent choose to look, which is the
|
|
3092
|
+
// complaint every Windsurf user brings ("it ignores the index unless I
|
|
3093
|
+
// remind it"). This is the one lever Windsurf gives us and we were not
|
|
3094
|
+
// pulling it.
|
|
3095
|
+
return `---
|
|
3096
|
+
trigger: always_on
|
|
3097
|
+
description: vexp code-graph orientation for this workspace
|
|
3098
|
+
---
|
|
3099
|
+
|
|
3100
|
+
## vexp for Windsurf <!-- vexp v${vars.version} -->
|
|
3101
|
+
|
|
3102
|
+
${MANDATE_CORE}
|
|
2634
3103
|
<!-- /vexp -->`;
|
|
2635
3104
|
}
|
|
2636
3105
|
function continueTemplate(vars) {
|
|
2637
|
-
return
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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);
|
|
3106
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
3107
|
+
|
|
3108
|
+
${MANDATE_CORE}
|
|
3109
|
+
<!-- /vexp -->`;
|
|
2654
3110
|
}
|
|
2655
3111
|
function augmentTemplate(vars) {
|
|
2656
|
-
return `## vexp
|
|
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.
|
|
3112
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2700
3113
|
|
|
2701
|
-
|
|
2702
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3114
|
+
${MANDATE_CORE}
|
|
2703
3115
|
<!-- /vexp -->`;
|
|
2704
3116
|
}
|
|
2705
3117
|
function copilotTemplate(vars) {
|
|
2706
|
-
return `## vexp
|
|
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.
|
|
3118
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2750
3119
|
|
|
2751
|
-
|
|
2752
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3120
|
+
${MANDATE_CORE}
|
|
2753
3121
|
<!-- /vexp -->`;
|
|
2754
3122
|
}
|
|
2755
3123
|
function zedTemplate(vars) {
|
|
2756
|
-
return `## vexp
|
|
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.
|
|
3124
|
+
return `## vexp <!-- vexp v${vars.version} -->
|
|
2800
3125
|
|
|
2801
|
-
|
|
2802
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3126
|
+
${MANDATE_CORE}
|
|
2803
3127
|
<!-- /vexp -->`;
|
|
2804
3128
|
}
|
|
2805
3129
|
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.
|
|
3130
|
+
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
2849
3131
|
|
|
2850
|
-
|
|
2851
|
-
\`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
|
|
3132
|
+
${MANDATE_CORE}
|
|
2852
3133
|
<!-- /vexp -->`;
|
|
2853
3134
|
}
|
|
2854
3135
|
function kiroTemplate(vars) {
|
|
2855
3136
|
return `# vexp steering <!-- vexp v${vars.version} -->
|
|
2856
3137
|
|
|
2857
|
-
|
|
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.
|
|
3138
|
+
${MANDATE_CORE}
|
|
2902
3139
|
<!-- /vexp -->`;
|
|
2903
3140
|
}
|
|
2904
3141
|
function genericTemplate(vars) {
|
|
2905
3142
|
return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
|
|
2906
3143
|
|
|
2907
|
-
|
|
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.
|
|
3144
|
+
${MANDATE_CORE}
|
|
2973
3145
|
<!-- /vexp -->`;
|
|
2974
3146
|
}
|