vexp-cli 2.2.1 → 2.2.2

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.
@@ -376,6 +376,60 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
376
376
  export function getAgentList() {
377
377
  return [...AGENT_DETECTORS];
378
378
  }
379
+ /** Fold a name to its comparable form: case, spaces, dots and dashes carry no
380
+ * meaning here ("claude-code", "Claude Code" and "claudecode" are one agent). */
381
+ function foldAgentName(s) {
382
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
383
+ }
384
+ /**
385
+ * Canonicalize a name given to `--agents`, or null if it names no agent.
386
+ *
387
+ * `configureSelectedAgents` matches with `Array.includes` on the exact string,
388
+ * so "opencode" silently configured nothing while "Opencode" worked — nobody
389
+ * can be expected to know the capitalization, and the failure was invisible.
390
+ */
391
+ export function resolveAgentName(input) {
392
+ const want = foldAgentName(input);
393
+ if (!want)
394
+ return null;
395
+ return AGENT_DETECTORS.find((d) => foldAgentName(d.agent) === want)?.agent ?? null;
396
+ }
397
+ /** Levenshtein distance, for "did you mean" on an unknown --agents name. */
398
+ function editDistance(a, b) {
399
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
400
+ const cur = new Array(b.length + 1).fill(0);
401
+ for (let i = 1; i <= a.length; i++) {
402
+ cur[0] = i;
403
+ for (let j = 1; j <= b.length; j++) {
404
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
405
+ }
406
+ prev.splice(0, prev.length, ...cur);
407
+ }
408
+ return prev[b.length];
409
+ }
410
+ /**
411
+ * The known agent an unknown name most likely meant, or null when nothing is
412
+ * close enough to be worth suggesting. Substring hits win (a bare "copilot"
413
+ * means "GitHub Copilot"); otherwise allow a couple of typos.
414
+ */
415
+ export function suggestAgentName(input) {
416
+ const want = foldAgentName(input);
417
+ if (!want)
418
+ return null;
419
+ const contained = AGENT_DETECTORS.find((d) => {
420
+ const f = foldAgentName(d.agent);
421
+ return f.includes(want) || want.includes(f);
422
+ });
423
+ if (contained)
424
+ return contained.agent;
425
+ let best = null;
426
+ for (const d of AGENT_DETECTORS) {
427
+ const dist = editDistance(want, foldAgentName(d.agent));
428
+ if (!best || dist < best.d)
429
+ best = { name: d.agent, d: dist };
430
+ }
431
+ return best && best.d <= 3 ? best.name : null;
432
+ }
379
433
  /**
380
434
  * Configure specific agents selected by the user.
381
435
  * Unlike configureAgents which auto-detects, this forces configuration
@@ -1358,10 +1412,38 @@ export function installOpencodePlugin(workspaceRoot) {
1358
1412
  const current = fs.readFileSync(pluginPath, "utf-8");
1359
1413
  if (current === VEXP_OPENCODE_GUARD)
1360
1414
  return null; // identical - skip
1415
+ // Content differs: either an older vexp guard, or a copy the user tuned by
1416
+ // hand. We refresh either way — refusing to touch a hand-edited plugin would
1417
+ // freeze that user on a stale guard forever — but their version stays
1418
+ // recoverable at vexp-guard.js.vexp-bak instead of being silently discarded.
1419
+ backupConfig(pluginPath);
1361
1420
  }
1362
1421
  fs.writeFileSync(pluginPath, VEXP_OPENCODE_GUARD, "utf-8");
1363
1422
  return existed ? "updated" : "created";
1364
1423
  }
1424
+ /**
1425
+ * The single highest-leverage instruction we ship, and for a long time the one
1426
+ * most agents never saw.
1427
+ *
1428
+ * vexp retrieval is identifier/path-first: FTS supplies the candidate pool and
1429
+ * the semantic layer only RE-RANKS it, so a symbol that no query token reaches
1430
+ * never enters the running at all. A task written as prose ("why does the proxy
1431
+ * fail?") therefore ranks far worse than the same task anchored on the symbols
1432
+ * and paths it is actually about — the difference between a useful pivot set and
1433
+ * "the vexp result wasn't relevant".
1434
+ *
1435
+ * This shipped only in the Claude Code and Cursor templates, while the AGENTS.md
1436
+ * example (`"task": "fix auth bug"`) actively modelled the weak mode — so two
1437
+ * independent field reports ("the index seems not very useful") came from users
1438
+ * whose instructions never told them the one thing that makes retrieval land.
1439
+ * Shared by every prose template so the wording cannot drift again. ASCII only
1440
+ * (these land in files that get read on Windows).
1441
+ */
1442
+ const QUERY_SHAPE = `### Query shape (do this)
1443
+ - Anchor the task on real identifiers (ClassName, functionName) or file paths:
1444
+ \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1445
+ - A pure natural-language question ("why does login fail?") falls back to text
1446
+ ranking and is much less reliable - name the symbols/files you want, not the question.`;
1365
1447
  function generateAgentConfig(template, vars) {
1366
1448
  switch (template) {
1367
1449
  case "claude-code":
@@ -1403,6 +1485,12 @@ uses fewer tokens than manual searching. Prefer \`get_skeleton\` over Read to
1403
1485
  inspect files (detail: minimal/standard/detailed, 70-90% token savings).
1404
1486
  Only use Read when you need exact raw content to edit a specific line.
1405
1487
 
1488
+ The hook denies Grep/Glob while the daemon is up. That is policy, not a transient
1489
+ failure: call \`run_pipeline\` instead - do NOT work around it by shelling out or
1490
+ writing a script. vexp only covers indexed source inside the workspace: for runtime
1491
+ logs, build output (dist/, .vite/, node_modules/) or files outside the repo it has
1492
+ no answer - use Bash/Read there, those are never blocked.
1493
+
1406
1494
  ### Primary Tool
1407
1495
  - \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
1408
1496
  capsule + impact + memory server-side. Returns compressed results.
@@ -1418,6 +1506,8 @@ Only use Read when you need exact raw content to edit a specific line.
1418
1506
  - \`index_status\` - indexing status and health check
1419
1507
  - \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
1420
1508
 
1509
+ ${QUERY_SHAPE}
1510
+
1421
1511
  ### Workflow
1422
1512
  1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
1423
1513
  2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` - avoid Read unless editing
@@ -1438,9 +1528,6 @@ Only use Read when you need exact raw content to edit a specific line.
1438
1528
  - **Session Memory**: auto-captures observations; memories auto-surfaced in results
1439
1529
  - **LSP Bridge**: VS Code captures type-resolved call edges
1440
1530
  - **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
1531
 
1445
1532
  ### Advanced Parameters
1446
1533
  - \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
@@ -1471,19 +1558,19 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1471
1558
 
1472
1559
  ### Available MCP tools
1473
1560
  - \`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" })\`
1561
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1475
1562
  - \`get_skeleton\` - compact file structure
1476
1563
  - \`index_status\` - indexing status
1477
1564
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1478
1565
 
1566
+ ${QUERY_SHAPE}
1567
+
1479
1568
  ### Agentic search
1480
1569
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1481
1570
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1482
1571
  rather than letting them search the codebase independently
1483
1572
 
1484
1573
  ### 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
1574
  - Add \`include_tests: true\` when debugging
1488
1575
  - Use \`preset: "refactor"\` for deep impact analysis
1489
1576
 
@@ -1511,11 +1598,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1511
1598
 
1512
1599
  ### Available MCP tools
1513
1600
  - \`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" })\`
1601
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1515
1602
  - \`get_skeleton\` - compact file structure
1516
1603
  - \`index_status\` - indexing status
1517
1604
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1518
1605
 
1606
+ ${QUERY_SHAPE}
1607
+
1519
1608
  ### Agentic search
1520
1609
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1521
1610
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1560,11 +1649,13 @@ When working on this codebase:
1560
1649
 
1561
1650
  ### Available MCP tools
1562
1651
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1563
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1652
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1564
1653
  - \`get_skeleton\` - token-efficient file structure
1565
1654
  - \`index_status\` - indexing status
1566
1655
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1567
1656
 
1657
+ ${QUERY_SHAPE}
1658
+
1568
1659
  ### Agentic search
1569
1660
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1570
1661
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1590,11 +1681,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1590
1681
 
1591
1682
  ### Available MCP tools
1592
1683
  - \`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" })\`
1684
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1594
1685
  - \`get_skeleton\` - compact file structure
1595
1686
  - \`index_status\` - indexing status
1596
1687
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1597
1688
 
1689
+ ${QUERY_SHAPE}
1690
+
1598
1691
  ### Agentic search
1599
1692
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1600
1693
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1620,11 +1713,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1620
1713
 
1621
1714
  ### Available MCP tools
1622
1715
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1623
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1716
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1624
1717
  - \`get_skeleton\` - compact file structure
1625
1718
  - \`index_status\` - indexing status
1626
1719
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1627
1720
 
1721
+ ${QUERY_SHAPE}
1722
+
1628
1723
  ### Agentic search
1629
1724
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1630
1725
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1650,13 +1745,20 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1650
1745
 
1651
1746
  ### Available MCP tools
1652
1747
  - \`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" })\`
1748
+ Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1654
1749
  - \`get_skeleton\` - compact file structure
1655
1750
  - \`index_status\` - indexing status
1656
1751
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1657
1752
 
1753
+ ${QUERY_SHAPE}
1754
+
1658
1755
  ### Agentic search
1659
1756
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1757
+ - If a search tool is denied, that is policy, not a transient failure: call \`run_pipeline\`
1758
+ instead. Do NOT work around it with shell search or by writing a script.
1759
+ - vexp only covers indexed source inside the workspace. For runtime logs, build output
1760
+ (dist/, .vite/, node_modules/) or files outside the repo it has no answer - use your
1761
+ normal tools there; those searches are never blocked.
1660
1762
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1661
1763
  rather than letting them search the codebase independently
1662
1764
 
@@ -1680,11 +1782,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1680
1782
 
1681
1783
  ## Available vexp tools
1682
1784
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1683
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1785
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1684
1786
  - \`get_skeleton\` - compact file structure
1685
1787
  - \`index_status\` - indexing status
1686
1788
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1687
1789
 
1790
+ ${QUERY_SHAPE}
1791
+
1688
1792
  ## Agentic search
1689
1793
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1690
1794
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
@@ -1710,11 +1814,13 @@ vexp returns pre-indexed, graph-ranked context in a single call.
1710
1814
 
1711
1815
  ### Available MCP tools
1712
1816
  - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1713
- Example: \`run_pipeline({ "task": "fix auth bug" })\`
1817
+ Example: \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
1714
1818
  - \`get_skeleton\` - compact file structure
1715
1819
  - \`index_status\` - indexing status
1716
1820
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1717
1821
 
1822
+ ${QUERY_SHAPE}
1823
+
1718
1824
  ### Agentic search
1719
1825
  - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1720
1826
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import * as fs from "fs";
8
8
  import * as net from "net";
9
9
  import { checkbox, confirm } from "@inquirer/prompts";
10
10
  import { getBinaryPath, getInstalledVersion, getMcpServerPath, binaryEnv } from "./binary.js";
11
- import { detectAgents, getAgentList, configureSelectedAgents } from "./agent-config.js";
11
+ import { detectAgents, getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName } from "./agent-config.js";
12
12
  import { CLI_VERSION } from "./version.js";
13
13
  import { activateLicense, deactivateLicense, readLicenseLimits, readDeviceBlocked, } from "./license.js";
14
14
  import { checkForUpdate } from "./update-check.js";
@@ -555,8 +555,24 @@ program
555
555
  const detectedNames = new Set(detected.map((a) => a.agent));
556
556
  let selectedNames;
557
557
  if (typeof opts.agents === "string") {
558
- // Explicit list from --agents flag
559
- selectedNames = opts.agents.split(",").map((s) => s.trim());
558
+ // Explicit list from --agents flag. Resolve every name BEFORE writing
559
+ // anything: matching is exact downstream, so an unrecognised name used
560
+ // to configure nothing while setup still reported success — the worst
561
+ // possible outcome for the flag whose whole point is automation.
562
+ const requested = opts.agents.split(",").map((s) => s.trim()).filter(Boolean);
563
+ const unknown = requested.filter((n) => resolveAgentName(n) === null);
564
+ if (unknown.length > 0 || requested.length === 0) {
565
+ console.error(chalk.red(`\n✖ Unknown agent: ${unknown.map((u) => `"${u}"`).join(", ") || "(empty --agents list)"}`));
566
+ for (const u of unknown) {
567
+ const hint = suggestAgentName(u);
568
+ if (hint)
569
+ console.error(chalk.yellow(` Did you mean: ${hint}?`));
570
+ }
571
+ console.error(chalk.dim(` Valid agents: ${allAgents.map((a) => a.agent).join(", ")}`));
572
+ console.error(chalk.dim(" Names are matched ignoring case and punctuation.\n"));
573
+ process.exit(1);
574
+ }
575
+ selectedNames = requested.map((n) => resolveAgentName(n));
560
576
  console.log(chalk.dim(` Agents (from flag): ${selectedNames.join(", ")}`));
561
577
  }
562
578
  else {
@@ -69,13 +69,25 @@ esac
69
69
  * test in packages/vexp-cli/test/hook-template.test.ts). Do NOT introduce any
70
70
  * of those characters here without updating that test's un-escape logic.
71
71
  */
72
- export const VEXP_OPENCODE_GUARD = `// vexp-guard - opencode plugin (generated by 'vexp setup-agents').
73
- // Forces the agent onto vexp's run_pipeline by blocking opencode's native
74
- // grep/glob (and shelled-out tree search) WHILE the vexp daemon is healthy.
72
+ export const VEXP_OPENCODE_GUARD = `// vexp-guard - opencode plugin (generated by 'vexp setup-agents'; it is rewritten
73
+ // on every setup, and a modified copy is saved to vexp-guard.js.vexp-bak first).
74
+ //
75
+ // Blocks opencode's native grep/glob ONLY where vexp has a better answer: indexed
76
+ // source inside the workspace, while the daemon is healthy. Two deliberate limits:
77
+ //
78
+ // 1. It never touches bash. Shelling out is the escape hatch for everything vexp
79
+ // cannot index - runtime logs, build output, files outside the repo. vexp's
80
+ // Claude Code guard has always matched Grep|Glob|Regex only, and it works
81
+ // precisely because that valve stays open. Blocking shell search instead
82
+ // teaches the agent to evade (any prefix defeats it) and strands it when the
83
+ // answer genuinely is not in the index.
84
+ // 2. It only blocks targets vexp actually indexed. A search aimed at a log file,
85
+ // at dist/ or node_modules, or outside the workspace has no run_pipeline
86
+ // answer, so blocking it would be a dead end rather than a redirect.
87
+ //
75
88
  // Fails OPEN when the daemon is down, so native search still works with no index.
76
- // Mirrors vexp's Claude Code PreToolUse guard (.claude/hooks/vexp-guard.sh).
77
89
  import { existsSync, readFileSync } from "node:fs";
78
- import { join, dirname } from "node:path";
90
+ import { join, dirname, relative, isAbsolute, sep } from "node:path";
79
91
 
80
92
  // Walk up from start to the first ancestor that owns a .vexp dir.
81
93
  function findVexpDir(start) {
@@ -108,35 +120,74 @@ function daemonHealthy(vexpDir) {
108
120
  }
109
121
 
110
122
  const SEARCH_TOOLS = new Set(["grep", "glob"]);
111
- const SHELL_SEARCH_BINS = ["rg", "grep", "egrep", "fgrep", "ag", "ack", "find", "fd", "fdfind"];
112
123
 
113
- // True when a bash command's first token is a tree-search binary (grep/rg/find).
114
- function isShellSearch(command) {
115
- const t = (command || "").trim();
116
- for (const b of SHELL_SEARCH_BINS) {
117
- if (t === b || t.indexOf(b + " ") === 0) return true;
124
+ // Directory names vexp never indexes: build output and vendored dependencies.
125
+ const NON_INDEXED_DIRS = [
126
+ "node_modules", "dist", "build", "out", "coverage", "target", "vendor",
127
+ ".vite", ".next", ".nuxt", ".output", ".turbo", ".cache", ".git", ".vexp"
128
+ ];
129
+
130
+ // File kinds vexp never indexes. Logs are the case agents legitimately grep the
131
+ // most while debugging a running app. "-lock." is not redundant with ".lock":
132
+ // the two commonest JS lockfiles (package-lock.json, pnpm-lock.yaml) have no dot
133
+ // before "lock", so ".lock" alone let yarn.lock and Cargo.lock through while
134
+ // blocking them.
135
+ const NON_INDEXED_HINTS = [".log", ".lock", "-lock.", ".map", ".min.js"];
136
+
137
+ // Is this search target inside what vexp indexed? Only then is "call
138
+ // run_pipeline instead" real advice rather than a dead end. Anything that cannot
139
+ // be resolved confidently returns false (allow) - under-blocking just leaves the
140
+ // agent on grep, over-blocking strands it with no tool at all.
141
+ function isIndexedTarget(root, target) {
142
+ if (!target) return true; // no path given: a whole-workspace search is vexp's job
143
+ const s = String(target);
144
+ const low = s.toLowerCase();
145
+ for (const h of NON_INDEXED_HINTS) if (low.indexOf(h) !== -1) return false;
146
+ let rel;
147
+ try {
148
+ rel = relative(root, isAbsolute(s) ? s : join(root, s));
149
+ } catch (e) {
150
+ return false;
118
151
  }
119
- return false;
152
+ if (rel === "..") return false;
153
+ if (rel.indexOf(".." + sep) === 0) return false; // escapes the workspace
154
+ if (isAbsolute(rel)) return false; // different root or drive
155
+ const segs = rel.split(sep);
156
+ for (const g of segs) if (NON_INDEXED_DIRS.indexOf(g) !== -1) return false;
157
+ return true;
158
+ }
159
+
160
+ // Where a search is aimed. grep carries the regex in its pattern arg (never a
161
+ // path), so only path and the include filter describe its target; for glob the
162
+ // pattern IS the path.
163
+ function searchTargets(tool, args) {
164
+ if (!args) return [];
165
+ const out = [];
166
+ if (args.path) out.push(args.path);
167
+ if (tool === "glob" && args.pattern) out.push(args.pattern);
168
+ if (tool === "grep" && args.include) out.push(args.include);
169
+ return out;
120
170
  }
121
171
 
122
172
  export const VexpGuard = async ({ directory, worktree }) => {
123
- const root = worktree || directory || ".";
173
+ const start = worktree || directory || ".";
124
174
  let vexpDir = null;
125
175
  return {
126
176
  "tool.execute.before": async (input, output) => {
127
- if (!vexpDir) vexpDir = findVexpDir(root);
128
- if (!daemonHealthy(vexpDir)) return;
129
- if (SEARCH_TOOLS.has(input.tool)) {
130
- throw new Error(
131
- "vexp daemon is running - call run_pipeline instead of " + input.tool +
132
- " (pre-indexed, graph-ranked, ~60% fewer tokens)."
133
- );
134
- }
135
- if (input.tool === "bash" && isShellSearch(output && output.args && output.args.command)) {
136
- throw new Error(
137
- "vexp daemon is running - call run_pipeline instead of shell search (grep/rg/find)."
138
- );
177
+ if (!vexpDir) vexpDir = findVexpDir(start);
178
+ if (!daemonHealthy(vexpDir)) return; // no live index -> allow
179
+ if (!SEARCH_TOOLS.has(input.tool)) return; // bash and everything else: never blocked
180
+ const root = dirname(vexpDir);
181
+ const targets = searchTargets(input.tool, output && output.args);
182
+ for (const t of targets) {
183
+ if (!isIndexedTarget(root, t)) return; // vexp has no answer here -> allow
139
184
  }
185
+ throw new Error(
186
+ "vexp daemon is running. AGENT INSTRUCTION: call run_pipeline instead of " +
187
+ input.tool + " - it returns pre-indexed, graph-ranked context in one call." +
188
+ " This is policy, not a transient failure: do NOT work around it with shell" +
189
+ " search or by writing a script."
190
+ );
140
191
  },
141
192
  };
142
193
  };