vexp-cli 2.2.1 → 2.2.3

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 } from "./hook-template.js";
13
+ import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD } from "./hook-template.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // Constants
16
16
  // ---------------------------------------------------------------------------
@@ -345,10 +345,47 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
345
345
  const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
346
346
  if (wrote)
347
347
  mcpConfigs.push(wrote);
348
+ // Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
349
+ // markdown alone was demonstrably not enough (a reported session loaded
350
+ // vexp.md, quoted it back, and still read five files by hand).
351
+ const kilo = installKiloPlugin(workspaceRoot);
352
+ if (kilo.plugin) {
353
+ results.push({
354
+ agent: "Kilo Code Guard",
355
+ configFile: path.join(".kilo", "plugins", "vexp-guard.js"),
356
+ content: VEXP_OPENCODE_GUARD,
357
+ alreadyExists: kilo.plugin === "updated",
358
+ action: kilo.plugin,
359
+ });
360
+ }
361
+ if (kilo.registered)
362
+ mcpConfigs.push(path.relative(workspaceRoot, kiloConfigTarget(workspaceRoot)));
363
+ }
364
+ // Cursor: preToolUse hook matching Grep. Text search only - Cursor's native
365
+ // semantic search is not hookable.
366
+ if (detector.agent === "Cursor") {
367
+ const cur = installCursorHook(workspaceRoot);
368
+ if (cur.hook) {
369
+ results.push({
370
+ agent: "Cursor Guard",
371
+ configFile: path.join(".cursor", "hooks", "vexp-guard.js"),
372
+ content: VEXP_CURSOR_GUARD,
373
+ alreadyExists: cur.hook === "updated",
374
+ action: cur.hook,
375
+ });
376
+ }
377
+ if (cur.registered)
378
+ mcpConfigs.push(path.join(".cursor", "hooks.json"));
348
379
  }
349
- // Opencode: install the guard plugin that blocks grep/glob while the daemon
350
- // is up (opencode's analogue of the Claude Code PreToolUse hook).
380
+ // Opencode: MCP lives under the `mcp` key in opencode.json(c) — opencode
381
+ // carries no `mcpConfigFile`, so without this call the generic writer above
382
+ // skips it and setup registers no tool at all. Plus the guard plugin that
383
+ // blocks grep/glob while the daemon is up (opencode's analogue of the
384
+ // Claude Code PreToolUse hook) — which is only safe once MCP is registered.
351
385
  if (detector.agent === "Opencode") {
386
+ const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
387
+ if (wroteMcp)
388
+ mcpConfigs.push(wroteMcp);
352
389
  const pluginResult = installOpencodePlugin(workspaceRoot);
353
390
  if (pluginResult) {
354
391
  results.push({
@@ -376,6 +413,60 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
376
413
  export function getAgentList() {
377
414
  return [...AGENT_DETECTORS];
378
415
  }
416
+ /** Fold a name to its comparable form: case, spaces, dots and dashes carry no
417
+ * meaning here ("claude-code", "Claude Code" and "claudecode" are one agent). */
418
+ function foldAgentName(s) {
419
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
420
+ }
421
+ /**
422
+ * Canonicalize a name given to `--agents`, or null if it names no agent.
423
+ *
424
+ * `configureSelectedAgents` matches with `Array.includes` on the exact string,
425
+ * so "opencode" silently configured nothing while "Opencode" worked — nobody
426
+ * can be expected to know the capitalization, and the failure was invisible.
427
+ */
428
+ export function resolveAgentName(input) {
429
+ const want = foldAgentName(input);
430
+ if (!want)
431
+ return null;
432
+ return AGENT_DETECTORS.find((d) => foldAgentName(d.agent) === want)?.agent ?? null;
433
+ }
434
+ /** Levenshtein distance, for "did you mean" on an unknown --agents name. */
435
+ function editDistance(a, b) {
436
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
437
+ const cur = new Array(b.length + 1).fill(0);
438
+ for (let i = 1; i <= a.length; i++) {
439
+ cur[0] = i;
440
+ for (let j = 1; j <= b.length; j++) {
441
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
442
+ }
443
+ prev.splice(0, prev.length, ...cur);
444
+ }
445
+ return prev[b.length];
446
+ }
447
+ /**
448
+ * The known agent an unknown name most likely meant, or null when nothing is
449
+ * close enough to be worth suggesting. Substring hits win (a bare "copilot"
450
+ * means "GitHub Copilot"); otherwise allow a couple of typos.
451
+ */
452
+ export function suggestAgentName(input) {
453
+ const want = foldAgentName(input);
454
+ if (!want)
455
+ return null;
456
+ const contained = AGENT_DETECTORS.find((d) => {
457
+ const f = foldAgentName(d.agent);
458
+ return f.includes(want) || want.includes(f);
459
+ });
460
+ if (contained)
461
+ return contained.agent;
462
+ let best = null;
463
+ for (const d of AGENT_DETECTORS) {
464
+ const dist = editDistance(want, foldAgentName(d.agent));
465
+ if (!best || dist < best.d)
466
+ best = { name: d.agent, d: dist };
467
+ }
468
+ return best && best.d <= 3 ? best.name : null;
469
+ }
379
470
  /**
380
471
  * Configure specific agents selected by the user.
381
472
  * Unlike configureAgents which auto-detects, this forces configuration
@@ -469,8 +560,24 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
469
560
  mcpConfigs.push(".zed/settings.json");
470
561
  }
471
562
  if (detector.agent === "Opencode") {
563
+ const wrote = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
564
+ if (wrote)
565
+ mcpConfigs.push(wrote);
472
566
  installOpencodePlugin(workspaceRoot);
473
567
  }
568
+ if (detector.agent === "Cursor") {
569
+ installCursorHook(workspaceRoot);
570
+ }
571
+ // Kilo carries no `mcpConfigFile` (its MCP lives under the `mcp` key inside
572
+ // kilo.jsonc), so without this branch the generic writer above skips it and
573
+ // picking Kilo explicitly used to write the rules file and NOTHING else — an
574
+ // agent told "always call run_pipeline" while the tool was never registered.
575
+ if (detector.agent === "Kilo Code") {
576
+ const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
577
+ if (wrote)
578
+ mcpConfigs.push(wrote);
579
+ installKiloPlugin(workspaceRoot);
580
+ }
474
581
  results.push({ agent: detector.agent, configFile: detector.configFile, content, alreadyExists, action });
475
582
  }
476
583
  return { agents: results, mcpConfigs };
@@ -1070,14 +1177,93 @@ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot
1070
1177
  * an existing file is backed up before rewrite. Returns the workspace-relative
1071
1178
  * path written, or null on a no-op / unparseable file.
1072
1179
  */
1073
- export function configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath) {
1180
+ /**
1181
+ * The kilo.jsonc that Kilo will actually read: `.kilo/kilo.jsonc` wins over a
1182
+ * root `kilo.jsonc` when both exist, and a fresh setup gets the cleaner `.kilo/`
1183
+ * location. Shared by the MCP writer and the guard-plugin installer so the two
1184
+ * can never disagree about which file is live.
1185
+ */
1186
+ export function kiloConfigTarget(workspaceRoot) {
1074
1187
  const dotKilo = path.join(workspaceRoot, ".kilo", "kilo.jsonc");
1075
1188
  const rootKilo = path.join(workspaceRoot, "kilo.jsonc");
1076
- const target = fs.existsSync(dotKilo)
1189
+ return fs.existsSync(dotKilo)
1077
1190
  ? dotKilo
1078
1191
  : fs.existsSync(rootKilo)
1079
1192
  ? rootKilo
1080
- : dotKilo; // fresh setup → the recommended cleaner location
1193
+ : dotKilo;
1194
+ }
1195
+ /**
1196
+ * The opencode config file that opencode will actually read for `mcp`:
1197
+ * `opencode.jsonc` wins over `opencode.json` when both exist (jsonc is the
1198
+ * variant users keep when they want comments, and it is the one they edit),
1199
+ * and a fresh setup gets plain `opencode.json` — opencode's documented default.
1200
+ * Project-scope only: we never touch the machine-global
1201
+ * `~/.config/opencode/opencode.json`, because a per-workspace VEXP_WORKSPACE pin
1202
+ * in a global file would mis-target every other project on the machine.
1203
+ */
1204
+ export function opencodeConfigTarget(workspaceRoot) {
1205
+ const jsonc = path.join(workspaceRoot, "opencode.jsonc");
1206
+ const json = path.join(workspaceRoot, "opencode.json");
1207
+ return fs.existsSync(jsonc) ? jsonc : json;
1208
+ }
1209
+ /**
1210
+ * Opencode MCP setup. Opencode reads MCP servers from the top-level `mcp` key
1211
+ * of `opencode.json` / `opencode.jsonc` — the same shape Kilo Code uses, because
1212
+ * Kilo v7 vendors opencode wholesale: `{type:"local", command:[…], enabled:true}`.
1213
+ *
1214
+ * Until this existed, picking Opencode in `vexp setup` wrote AGENTS.md and the
1215
+ * guard plugin and NOTHING else — the worst possible end state, because the
1216
+ * guard blocks grep/glob whenever the daemon is healthy while `run_pipeline` was
1217
+ * never registered: the agent lost search and gained no replacement. Reported by
1218
+ * an AppSumo user who had to add the `mcp` block by hand.
1219
+ *
1220
+ * Other `mcp` entries and unrelated top-level keys are preserved; an existing
1221
+ * file is backed up before rewrite. Returns the workspace-relative path written,
1222
+ * or null on a no-op / unparseable file.
1223
+ */
1224
+ export function configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath) {
1225
+ const target = opencodeConfigTarget(workspaceRoot);
1226
+ const read = readJsonConfigSafe(target);
1227
+ if (!read.ok) {
1228
+ warnUnparseable(target);
1229
+ return null;
1230
+ }
1231
+ const cfg = read.data;
1232
+ const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
1233
+ const command = useNode ? ["node", mcpServerPath] : [binaryPath, "mcp"];
1234
+ const env = { VEXP_WORKSPACE: workspaceRoot };
1235
+ const mcp = cfg.mcp ?? {};
1236
+ const prev = mcp["vexp"];
1237
+ const envMatches = JSON.stringify(prev?.["env"]) === JSON.stringify(env);
1238
+ const identical = prev?.["type"] === "local" &&
1239
+ prev?.["enabled"] === true &&
1240
+ Array.isArray(prev?.["command"]) &&
1241
+ JSON.stringify(prev["command"]) === JSON.stringify(command) &&
1242
+ envMatches;
1243
+ if (identical)
1244
+ return null;
1245
+ // A working entry is already here — either another vexp install, or the user's
1246
+ // own hand-written block (this bug made several people write one). Don't fight
1247
+ // it: only the install path may differ, and `envMatches` still gates on the
1248
+ // VEXP_WORKSPACE pin so a moved project is repinned normally.
1249
+ if (envMatches && prev?.["enabled"] === true && vexpEntryStillResolves(prev))
1250
+ return null;
1251
+ mcp["vexp"] = { type: "local", command, env, enabled: true };
1252
+ cfg.mcp = mcp;
1253
+ // `$schema` first if opencode's own key is absent — a fresh file we author
1254
+ // should look like the one opencode's docs tell users to write.
1255
+ if (cfg.$schema === undefined) {
1256
+ cfg.$schema = "https://opencode.ai/config.json";
1257
+ }
1258
+ if (read.existed)
1259
+ backupConfig(target);
1260
+ // A .jsonc target loses its comments here (we reserialize parsed JSON), which
1261
+ // is why the pre-write backup is not optional for this writer.
1262
+ fs.writeFileSync(target, JSON.stringify(cfg, null, 2), "utf-8");
1263
+ return path.relative(workspaceRoot, target);
1264
+ }
1265
+ export function configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath) {
1266
+ const target = kiloConfigTarget(workspaceRoot);
1081
1267
  const read = readJsonConfigSafe(target);
1082
1268
  if (!read.ok) {
1083
1269
  warnUnparseable(target);
@@ -1349,19 +1535,155 @@ export function installClaudeCodeHook(workspaceRoot) {
1349
1535
  * startup — no config-file merge required). Returns the action taken, or null
1350
1536
  * when the file is already byte-identical.
1351
1537
  */
1352
- export function installOpencodePlugin(workspaceRoot) {
1353
- const pluginDir = path.join(workspaceRoot, ".opencode", "plugins");
1354
- const pluginPath = path.join(pluginDir, "vexp-guard.js");
1355
- fs.mkdirSync(pluginDir, { recursive: true });
1538
+ function writeGuardScript(scriptPath, content) {
1539
+ const pluginPath = scriptPath;
1540
+ fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
1356
1541
  const existed = fs.existsSync(pluginPath);
1357
1542
  if (existed) {
1358
1543
  const current = fs.readFileSync(pluginPath, "utf-8");
1359
- if (current === VEXP_OPENCODE_GUARD)
1544
+ if (current === content)
1360
1545
  return null; // identical - skip
1546
+ // Content differs: either an older vexp guard, or a copy the user tuned by
1547
+ // hand. We refresh either way — refusing to touch a hand-edited plugin would
1548
+ // freeze that user on a stale guard forever — but their version stays
1549
+ // recoverable at vexp-guard.js.vexp-bak instead of being silently discarded.
1550
+ backupConfig(pluginPath);
1361
1551
  }
1362
- fs.writeFileSync(pluginPath, VEXP_OPENCODE_GUARD, "utf-8");
1552
+ fs.writeFileSync(pluginPath, content, "utf-8");
1363
1553
  return existed ? "updated" : "created";
1364
1554
  }
1555
+ export function installOpencodePlugin(workspaceRoot) {
1556
+ return writeGuardScript(path.join(workspaceRoot, ".opencode", "plugins", "vexp-guard.js"), VEXP_OPENCODE_GUARD);
1557
+ }
1558
+ /**
1559
+ * Cursor enforcement — `preToolUse` hook with matcher `Grep`.
1560
+ *
1561
+ * Cursor is the only VS Code fork where this is possible: Windsurf's hooks are
1562
+ * action-category based (twelve fixed events, no matcher, no tool names) and
1563
+ * expose NO search event at all, so there is nothing there to redirect. Cursor
1564
+ * exposes `Grep` as a matchable tool and feeds `agent_message` back to the model,
1565
+ * which is the whole mechanism.
1566
+ *
1567
+ * Scope of the claim, deliberately narrow: this covers TEXT search. Cursor's
1568
+ * native semantic codebase search has no documented tool identifier, so it is not
1569
+ * hookable and not covered. Do not describe this as "Cursor is enforced".
1570
+ *
1571
+ * `command` is `node`, not a shell script: Cursor documents no PowerShell variant
1572
+ * and never says which shell runs `command` on Windows. A bash guard would repeat
1573
+ * the Claude Code hook's Windows bug (silently enforced nothing for releases).
1574
+ * The path is project-root-relative because Cursor runs PROJECT hooks from the
1575
+ * project root — `./hooks/x` there would resolve to `<project>/hooks/x`.
1576
+ *
1577
+ * `failClosed` is left at its default (false): a crashing guard must let the
1578
+ * agent work, not lock it out.
1579
+ */
1580
+ export function installCursorHook(workspaceRoot) {
1581
+ const rel = path.join(".cursor", "hooks", "vexp-guard.js");
1582
+ const hookPath = path.join(workspaceRoot, rel);
1583
+ const hook = writeGuardScript(hookPath, VEXP_CURSOR_GUARD);
1584
+ const cfgPath = path.join(workspaceRoot, ".cursor", "hooks.json");
1585
+ const command = `node ${rel.split(path.sep).join("/")}`;
1586
+ const read = readJsonConfigSafe(cfgPath);
1587
+ if (!read.ok) {
1588
+ warnUnparseable(cfgPath);
1589
+ return { hook, registered: false };
1590
+ }
1591
+ const cfg = read.data;
1592
+ const hooks = cfg.hooks ?? {};
1593
+ const preToolUse = Array.isArray(hooks.preToolUse) ? [...hooks.preToolUse] : [];
1594
+ // Idempotent by COMMAND, not by array position: re-running setup must not
1595
+ // append a second copy, and a user's own preToolUse hooks must survive.
1596
+ const ours = (e) => !!e && typeof e === "object" && e.command === command;
1597
+ const existing = preToolUse.findIndex(ours);
1598
+ const entry = { command, matcher: "Grep" };
1599
+ if (existing >= 0 && JSON.stringify(preToolUse[existing]) === JSON.stringify(entry)) {
1600
+ return { hook, registered: false }; // already exactly right
1601
+ }
1602
+ if (existing >= 0)
1603
+ preToolUse[existing] = entry;
1604
+ else
1605
+ preToolUse.push(entry);
1606
+ hooks.preToolUse = preToolUse;
1607
+ cfg.hooks = hooks;
1608
+ if (cfg.version === undefined)
1609
+ cfg.version = 1;
1610
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
1611
+ if (read.existed)
1612
+ backupConfig(cfgPath);
1613
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf-8");
1614
+ return { hook, registered: true };
1615
+ }
1616
+ /**
1617
+ * Kilo Code enforcement — the same plugin, because Kilo v7 IS opencode: the
1618
+ * rewrite vendors it wholesale (the loader lives at packages/opencode/src/ inside
1619
+ * Kilo-Org/kilocode), so `tool.execute.before`, the `input.tool` ids and the
1620
+ * grep/glob arg schemas are the same code, not merely a similar API.
1621
+ *
1622
+ * Loading is by auto-discovery: the loader globs `{plugin,plugins}/*.{ts,js}`
1623
+ * with cwd = the directory of the kilo.jsonc it read, and every match is
1624
+ * registered at startup with no config entry. So `.kilo/plugins/vexp-guard.js`
1625
+ * is picked up for free — and we prefer that to an explicit `plugin` array entry
1626
+ * because it leaves the user's config untouched.
1627
+ *
1628
+ * The one case auto-discovery misses: a project whose config is the ROOT
1629
+ * `kilo.jsonc`, where cwd is the repo root and the scanned directory would be
1630
+ * `<root>/plugins/` — we will not create a top-level `plugins/` dir in someone's
1631
+ * repo just to be found. There we keep the file in `.kilo/plugins/` and register
1632
+ * it explicitly, with the path relative to the config file (the loader resolves
1633
+ * path specs against the declaring config's own directory, NOT the project root).
1634
+ *
1635
+ * The glob is non-recursive, so the file must sit flat in that directory.
1636
+ *
1637
+ * NOTE: `KILO_PURE=1` skips all external plugins. A CI run with it set gets no
1638
+ * guard — correctly so, but it means "the guard is installed" is not the same
1639
+ * claim as "the guard ran".
1640
+ */
1641
+ export function installKiloPlugin(workspaceRoot) {
1642
+ const pluginPath = path.join(workspaceRoot, ".kilo", "plugins", "vexp-guard.js");
1643
+ const plugin = writeGuardScript(pluginPath, VEXP_OPENCODE_GUARD);
1644
+ // `.kilo/kilo.jsonc` (the common case) → auto-discovery already covers us.
1645
+ const target = kiloConfigTarget(workspaceRoot);
1646
+ if (path.dirname(target) !== workspaceRoot)
1647
+ return { plugin, registered: false };
1648
+ const rel = "./" + path.relative(workspaceRoot, pluginPath).split(path.sep).join("/");
1649
+ const read = readJsonConfigSafe(target);
1650
+ if (!read.ok) {
1651
+ warnUnparseable(target);
1652
+ return { plugin, registered: false };
1653
+ }
1654
+ const cfg = read.data;
1655
+ const list = Array.isArray(cfg.plugin) ? cfg.plugin : [];
1656
+ if (list.includes(rel))
1657
+ return { plugin, registered: false }; // already declared
1658
+ cfg.plugin = [...list, rel];
1659
+ if (read.existed)
1660
+ backupConfig(target);
1661
+ fs.writeFileSync(target, JSON.stringify(cfg, null, 2), "utf-8");
1662
+ return { plugin, registered: true };
1663
+ }
1664
+ /**
1665
+ * The single highest-leverage instruction we ship, and for a long time the one
1666
+ * most agents never saw.
1667
+ *
1668
+ * vexp retrieval is identifier/path-first: FTS supplies the candidate pool and
1669
+ * the semantic layer only RE-RANKS it, so a symbol that no query token reaches
1670
+ * never enters the running at all. A task written as prose ("why does the proxy
1671
+ * fail?") therefore ranks far worse than the same task anchored on the symbols
1672
+ * and paths it is actually about — the difference between a useful pivot set and
1673
+ * "the vexp result wasn't relevant".
1674
+ *
1675
+ * This shipped only in the Claude Code and Cursor templates, while the AGENTS.md
1676
+ * example (`"task": "fix auth bug"`) actively modelled the weak mode — so two
1677
+ * independent field reports ("the index seems not very useful") came from users
1678
+ * whose instructions never told them the one thing that makes retrieval land.
1679
+ * Shared by every prose template so the wording cannot drift again. ASCII only
1680
+ * (these land in files that get read on Windows).
1681
+ */
1682
+ const QUERY_SHAPE = `### Query shape (do this)
1683
+ - Anchor the task on real identifiers (ClassName, functionName) or file paths:
1684
+ \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1685
+ - A pure natural-language question ("why does login fail?") falls back to text
1686
+ ranking and is much less reliable - name the symbols/files you want, not the question.`;
1365
1687
  function generateAgentConfig(template, vars) {
1366
1688
  switch (template) {
1367
1689
  case "claude-code":
@@ -1403,6 +1725,12 @@ uses fewer tokens than manual searching. Prefer \`get_skeleton\` over Read to
1403
1725
  inspect files (detail: minimal/standard/detailed, 70-90% token savings).
1404
1726
  Only use Read when you need exact raw content to edit a specific line.
1405
1727
 
1728
+ The hook denies Grep/Glob while the daemon is up. That is policy, not a transient
1729
+ failure: call \`run_pipeline\` instead - do NOT work around it by shelling out or
1730
+ writing a script. vexp only covers indexed source inside the workspace: for runtime
1731
+ logs, build output (dist/, .vite/, node_modules/) or files outside the repo it has
1732
+ no answer - use Bash/Read there, those are never blocked.
1733
+
1406
1734
  ### Primary Tool
1407
1735
  - \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
1408
1736
  capsule + impact + memory server-side. Returns compressed results.
@@ -1418,6 +1746,8 @@ Only use Read when you need exact raw content to edit a specific line.
1418
1746
  - \`index_status\` - indexing status and health check
1419
1747
  - \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
1420
1748
 
1749
+ ${QUERY_SHAPE}
1750
+
1421
1751
  ### Workflow
1422
1752
  1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
1423
1753
  2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` - avoid Read unless editing
@@ -1438,9 +1768,6 @@ Only use Read when you need exact raw content to edit a specific line.
1438
1768
  - **Session Memory**: auto-captures observations; memories auto-surfaced in results
1439
1769
  - **LSP Bridge**: VS Code captures type-resolved call edges
1440
1770
  - **Change Coupling**: co-changed files included as related context
1441
- - **Query tips**: include real identifiers (ClassName, function_name) or file paths
1442
- in the task for precise matches - pure natural-language phrasing falls back to
1443
- text ranking and is less reliable
1444
1771
 
1445
1772
  ### Advanced Parameters
1446
1773
  - \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
@@ -1471,19 +1798,19 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1471
1798
 
1472
1799
  ### Available MCP tools
1473
1800
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1474
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
1801
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1475
1802
  - \`get_skeleton\` - compact file structure
1476
1803
  - \`index_status\` - indexing status
1477
1804
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1478
1805
 
1806
+ ${QUERY_SHAPE}
1807
+
1479
1808
  ### Agentic search
1480
1809
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1481
1810
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1482
1811
  rather than letting them search the codebase independently
1483
1812
 
1484
1813
  ### Tips
1485
- - Include real identifiers (class/function names) or file paths in the task - literal
1486
- matches rank best; pure natural-language phrasing falls back to text ranking
1487
1814
  - Add \`include_tests: true\` when debugging
1488
1815
  - Use \`preset: "refactor"\` for deep impact analysis
1489
1816
 
@@ -1511,11 +1838,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1511
1838
 
1512
1839
  ### Available MCP tools
1513
1840
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1514
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
1841
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1515
1842
  - \`get_skeleton\` - compact file structure
1516
1843
  - \`index_status\` - indexing status
1517
1844
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1518
1845
 
1846
+ ${QUERY_SHAPE}
1847
+
1519
1848
  ### Agentic search
1520
1849
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1521
1850
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1560,11 +1889,13 @@ When working on this codebase:
1560
1889
 
1561
1890
  ### Available MCP tools
1562
1891
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1563
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1892
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1564
1893
  - \`get_skeleton\` - token-efficient file structure
1565
1894
  - \`index_status\` - indexing status
1566
1895
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1567
1896
 
1897
+ ${QUERY_SHAPE}
1898
+
1568
1899
  ### Agentic search
1569
1900
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1570
1901
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1590,11 +1921,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1590
1921
 
1591
1922
  ### Available MCP tools
1592
1923
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1593
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
1924
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1594
1925
  - \`get_skeleton\` - compact file structure
1595
1926
  - \`index_status\` - indexing status
1596
1927
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1597
1928
 
1929
+ ${QUERY_SHAPE}
1930
+
1598
1931
  ### Agentic search
1599
1932
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1600
1933
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1620,11 +1953,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1620
1953
 
1621
1954
  ### Available MCP tools
1622
1955
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1623
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1956
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1624
1957
  - \`get_skeleton\` - compact file structure
1625
1958
  - \`index_status\` - indexing status
1626
1959
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1627
1960
 
1961
+ ${QUERY_SHAPE}
1962
+
1628
1963
  ### Agentic search
1629
1964
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1630
1965
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1650,13 +1985,20 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1650
1985
 
1651
1986
  ### Available MCP tools
1652
1987
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1653
- Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
1988
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1654
1989
  - \`get_skeleton\` - compact file structure
1655
1990
  - \`index_status\` - indexing status
1656
1991
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1657
1992
 
1993
+ ${QUERY_SHAPE}
1994
+
1658
1995
  ### Agentic search
1659
1996
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1997
+ - If a search tool is denied, that is policy, not a transient failure: call \`run_pipeline\`
1998
+ instead. Do NOT work around it with shell search or by writing a script.
1999
+ - vexp only covers indexed source inside the workspace. For runtime logs, build output
2000
+ (dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
2001
+ normal tools there; those searches are never blocked.
1660
2002
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1661
2003
  rather than letting them search the codebase independently
1662
2004
 
@@ -1680,11 +2022,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1680
2022
 
1681
2023
  ## Available vexp tools
1682
2024
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1683
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
2025
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1684
2026
  - \`get_skeleton\` - compact file structure
1685
2027
  - \`index_status\` - indexing status
1686
2028
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1687
2029
 
2030
+ ${QUERY_SHAPE}
2031
+
1688
2032
  ## Agentic search
1689
2033
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1690
2034
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1698,32 +2042,65 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1698
2042
  <!-- /vexp -->`;
1699
2043
  }
1700
2044
  function genericTemplate(vars) {
1701
- return `## vexp <!-- vexp v${vars.version} -->
2045
+ return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
1702
2046
 
1703
- **MANDATORY: use \`run_pipeline\` - do NOT grep or glob the codebase.**
1704
- vexp returns pre-indexed, graph-ranked context in a single call.
2047
+ ### MANDATORY: call run_pipeline FIRST - do NOT grep, glob, or read to explore
2048
+ For every task - bug fixes, features, refactors, questions about the code:
2049
+ **call \`run_pipeline\` before any other tool**. It runs context search + impact
2050
+ analysis + memory recall in a single call and returns compressed, graph-ranked
2051
+ results.
2052
+
2053
+ This holds even when you already know the file path. The pipeline returns the
2054
+ callers, the blast radius and the co-changed files that opening the file cannot.
2055
+ No tool here will stop you from searching manually, so honouring this rule is on
2056
+ you: "I already knew where to look" is not an exception, it is the exact case the
2057
+ rule exists for.
2058
+
2059
+ Do NOT use grep, glob, built-in codebase search, or shell commands to explore.
2060
+ Do NOT open files one by one to find your way around: use \`get_skeleton\`
2061
+ (detail: minimal/standard/detailed, 70-90% fewer tokens than reading the file).
2062
+ Read raw file content only when you need it to edit a specific line.
2063
+
2064
+ vexp indexes source inside the workspace and nothing else. Runtime logs, build
2065
+ output (dist/, .vite/, node_modules/) and files outside the repo are NOT indexed -
2066
+ read those directly, this rule does not cover them.
2067
+
2068
+ ### Primary tool
2069
+ - \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Auto-detects intent
2070
+ (debug/modify/refactor/explore) from your task. Includes file content for pivots.
2071
+ - \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2072
+ - \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\`
2073
+ - \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - saves an insight in the same call
2074
+
2075
+ ### Other MCP tools (only when run_pipeline is not enough)
2076
+ - \`get_skeleton\` - **preferred over reading a file**: signatures and structure, 3 detail levels
2077
+ - \`index_status\` - indexing status and health check
2078
+ - \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
2079
+
2080
+ ${QUERY_SHAPE}
1705
2081
 
1706
2082
  ### Workflow
1707
- 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
1708
- 2. Make targeted changes based on the context returned
1709
- 3. \`run_pipeline\` again only if you need more context
2083
+ 1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
2084
+ 2. Need more on a file? \`get_skeleton({ files: [...], detail: "detailed" })\` - not a raw read
2085
+ 3. Make targeted changes based on the context returned
2086
+ 4. \`run_pipeline\` again ONLY if you need more context while implementing
2087
+ 5. Do NOT chain vexp calls - one \`run_pipeline\` replaces capsule + impact + memory + observation
1710
2088
 
1711
- ### Available MCP tools
1712
- - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1713
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1714
- - \`get_skeleton\` - compact file structure
1715
- - \`index_status\` - indexing status
1716
- - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2089
+ ### Sub-agents and background tasks
2090
+ - Sub-agents CAN and MUST call \`run_pipeline\` - always give them the task description
2091
+ - Do NOT spawn an agent to search freely: call \`run_pipeline\` first, then pass the
2092
+ returned context into the agent prompt
1717
2093
 
1718
- ### Agentic search
1719
- - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1720
- - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1721
- rather than letting them search the codebase independently
2094
+ ### Fallback
2095
+ If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
2096
+ the index is empty or still building. Use the built-in search and read tools directly
2097
+ until it is ready - do not stall waiting for vexp.
1722
2098
 
1723
- ### Smart Features
1724
- Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
2099
+ ### Smart features (automatic - no action needed)
2100
+ Intent detection, hybrid keyword+semantic+graph ranking, session memory,
2101
+ change coupling, auto-expanding budget.
1725
2102
 
1726
- ### Multi-Repo
2103
+ ### Multi-repo
1727
2104
  \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
1728
2105
  <!-- /vexp -->`;
1729
2106
  }