vexp-cli 2.3.1 → 2.4.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.
@@ -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 } from "./hook-template.js";
13
+ import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpOpencodeHintPlugin } from "./hook-template.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // Constants
16
16
  // ---------------------------------------------------------------------------
@@ -320,6 +320,18 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
320
320
  const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot);
321
321
  if (wrote)
322
322
  mcpConfigs.push("~/.claude.json");
323
+ // Event-driven hint hook: default-on (non-blocking, fail-open — the
324
+ // 2.3 A2 opt-in applies to DENY hooks, this one cannot block).
325
+ const hintResult = installClaudeCodeHintHook(workspaceRoot, binaryPath);
326
+ if (hintResult) {
327
+ results.push({
328
+ agent: "Claude Code Hint",
329
+ configFile: ".claude/hooks/vexp-hint.sh",
330
+ content: vexpHintHookScript(binaryPath),
331
+ alreadyExists: false,
332
+ action: hintResult,
333
+ });
334
+ }
323
335
  // Guard is OPT-IN from 2.3 (see GuardMode): strict installs the
324
336
  // PreToolUse deny hook; default removes any previously installed one.
325
337
  if (guardMode() === "strict") {
@@ -349,6 +361,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
349
361
  const wrote = configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot);
350
362
  if (wrote)
351
363
  mcpConfigs.push("~/.codex/config.toml");
364
+ installCodexHintHook(workspaceRoot, binaryPath);
352
365
  }
353
366
  // Antigravity: machine-global MCP config (~/.gemini/antigravity/mcp_config.json)
354
367
  if (detector.agent === "Antigravity") {
@@ -376,6 +389,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
376
389
  const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
377
390
  if (wrote)
378
391
  mcpConfigs.push(wrote);
392
+ installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
379
393
  // Kilo v7 vendors opencode, so it takes the same guard plugin — the rules
380
394
  // markdown alone was demonstrably not enough (a reported session loaded
381
395
  // vexp.md, quoted it back, and still read five files by hand).
@@ -437,6 +451,7 @@ export function configureAgents(workspaceRoot, binaryPath, version, agentFilter,
437
451
  // Claude Code PreToolUse hook) — which is only safe once MCP is registered.
438
452
  if (detector.agent === "Opencode") {
439
453
  const wroteMcp = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
454
+ installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
440
455
  if (wroteMcp)
441
456
  mcpConfigs.push(wroteMcp);
442
457
  if (guardMode() === "strict") {
@@ -607,6 +622,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
607
622
  const wrote = configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot);
608
623
  if (wrote)
609
624
  mcpConfigs.push("~/.claude.json");
625
+ installClaudeCodeHintHook(workspaceRoot, binaryPath);
610
626
  if (guardMode() === "strict")
611
627
  installClaudeCodeHook(workspaceRoot);
612
628
  else
@@ -616,6 +632,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
616
632
  const wrote = configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot);
617
633
  if (wrote)
618
634
  mcpConfigs.push("~/.codex/config.toml");
635
+ installCodexHintHook(workspaceRoot, binaryPath);
619
636
  }
620
637
  if (detector.agent === "Antigravity") {
621
638
  const wrote = configureAntigravityGlobal(binaryPath, mcpServerPath);
@@ -636,6 +653,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
636
653
  const wrote = configureOpencodeMcp(workspaceRoot, binaryPath, mcpServerPath);
637
654
  if (wrote)
638
655
  mcpConfigs.push(wrote);
656
+ installOpencodeHintPlugin(workspaceRoot, binaryPath, ".opencode/plugin");
639
657
  if (guardMode() === "strict")
640
658
  installOpencodePlugin(workspaceRoot);
641
659
  else
@@ -655,6 +673,7 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
655
673
  const wrote = configureKiloMcp(workspaceRoot, binaryPath, mcpServerPath);
656
674
  if (wrote)
657
675
  mcpConfigs.push(wrote);
676
+ installOpencodeHintPlugin(workspaceRoot, binaryPath, ".kilo/plugin");
658
677
  if (guardMode() === "strict")
659
678
  installKiloPlugin(workspaceRoot);
660
679
  else
@@ -1634,20 +1653,23 @@ export function installClaudeCodeHook(workspaceRoot) {
1634
1653
  const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
1635
1654
  // Aggressively remove ALL vexp-related hook entries (old format, stale matchers, malformed)
1636
1655
  const filtered = existingPreToolUse.filter((h) => !isVexpGuardHookEntry(h));
1637
- // Exec form (`args` present → direct spawn, no `sh -c`): the old shell-form
1638
- // `$CLAUDE_PROJECT_DIR/...` word-split on project paths containing spaces,
1639
- // so sh tried to exec the path's first fragment and the guard NEVER ran —
1640
- // non-blocking failure, every search went through unguarded. In exec form
1641
- // Claude Code substitutes `${CLAUDE_PROJECT_DIR}` itself (brace form
1642
- // required) before spawning. `timeout` is SECONDS (default 600), not ms:
1643
- // 3000 configured a 50-minute hook timeout.
1656
+ // Shell form with an explicit `bash` prefix and a QUOTED path — the one
1657
+ // shape that works on every OS Claude Code supports:
1658
+ // - the quotes fix the historical spaces bug (unquoted
1659
+ // `$CLAUDE_PROJECT_DIR/...` word-split and the guard never ran);
1660
+ // - exec form (`args` present → direct spawn) fixed spaces but broke
1661
+ // Windows entirely: the OS cannot spawn a `.sh` without a shell, the
1662
+ // hook errored, failed open, and enforced nothing (field report) —
1663
+ // Claude Code on Windows runs shell-form hooks through Git Bash, so
1664
+ // `bash "$VAR"` works there and on unix alike.
1665
+ // `timeout` is SECONDS (default 600), not ms: 3000 once configured a
1666
+ // 50-minute hook timeout.
1644
1667
  filtered.push({
1645
1668
  matcher: "Grep|Glob|Regex",
1646
1669
  hooks: [
1647
1670
  {
1648
1671
  type: "command",
1649
- command: "${CLAUDE_PROJECT_DIR}/.claude/hooks/vexp-guard.sh",
1650
- args: [],
1672
+ command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-guard.sh"',
1651
1673
  timeout: 5,
1652
1674
  },
1653
1675
  ],
@@ -1667,6 +1689,144 @@ export function installClaudeCodeHook(workspaceRoot) {
1667
1689
  }
1668
1690
  return existed ? "updated" : "created";
1669
1691
  }
1692
+ function isVexpHintHookEntry(h) {
1693
+ if (!h || typeof h !== "object")
1694
+ return false;
1695
+ const entry = h;
1696
+ const own = entry.command?.includes("vexp-hint");
1697
+ if (own)
1698
+ return true;
1699
+ const hks = entry.hooks;
1700
+ return Array.isArray(hks) && hks.some((hook) => {
1701
+ if (!hook || typeof hook !== "object")
1702
+ return false;
1703
+ return hook.command?.includes("vexp-hint") ?? false;
1704
+ });
1705
+ }
1706
+ /**
1707
+ * Install the event-driven hint hook for Claude Code (2.3.3 default).
1708
+ * Replaces the resident CLAUDE.md strategy text: a UserPromptSubmit hook
1709
+ * asks the daemon to classify each prompt against the index and injects a
1710
+ * one-line run_pipeline suggestion ONLY for unfamiliar-area prompts. On
1711
+ * oriented prompts the injection is empty — zero context cost. Fail-open by
1712
+ * construction (see VEXP_HINT_HOOK): any failure = vanilla behavior.
1713
+ * Non-blocking and additive, so it is NOT gated by guardMode (the 2.3 A2
1714
+ * opt-in applies to DENY hooks, which add a failure mode; this one cannot
1715
+ * block anything).
1716
+ */
1717
+ export function installClaudeCodeHintHook(workspaceRoot, binaryPath) {
1718
+ const hookDir = path.join(workspaceRoot, ".claude", "hooks");
1719
+ const hookPath = path.join(hookDir, "vexp-hint.sh");
1720
+ const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
1721
+ const script = vexpHintHookScript(binaryPath);
1722
+ fs.mkdirSync(hookDir, { recursive: true });
1723
+ const existed = fs.existsSync(hookPath);
1724
+ const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
1725
+ if (!scriptIdentical) {
1726
+ fs.writeFileSync(hookPath, script, { mode: 0o755 });
1727
+ }
1728
+ const read = readJsonConfigSafe(settingsPath);
1729
+ if (!read.ok) {
1730
+ warnUnparseable(settingsPath);
1731
+ return scriptIdentical ? null : existed ? "updated" : "created";
1732
+ }
1733
+ const settings = read.data;
1734
+ const hooks = (settings.hooks ?? {});
1735
+ const existing = Array.isArray(hooks.UserPromptSubmit)
1736
+ ? hooks.UserPromptSubmit
1737
+ : [];
1738
+ const filtered = existing.filter((h) => !isVexpHintHookEntry(h));
1739
+ // Same cross-OS shape as the guard entry: bash prefix + QUOTED path
1740
+ // (spaces-safe on unix, Git Bash on Windows). timeout is SECONDS.
1741
+ filtered.push({
1742
+ hooks: [
1743
+ {
1744
+ type: "command",
1745
+ command: 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/vexp-hint.sh"',
1746
+ timeout: 5,
1747
+ },
1748
+ ],
1749
+ });
1750
+ const merged = { ...hooks, UserPromptSubmit: filtered };
1751
+ const settingsIdentical = JSON.stringify(merged) === JSON.stringify(settings.hooks ?? {});
1752
+ if (scriptIdentical && settingsIdentical)
1753
+ return null;
1754
+ settings.hooks = merged;
1755
+ if (!settingsIdentical) {
1756
+ if (read.existed)
1757
+ backupConfig(settingsPath);
1758
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1759
+ }
1760
+ return existed ? "updated" : "created";
1761
+ }
1762
+ /**
1763
+ * opencode/Kilo hint plugin (2.4.0): default-ON, non-blocking, fail-open —
1764
+ * unlike the deny guard (strict-only), this cannot break a session; it only
1765
+ * ever appends an orientation line the daemon judged useful.
1766
+ */
1767
+ export function installOpencodeHintPlugin(workspaceRoot, binaryPath, pluginDir) {
1768
+ const pluginPath = path.join(workspaceRoot, pluginDir, "vexp-hint.js");
1769
+ const content = vexpOpencodeHintPlugin(binaryPath);
1770
+ fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
1771
+ const existed = fs.existsSync(pluginPath);
1772
+ if (existed && fs.readFileSync(pluginPath, "utf-8") === content)
1773
+ return null;
1774
+ fs.writeFileSync(pluginPath, content);
1775
+ return existed ? "updated" : "created";
1776
+ }
1777
+ /**
1778
+ * Codex hint hook (2.4.0). Codex >=0.129 discovers `<repo>/.codex/hooks.json`
1779
+ * and its UserPromptSubmit stdout envelope is byte-identical to Claude
1780
+ * Code's (hookSpecificOutput.additionalContext), so the same prompt-hint
1781
+ * backend serves both: PROJECT-scope file, no global config touched.
1782
+ * Fail-open: missing bash/binary/daemon => no output => vanilla.
1783
+ */
1784
+ export function installCodexHintHook(workspaceRoot, binaryPath) {
1785
+ const dir = path.join(workspaceRoot, ".codex");
1786
+ const hookPath = path.join(dir, "vexp-hint.sh");
1787
+ const hooksJsonPath = path.join(dir, "hooks.json");
1788
+ const script = vexpHintHookScript(binaryPath);
1789
+ fs.mkdirSync(dir, { recursive: true });
1790
+ const existed = fs.existsSync(hookPath);
1791
+ const scriptIdentical = existed && fs.readFileSync(hookPath, "utf-8") === script;
1792
+ if (!scriptIdentical) {
1793
+ fs.writeFileSync(hookPath, script, { mode: 0o755 });
1794
+ }
1795
+ let root = {};
1796
+ if (fs.existsSync(hooksJsonPath)) {
1797
+ try {
1798
+ root = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
1799
+ }
1800
+ catch {
1801
+ warnUnparseable(hooksJsonPath);
1802
+ return scriptIdentical ? null : existed ? "updated" : "created";
1803
+ }
1804
+ }
1805
+ // Codex hooks.json shape: EVENT NAME at top level (no "hooks" wrapper).
1806
+ const existing = Array.isArray(root.UserPromptSubmit)
1807
+ ? root.UserPromptSubmit
1808
+ : [];
1809
+ const filtered = existing.filter((h) => !isVexpHintHookEntry(h));
1810
+ // Absolute path: Codex has no $CLAUDE_PROJECT_DIR substitution.
1811
+ filtered.push({
1812
+ hooks: [
1813
+ {
1814
+ type: "command",
1815
+ command: `bash "${hookPath.replace(/\\/g, "/")}"`,
1816
+ timeout: 5,
1817
+ },
1818
+ ],
1819
+ });
1820
+ const before = JSON.stringify(root.UserPromptSubmit ?? null);
1821
+ root.UserPromptSubmit = filtered;
1822
+ const identical = before === JSON.stringify(filtered);
1823
+ if (scriptIdentical && identical)
1824
+ return null;
1825
+ if (!identical) {
1826
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(root, null, 2) + "\n");
1827
+ }
1828
+ return existed ? "updated" : "created";
1829
+ }
1670
1830
  // ---------------------------------------------------------------------------
1671
1831
  // opencode plugin - blocks grep/glob (and shelled-out search) when the daemon
1672
1832
  // is healthy. opencode has no PreToolUse hook, but auto-loads plugins from
@@ -1929,90 +2089,30 @@ function generateAgentConfig(template, vars) {
1929
2089
  // ---------------------------------------------------------------------------
1930
2090
  function claudeCodeTemplate(vars) {
1931
2091
  return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
1932
-
1933
- ### Context strategy: one pipeline call up front, then your normal tools
1934
- For every multi-file task - bug fixes, features, refactors, debugging:
1935
- **call \`run_pipeline\` ONCE at the start**. It executes context search + impact
1936
- analysis + memory recall in a single call: which files matter, what breaks,
1937
- what was learned before. Then implement with your normal tools.
1938
-
1939
- Division of labor (each tool where it wins):
1940
- - **Architecture, impact, "where is X wired"**: \`run_pipeline\` - the graph
1941
- answers these better and cheaper than exploratory reading.
1942
- - **Literal text sweeps** (string constants, log messages, config keys, e.g.
1943
- \`localStorage\`, API paths): Grep/Glob directly - text search is what they
1944
- are best at, do NOT route text sweeps through vexp.
1945
- - **Editing**: Read the files you are about to modify; use \`get_skeleton\`
1946
- (detail: minimal/standard/detailed, 70-90% token savings) for files you only
2092
+ vexp runs entirely on this machine: local daemon, index in \`.vexp/\`.
2093
+ \`run_pipeline\` transmits nothing to any external service.
2094
+ - \`run_pipeline({ "task": "..." })\` - orientation in one call (ranked pivot
2095
+ files with line ranges + blast radius + session notes) when a task does NOT
2096
+ name the files/symbols to touch. If it does, SKIP vexp - use your normal tools.
2097
+ - \`get_skeleton\` - file structure at 70-90% token savings for files you only
1947
2098
  need to understand, not edit.
2099
+ - vexp may append a one-line hint to a prompt when orientation would help;
2100
+ otherwise it stays silent.
1948
2101
 
1949
- Every extra tool call costs a turn: one good \`run_pipeline\` call beats five
1950
- small ones. Each result opens with a coverage header (index: N files, M nodes) -
1951
- if it does not match this repo, run \`vexp index\`.
1952
- vexp only covers indexed source inside the workspace: for runtime logs, build
1953
- output (dist/, .vite/, node_modules/) or files outside the repo use Bash/Read.
1954
- vexp runs entirely on this machine: local daemon, local index stored inside the
1955
- workspace (\`.vexp/\`). \`run_pipeline\` transmits nothing to any external service -
1956
- treat it like a local build tool; no data-sharing consent is needed to call it.
1957
-
1958
- ### Primary Tool
1959
- - \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
1960
- capsule + impact + memory server-side. Returns compressed results.
1961
- Auto-detects intent (debug/modify/refactor/explore) from your task.
1962
- Includes full file content for pivots.
1963
- Examples:
1964
- - \`run_pipeline({ "task": "fix JWT validation bug" })\` - auto-detect
1965
- - \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\` - explicit
1966
- - \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - save insight in same call
1967
-
1968
- ### Other MCP tools (use only when run_pipeline is insufficient)
1969
- - \`get_skeleton\` - **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings)
1970
- - \`index_status\` - indexing status and health check
1971
- - \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
1972
-
1973
- ${QUERY_SHAPE}
1974
-
1975
- ### Workflow
1976
- 1. \`run_pipeline("your task")\` - ONCE at task start. Returns pivots + impact + memories in 1 call
1977
- 2. Literal string sweeps? Grep/Glob directly. Editing a file? Read it directly.
1978
- 3. Structural overview of a non-edit file? \`get_skeleton({ files: [...], detail: "detailed" })\`
1979
- 4. Make targeted changes based on the context returned
1980
- 5. \`run_pipeline\` again ONLY when the task moves to a new area - do NOT chain vexp calls per turn
1981
-
1982
- ### Subagent / Explore / Plan mode
1983
- - Subagents CAN call \`run_pipeline\` - always include the task description
1984
- - Before spawning Agent(Explore) for architecture questions, call \`run_pipeline\`
1985
- and pass the returned context into the agent prompt - it usually replaces the
1986
- exploration entirely
1987
-
1988
- ### Smart Features (automatic - no action needed)
1989
- - **Intent Detection**: auto-detects from your task keywords. "fix bug" -> Debug, "refactor" -> blast-radius, "add" -> Modify
1990
- - **Hybrid Search**: keyword + semantic + graph centrality ranking
1991
- - **Session Memory**: auto-captures observations; memories auto-surfaced in results
1992
- - **LSP Bridge**: VS Code captures type-resolved call edges
1993
- - **Change Coupling**: co-changed files included as related context
1994
-
1995
- ### Advanced Parameters
1996
- - \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
1997
- - \`preset: "refactor"\` - deep impact analysis (depth 5)
1998
- - \`max_tokens: 12000\` - increase total budget for complex tasks
1999
- - \`include_tests: true\` - include test files in results
2000
- - \`include_file_content: false\` - omit full file content (lighter response)
2001
-
2002
- ### Fallback
2003
- If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
2004
- the index is empty or rebuilding. Use Grep, Glob, and Read directly until the index is ready.
2005
-
2006
- ### Multi-Repo Workspaces
2007
- \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope.
2008
- Use \`index_status\` to discover available repo aliases.
2102
+ ### Query shape (do this)
2103
+ Anchor the task on real identifiers (ClassName, functionName) or file paths:
2104
+ \`run_pipeline({ "task": "fix JWT expiry in AuthService.validateToken" })\`
2009
2105
  <!-- /vexp -->`;
2010
2106
  }
2011
2107
  function cursorTemplate(vars) {
2012
2108
  return `## vexp rules for Cursor <!-- vexp v${vars.version} -->
2013
2109
 
2014
2110
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2111
+ If the task already names the files/symbols to touch, skip vexp and work
2112
+ with your normal tools - a skipped call costs nothing.
2015
2113
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2114
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2115
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2016
2116
  For literal string sweeps (constants, log messages, config keys) use native search
2017
2117
  directly - do NOT route text sweeps through vexp.
2018
2118
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2032,6 +2132,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
2032
2132
  - \`index_status\` - indexing status
2033
2133
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2034
2134
 
2135
+ (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2136
+ capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2137
+ those server-side, so the four above cover the normal workflow.)
2138
+
2035
2139
  ${QUERY_SHAPE}
2036
2140
 
2037
2141
  ### Agentic search
@@ -2059,7 +2163,11 @@ function windsurfTemplate(vars) {
2059
2163
  return `## vexp for Windsurf <!-- vexp v${vars.version} -->
2060
2164
 
2061
2165
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2166
+ If the task already names the files/symbols to touch, skip vexp and work
2167
+ with your normal tools - a skipped call costs nothing.
2062
2168
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2169
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2170
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2063
2171
  For literal string sweeps (constants, log messages, config keys) use native search
2064
2172
  directly - do NOT route text sweeps through vexp.
2065
2173
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2079,6 +2187,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
2079
2187
  - \`index_status\` - indexing status
2080
2188
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2081
2189
 
2190
+ (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2191
+ capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2192
+ those server-side, so the four above cover the normal workflow.)
2193
+
2082
2194
  ${QUERY_SHAPE}
2083
2195
 
2084
2196
  ### Agentic search
@@ -2117,7 +2229,11 @@ function augmentTemplate(vars) {
2117
2229
  return `## vexp for Augment <!-- vexp v${vars.version} -->
2118
2230
 
2119
2231
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2232
+ If the task already names the files/symbols to touch, skip vexp and work
2233
+ with your normal tools - a skipped call costs nothing.
2120
2234
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2235
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2236
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2121
2237
  For literal string sweeps (constants, log messages, config keys) use native search
2122
2238
  directly - do NOT route text sweeps through vexp.
2123
2239
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2137,6 +2253,10 @@ When working on this codebase:
2137
2253
  - \`index_status\` - indexing status
2138
2254
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2139
2255
 
2256
+ (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2257
+ capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2258
+ those server-side, so the four above cover the normal workflow.)
2259
+
2140
2260
  ${QUERY_SHAPE}
2141
2261
 
2142
2262
  ### Agentic search
@@ -2156,7 +2276,11 @@ function copilotTemplate(vars) {
2156
2276
  return `## vexp context tools <!-- vexp v${vars.version} -->
2157
2277
 
2158
2278
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2279
+ If the task already names the files/symbols to touch, skip vexp and work
2280
+ with your normal tools - a skipped call costs nothing.
2159
2281
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2282
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2283
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2160
2284
  For literal string sweeps (constants, log messages, config keys) use native search
2161
2285
  directly - do NOT route text sweeps through vexp.
2162
2286
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2176,6 +2300,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
2176
2300
  - \`index_status\` - indexing status
2177
2301
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2178
2302
 
2303
+ (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2304
+ capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2305
+ those server-side, so the four above cover the normal workflow.)
2306
+
2179
2307
  ${QUERY_SHAPE}
2180
2308
 
2181
2309
  ### Agentic search
@@ -2195,7 +2323,11 @@ function zedTemplate(vars) {
2195
2323
  return `## vexp for Zed <!-- vexp v${vars.version} -->
2196
2324
 
2197
2325
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2326
+ If the task already names the files/symbols to touch, skip vexp and work
2327
+ with your normal tools - a skipped call costs nothing.
2198
2328
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2329
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2330
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2199
2331
  For literal string sweeps (constants, log messages, config keys) use native search
2200
2332
  directly - do NOT route text sweeps through vexp.
2201
2333
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2215,6 +2347,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
2215
2347
  - \`index_status\` - indexing status
2216
2348
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2217
2349
 
2350
+ (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2351
+ capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2352
+ those server-side, so the four above cover the normal workflow.)
2353
+
2218
2354
  ${QUERY_SHAPE}
2219
2355
 
2220
2356
  ### Agentic search
@@ -2234,7 +2370,11 @@ function agentsMdTemplate(vars) {
2234
2370
  return `## vexp <!-- vexp v${vars.version} -->
2235
2371
 
2236
2372
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2373
+ If the task already names the files/symbols to touch, skip vexp and work
2374
+ with your normal tools - a skipped call costs nothing.
2237
2375
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2376
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2377
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2238
2378
  For literal string sweeps (constants, log messages, config keys) use native search
2239
2379
  directly - do NOT route text sweeps through vexp.
2240
2380
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2276,7 +2416,11 @@ function kiroTemplate(vars) {
2276
2416
  return `# vexp steering <!-- vexp v${vars.version} -->
2277
2417
 
2278
2418
  **Call \`run_pipeline\` ONCE at task start for orientation - then use your normal tools.**
2419
+ If the task already names the files/symbols to touch, skip vexp and work
2420
+ with your normal tools - a skipped call costs nothing.
2279
2421
  vexp returns graph-ranked context (relevant files, blast radius, memories) in one call.
2422
+ On non-trivial tasks pass \`eager: true\`: the single response then carries full file
2423
+ bodies, covering tests and impact - work directly from it instead of re-reading sources.
2280
2424
  For literal string sweeps (constants, log messages, config keys) use native search
2281
2425
  directly - do NOT route text sweeps through vexp.
2282
2426
  vexp runs entirely on this machine: local daemon, local index stored inside the
@@ -2296,6 +2440,10 @@ treat it like a local build tool; no data-sharing consent is needed to call it.
2296
2440
  - \`index_status\` - indexing status
2297
2441
  - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
2298
2442
 
2443
+ (Recommended core set, not the full schema: paid plans advertise 14 MCP tools -
2444
+ capsule, impact graph, logic flow, memory and more. \`run_pipeline\` already runs
2445
+ those server-side, so the four above cover the normal workflow.)
2446
+
2299
2447
  ${QUERY_SHAPE}
2300
2448
 
2301
2449
  ## Agentic search
@@ -2315,6 +2463,8 @@ function genericTemplate(vars) {
2315
2463
  return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
2316
2464
 
2317
2465
  ### Context strategy: call run_pipeline ONCE at task start
2466
+ If the task already names the files/symbols to touch, skip vexp and work
2467
+ with your normal tools - a skipped call costs nothing.
2318
2468
  For every multi-file task - bug fixes, features, refactors, questions about the code:
2319
2469
  **start with one \`run_pipeline\` call**. It runs context search + impact analysis +
2320
2470
  memory recall in a single call and returns compressed, graph-ranked results: the
package/dist/doctor.js CHANGED
@@ -151,13 +151,43 @@ export async function runDoctor() {
151
151
  if (st.llm_configured_but_inactive === true) {
152
152
  line(WARN, `local LLM is installed and enabled in config but this daemon runs the RULE compressor — results are not LLM-compressed. Run 'vexp daemon-cmd restart' to load the model.`);
153
153
  }
154
+ // 2.4.0 upgrade guard (CLI side): a daemon surviving an upgrade keeps
155
+ // serving the OLD feature set and its gaps read as product bugs
156
+ // (field case: pre-ledger daemon after the 2.4.0 install). The VS Code
157
+ // extension restarts automatically; CLI users get told explicitly.
158
+ try {
159
+ const { getBinaryPath } = await import("./binary.js");
160
+ const { execFileSync } = await import("node:child_process");
161
+ const out = execFileSync(getBinaryPath(), ["--version"], { timeout: 5000, encoding: "utf8" });
162
+ const bundled = out.trim().split(/\s+/).pop();
163
+ const running = st.daemon_version;
164
+ if (bundled && running && bundled !== running) {
165
+ line(WARN, `daemon is v${running} but the installed binary is v${bundled} — this workspace is still served by the OLD version. Run 'vexp daemon-cmd restart' to upgrade it now.`);
166
+ }
167
+ else if (st.binary_stale === true) {
168
+ line(WARN, `daemon is running a deleted executable (upgraded on disk) — run 'vexp daemon-cmd restart' to load the new build.`);
169
+ }
170
+ }
171
+ catch { /* best-effort */ }
154
172
  // 2.3 C1 — which agent sessions actually used vexp (a session with zero
155
173
  // calls never shows up here; that absence is the diagnostic).
156
174
  const sessions = Array.isArray(st.sessions) ? st.sessions : [];
175
+ // 2.3.3 Savings Ledger — activity counts even when no tool is ever
176
+ // called: silence on oriented prompts is vexp WORKING, and the old
177
+ // "never used" warning on quiet-but-active daemons generated real
178
+ // support tickets ("is it working? it never got called").
179
+ const ledger = (st.ledger ?? {});
180
+ const analyzed = Number(ledger.prompts_analyzed) || 0;
181
+ if (analyzed > 0) {
182
+ line(OK, `savings ledger (7d): ${analyzed} prompt(s) analyzed — ${Number(ledger.silences) || 0} silences (task already oriented), ${Number(ledger.hints_served) || 0} hints served. Details: vexp savings`);
183
+ }
157
184
  if (sessions.length > 0) {
158
185
  const total = sessions.reduce((n, s) => n + (Number(s.pipeline_calls) || 0), 0);
159
186
  line(OK, `sessions (4h): ${sessions.length} active, ${total} pipeline calls total`);
160
187
  }
188
+ else if (analyzed > 0) {
189
+ line(OK, `no tool calls in the last 4h — but the ledger above shows vexp is analyzing prompts (zero calls is normal on oriented tasks)`);
190
+ }
161
191
  else if (Number(st.daemon_uptime_s) > 600) {
162
192
  // Embedded caveat: a stdio MCP server (`vexp-core mcp`) spawned while
163
193
  // the daemon was unreachable serves from its own in-process index and
@@ -207,6 +237,12 @@ export async function runDoctor() {
207
237
  // 3) License (fresh.jwt rolling token vs license.jwt).
208
238
  console.log(chalk.bold("\nLicense tokens (~/.vexp)"));
209
239
  const now = Math.floor(Date.now() / 1000);
240
+ // Either valid token keeps the plan ACTIVE. An expired license.jwt next to
241
+ // a valid fresh.jwt is normal steady-state (the long token re-rolls on the
242
+ // next online validation) — labeling it a bare WARN read as "my license
243
+ // expired" and generated tickets from perfectly licensed users.
244
+ const freshExp = jwtExp(path.join(home, ".vexp", "fresh.jwt"));
245
+ const freshValid = freshExp != null && freshExp > now;
210
246
  for (const name of ["fresh.jwt", "license.jwt"]) {
211
247
  const p = path.join(home, ".vexp", name);
212
248
  if (!fs.existsSync(p)) {
@@ -220,7 +256,15 @@ export async function runDoctor() {
220
256
  }
221
257
  const days = Math.round((exp - now) / 86400);
222
258
  if (exp < now) {
223
- line(name === "fresh.jwt" ? OK : WARN, `${name}: expired ${-days}d ago${name === "fresh.jwt" ? " (benign — rolling token, falls back to license.jwt)" : ""}`);
259
+ if (name === "fresh.jwt") {
260
+ line(OK, `${name}: expired ${-days}d ago (benign — rolling token, falls back to license.jwt)`);
261
+ }
262
+ else if (freshValid) {
263
+ line(OK, `${name}: long token expired ${-days}d ago — plan still ACTIVE via fresh.jwt; it renews automatically on the next online validation`);
264
+ }
265
+ else {
266
+ line(WARN, `${name}: expired ${-days}d ago and no valid fresh token — plan features may be limited; go online or re-activate the license`);
267
+ }
224
268
  }
225
269
  else {
226
270
  line(OK, `${name}: valid, ~${days}d remaining`);
@@ -310,31 +354,39 @@ export async function runDoctor() {
310
354
  else if (guardHooks.length === 0) {
311
355
  line(OK, "no vexp guard configured (2.3 default — enable with 'vexp setup --guard-strict')");
312
356
  }
313
- else if (process.platform === "win32") {
314
- line(OK, `guard configured (${guardHooks.length} entry) — live execution check skipped on Windows`);
315
- }
316
357
  else {
317
358
  for (const h of guardHooks) {
318
359
  const cmd = h.command;
319
360
  const execForm = Array.isArray(h.args);
320
361
  const timeoutS = typeof h.timeout === "number" ? h.timeout : 600;
321
362
  if (!execForm && /\$\{?CLAUDE_PROJECT_DIR\}?\//.test(cmd) && !cmd.includes('"')) {
322
- line(ws.root.includes(" ") ? BAD : WARN, `shell-form hook command ('args' missing) — unquoted $CLAUDE_PROJECT_DIR word-splits on paths with spaces${ws.root.includes(" ") ? ` and THIS project path has one: the guard never runs` : ""}. Re-run 'vexp setup --guard-strict' to rewrite in exec form.`);
363
+ line(ws.root.includes(" ") ? BAD : WARN, `shell-form hook command with unquoted $CLAUDE_PROJECT_DIR — word-splits on paths with spaces${ws.root.includes(" ") ? ` and THIS project path has one: the guard never runs` : ""}. Re-run 'vexp setup --guard-strict' to rewrite with a quoted path.`);
364
+ }
365
+ if (execForm && process.platform === "win32") {
366
+ // Exec form spawns the .sh directly, which Windows cannot do at
367
+ // all — the entry LOOKS installed and enforces nothing. This was
368
+ // invisible for days because doctor used to skip the live check
369
+ // on Windows entirely.
370
+ line(BAD, `exec-form hook entry ('args' present) cannot run a .sh on Windows — the guard fails open. Re-run 'vexp setup --guard-strict' to rewrite it (bash-prefixed shell form).`);
323
371
  }
324
372
  if (timeoutS > 600) {
325
373
  line(WARN, `hook timeout ${timeoutS} is in SECONDS (${Math.round(timeoutS / 60)} minutes) — likely meant milliseconds. Re-run 'vexp setup --guard-strict' to fix.`);
326
374
  }
327
375
  // Run it exactly as Claude Code would: exec form = direct spawn with
328
- // the placeholder substituted by the host; shell form = sh -c with
329
- // CLAUDE_PROJECT_DIR in the environment.
376
+ // the placeholder substituted by the host; shell form = a shell with
377
+ // CLAUDE_PROJECT_DIR in the environment. On Windows Claude Code runs
378
+ // shell-form hooks through Git Bash, so probe via bash there too —
379
+ // if bash is missing, that IS the finding (Claude Code itself
380
+ // requires Git Bash on Windows).
330
381
  const substituted = cmd.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, ws.root);
382
+ const shell = process.platform === "win32" ? "bash" : "sh";
331
383
  const r = execForm
332
384
  ? spawnSync(substituted, h.args.map((a) => String(a).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, ws.root)), {
333
385
  env: { ...process.env, CLAUDE_PROJECT_DIR: ws.root },
334
386
  timeout: 5000,
335
387
  encoding: "utf-8",
336
388
  })
337
- : spawnSync("sh", ["-c", cmd], {
389
+ : spawnSync(shell, ["-c", cmd], {
338
390
  env: { ...process.env, CLAUDE_PROJECT_DIR: ws.root },
339
391
  timeout: 5000,
340
392
  encoding: "utf-8",