vexp-cli 2.0.19 → 2.0.21

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.
@@ -16,7 +16,7 @@ import { VEXP_GUARD_HOOK } from "./hook-template.js";
16
16
  // ---------------------------------------------------------------------------
17
17
  const VEXP_MARKER_RE = /<!-- vexp(?:\s+v[\d.]+)? -->/;
18
18
  const VEXP_MARKER_END = "<!-- /vexp -->";
19
- // Matches LEGACY vexp entry keys ONLY "vexp-mcp", "vexp-core", "vexp-stdio",
19
+ // Matches LEGACY vexp entry keys ONLY - "vexp-mcp", "vexp-core", "vexp-stdio",
20
20
  // "vexp_old", etc. Explicitly excludes the canonical "vexp" key so that idempotency
21
21
  // checks (command+args+env identical?) work: we only remove siblings, not the entry
22
22
  // we're about to upsert.
@@ -73,10 +73,10 @@ function logRemoval(removed) {
73
73
  if (removed.length === 0)
74
74
  return;
75
75
  for (const entry of removed) {
76
- process.stdout.write(` · Removed legacy entry: ${entry}\n`);
76
+ process.stdout.write(` - Removed legacy entry: ${entry}\n`);
77
77
  }
78
78
  }
79
- // All vexp MCP tools (all read-only safe to pre-approve).
79
+ // All vexp MCP tools (all read-only - safe to pre-approve).
80
80
  const VEXP_TOOLS = [
81
81
  "run_pipeline",
82
82
  "get_context_capsule",
@@ -92,7 +92,7 @@ const VEXP_TOOLS = [
92
92
  "get_token_savings",
93
93
  ];
94
94
  // ---------------------------------------------------------------------------
95
- // Agent detectors mirrors VS Code extension's AGENT_DETECTORS
95
+ // Agent detectors - mirrors VS Code extension's AGENT_DETECTORS
96
96
  // ---------------------------------------------------------------------------
97
97
  const AGENT_DETECTORS = [
98
98
  {
@@ -396,8 +396,104 @@ export function configureSelectedAgents(workspaceRoot, binaryPath, version, sele
396
396
  return { agents: results, mcpConfigs };
397
397
  }
398
398
  // ---------------------------------------------------------------------------
399
- // File helpers ported from agent-auto-config.ts
399
+ // File helpers - ported from agent-auto-config.ts
400
400
  // ---------------------------------------------------------------------------
401
+ /**
402
+ * Parse JSON that may use JSONC syntax - `//` and slash-star comments and
403
+ * trailing commas. VS Code mcp.json and Zed settings.json are JSONC, so a raw
404
+ * JSON.parse throws on perfectly valid user files. Strips comments and trailing
405
+ * commas (preserving string contents) before parsing.
406
+ */
407
+ export function parseJsonc(text) {
408
+ let out = "";
409
+ let inString = false;
410
+ let inLineComment = false;
411
+ let inBlockComment = false;
412
+ for (let i = 0; i < text.length; i++) {
413
+ const c = text[i];
414
+ const next = text[i + 1];
415
+ if (inLineComment) {
416
+ if (c === "\n") {
417
+ inLineComment = false;
418
+ out += c;
419
+ }
420
+ continue;
421
+ }
422
+ if (inBlockComment) {
423
+ if (c === "*" && next === "/") {
424
+ inBlockComment = false;
425
+ i++;
426
+ }
427
+ continue;
428
+ }
429
+ if (inString) {
430
+ out += c;
431
+ if (c === "\\") {
432
+ out += next ?? "";
433
+ i++;
434
+ }
435
+ else if (c === '"')
436
+ inString = false;
437
+ continue;
438
+ }
439
+ if (c === '"') {
440
+ inString = true;
441
+ out += c;
442
+ continue;
443
+ }
444
+ if (c === "/" && next === "/") {
445
+ inLineComment = true;
446
+ i++;
447
+ continue;
448
+ }
449
+ if (c === "/" && next === "*") {
450
+ inBlockComment = true;
451
+ i++;
452
+ continue;
453
+ }
454
+ out += c;
455
+ }
456
+ out = out.replace(/,(\s*[}\]])/g, "$1");
457
+ return JSON.parse(out);
458
+ }
459
+ /**
460
+ * Read a JSON/JSONC config file without ever risking user data. When the file
461
+ * exists but cannot be parsed (even as JSONC), returns ok:false so callers skip
462
+ * the write instead of silently clobbering the user's config.
463
+ */
464
+ export function readJsonConfigSafe(filePath) {
465
+ if (!fs.existsSync(filePath))
466
+ return { data: {}, ok: true, existed: false };
467
+ let raw;
468
+ try {
469
+ raw = fs.readFileSync(filePath, "utf-8");
470
+ }
471
+ catch {
472
+ return { data: {}, ok: false, existed: true };
473
+ }
474
+ if (raw.trim() === "")
475
+ return { data: {}, ok: true, existed: true };
476
+ try {
477
+ const parsed = parseJsonc(raw);
478
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
479
+ return { data: parsed, ok: true, existed: true };
480
+ }
481
+ }
482
+ catch { /* fall through to ok:false */ }
483
+ return { data: {}, ok: false, existed: true };
484
+ }
485
+ /** Copy an existing config to <file>.vexp-bak before overwriting it. */
486
+ function backupConfig(filePath) {
487
+ try {
488
+ if (fs.existsSync(filePath))
489
+ fs.copyFileSync(filePath, `${filePath}.vexp-bak`);
490
+ }
491
+ catch { /* non-fatal */ }
492
+ }
493
+ /** Warn that we refused to touch an unparseable config file. */
494
+ function warnUnparseable(filePath) {
495
+ process.stderr.write(` [!] ${filePath} could not be parsed - leaving it untouched. Fix the file or add vexp manually, then re-run setup.\n`);
496
+ }
401
497
  function appendOrCreate(filePath, content, version) {
402
498
  if (!fs.existsSync(filePath)) {
403
499
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -422,7 +518,7 @@ function appendOrCreate(filePath, content, version) {
422
518
  if (endIdx !== -1) {
423
519
  const existingSection = existing.substring(startIdx, endIdx + VEXP_MARKER_END.length);
424
520
  if (existingSection === content)
425
- return "skipped"; // truly identical no write
521
+ return "skipped"; // truly identical -> no write
426
522
  const before = existing.substring(0, startIdx);
427
523
  const after = existing.substring(endIdx + VEXP_MARKER_END.length);
428
524
  fs.writeFileSync(filePath, before + content + after, "utf-8");
@@ -433,16 +529,13 @@ function appendOrCreate(filePath, content, version) {
433
529
  }
434
530
  return "updated";
435
531
  }
436
- function mergeJsonConfig(filePath, mergePayload, version) {
437
- let existing = {};
438
- if (fs.existsSync(filePath)) {
439
- try {
440
- existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
441
- }
442
- catch {
443
- // Malformed JSON: overwrite
444
- }
532
+ export function mergeJsonConfig(filePath, mergePayload, version) {
533
+ const read = readJsonConfigSafe(filePath);
534
+ if (!read.ok) {
535
+ warnUnparseable(filePath);
536
+ return "skipped";
445
537
  }
538
+ const existing = read.data;
446
539
  // Build the merged result first, then compare to avoid unnecessary writes.
447
540
  const merged = { ...existing };
448
541
  for (const [key, value] of Object.entries(mergePayload)) {
@@ -461,32 +554,31 @@ function mergeJsonConfig(filePath, mergePayload, version) {
461
554
  }
462
555
  delete merged.__vexp;
463
556
  merged.__vexp_version = version;
464
- // Compare serialized output skip if truly identical
557
+ // Compare serialized output - skip if truly identical
465
558
  const mergedStr = JSON.stringify(merged, null, 2);
466
559
  const existingStr = JSON.stringify(existing, null, 2);
467
560
  if (mergedStr === existingStr)
468
561
  return "skipped";
469
562
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
563
+ if (read.existed)
564
+ backupConfig(filePath);
470
565
  fs.writeFileSync(filePath, mergedStr, "utf-8");
471
566
  return "merged";
472
567
  }
473
568
  // ---------------------------------------------------------------------------
474
- // MCP config writers adapted for Rust binary path
569
+ // MCP config writers - adapted for Rust binary path
475
570
  // ---------------------------------------------------------------------------
476
571
  /**
477
572
  * Write MCP config JSON for agents that use mcpServers format (Cursor, Windsurf).
478
573
  * Returns true if a new entry was written.
479
574
  */
480
- function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
481
- let existing = {};
482
- if (fs.existsSync(mcpConfigPath)) {
483
- try {
484
- existing = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
485
- }
486
- catch {
487
- // Malformed JSON: overwrite
488
- }
575
+ export function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, workspaceRoot) {
576
+ const read = readJsonConfigSafe(mcpConfigPath);
577
+ if (!read.ok) {
578
+ warnUnparseable(mcpConfigPath);
579
+ return false;
489
580
  }
581
+ const existing = read.data;
490
582
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
491
583
  const targetCmd = useNode ? "node" : binaryPath;
492
584
  const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
@@ -517,6 +609,8 @@ function writeMcpConfig(mcpConfigPath, binaryPath, alwaysAllow, mcpServerPath, w
517
609
  };
518
610
  existing.mcpServers = servers;
519
611
  fs.mkdirSync(path.dirname(mcpConfigPath), { recursive: true });
612
+ if (read.existed)
613
+ backupConfig(mcpConfigPath);
520
614
  fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), "utf-8");
521
615
  return true;
522
616
  }
@@ -561,49 +655,124 @@ function readMcpPort(workspaceRoot) {
561
655
  }
562
656
  return DEFAULT_PORT;
563
657
  }
658
+ // Marker placed inside a vexp-generated [mcp_servers.vexp] section so we can
659
+ // tell OUR config apart from a config the user hand-wrote. Must stay in sync
660
+ // with packages/vexp-vscode/src/providers/agent-auto-config.ts.
661
+ const CODEX_MANAGED_MARKER = "# vexp-managed";
662
+ /** Quote a value as a TOML literal string (single quotes, no escaping) so
663
+ * Windows paths work verbatim. Falls back to a basic escaped string if the
664
+ * value contains a single quote. */
665
+ function tomlString(value) {
666
+ if (!value.includes("'"))
667
+ return `'${value}'`;
668
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
669
+ }
670
+ /** Extract the existing canonical [mcp_servers.vexp] block (incl. subsections). */
671
+ function extractCodexVexpSection(content) {
672
+ const m = content.match(CANONICAL_VEXP_TOML_SECTION_RE);
673
+ return m ? m.join("\n") : "";
674
+ }
675
+ /** True when the existing [mcp_servers.vexp] section was written by the user
676
+ * (not by vexp) and must NOT be overwritten. */
677
+ function isUserManagedCodexSection(section) {
678
+ if (!section)
679
+ return false;
680
+ if (section.includes(CODEX_MANAGED_MARKER))
681
+ return false; // ours
682
+ if (/^\s*command\s*=/m.test(section))
683
+ return true; // hand-written direct config
684
+ const localUrl = /^\s*url\s*=\s*"https?:\/\/127\.0\.0\.1[:/]/m.test(section);
685
+ const anyUrl = /^\s*url\s*=/m.test(section);
686
+ if (anyUrl && !localUrl)
687
+ return true; // points elsewhere
688
+ return false;
689
+ }
690
+ /** Build the canonical [mcp_servers.vexp] section for the chosen transport.
691
+ * Returns null for `direct` when no usable core binary is available. */
692
+ function buildCodexSection(opts) {
693
+ if (opts.transport === "direct") {
694
+ if (!opts.coreBinaryPath)
695
+ return null;
696
+ const lines = [
697
+ "",
698
+ "[mcp_servers.vexp]",
699
+ `${CODEX_MANAGED_MARKER}: direct transport (set VEXP_CODEX_TRANSPORT=http for the HTTP supervisor)`,
700
+ `command = ${tomlString(opts.coreBinaryPath)}`,
701
+ `args = ["mcp"]`,
702
+ ];
703
+ if (opts.workspaceRoot)
704
+ lines.push(`cwd = ${tomlString(opts.workspaceRoot)}`);
705
+ lines.push("tool_timeout_sec = 120", "", "[mcp_servers.vexp.env]");
706
+ if (opts.workspaceRoot)
707
+ lines.push(`VEXP_WORKSPACE = ${tomlString(opts.workspaceRoot)}`);
708
+ if (opts.home)
709
+ lines.push(`VEXP_HOME = ${tomlString(opts.home)}`);
710
+ lines.push("");
711
+ return lines.join("\n");
712
+ }
713
+ const wsHash = opts.workspaceRoot ? workspaceHash(opts.workspaceRoot) : "";
714
+ const urlPath = wsHash ? `/ws/${wsHash}/mcp` : "/mcp";
715
+ const desiredUrl = `http://127.0.0.1:${opts.mcpPort}${urlPath}`;
716
+ return `\n[mcp_servers.vexp]\n${CODEX_MANAGED_MARKER}: http transport (set VEXP_CODEX_TRANSPORT=direct for stdio)\nurl = "${desiredUrl}"\ntool_timeout_sec = 120\n\n[mcp_servers.vexp.http_headers]\nAuthorization = "Bearer ${opts.token}"\n`;
717
+ }
564
718
  /**
565
719
  * Configure MCP in ~/.codex/config.toml (global).
566
720
  * Codex only reads the global config, not project-level .codex/config.toml.
567
- * Uses HTTP transport with bearer token so Codex shows "Auth: BearerToken"
568
- * instead of "Auth: Unsupported" (which is hardcoded for stdio servers).
569
- * Uses TOML format with [mcp_servers.<name>] sections.
721
+ *
722
+ * Default transport is `direct` (stdio: `vexp-core mcp`), which works with AND
723
+ * without VS Code/daemon: `vexp-core mcp` auto-proxies to a running daemon when
724
+ * reachable and otherwise runs an embedded in-process index. Override with the
725
+ * VEXP_CODEX_TRANSPORT env var ("direct" | "http").
726
+ *
727
+ * Never clobbers a config the user wrote by hand.
570
728
  */
571
- function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
729
+ export function configureCodexGlobal(binaryPath, _mcpServerPath, workspaceRoot) {
572
730
  const home = os.homedir();
573
731
  if (!home)
574
732
  return false;
575
733
  const configPath = path.join(home, ".codex", "config.toml");
576
734
  const mcpPort = readMcpPort(workspaceRoot);
577
735
  const token = resolveCodexToken();
578
- // Per-workspace URL path lets the single MCP server on :port route
579
- // each codex request to the correct daemon in multi-workspace setups.
580
- // Falls back to /mcp if no workspace is known (e.g. global bootstrap).
581
- const wsHash = workspaceRoot ? workspaceHash(workspaceRoot) : "";
582
- const urlPath = wsHash ? `/ws/${wsHash}/mcp` : "/mcp";
583
- const desiredUrl = `http://127.0.0.1:${mcpPort}${urlPath}`;
584
- const newSection = `\n[mcp_servers.vexp]\nurl = "${desiredUrl}"\ntool_timeout_sec = 120\n\n[mcp_servers.vexp.http_headers]\nAuthorization = "Bearer ${token}"\n`;
736
+ const requested = (process.env.VEXP_CODEX_TRANSPORT ?? "direct").toLowerCase();
737
+ let transport = requested === "http" ? "http" : "direct";
738
+ if (transport === "direct" && !(binaryPath && fs.existsSync(binaryPath))) {
739
+ transport = "http";
740
+ }
585
741
  let content = "";
586
- if (fs.existsSync(configPath)) {
742
+ if (fs.existsSync(configPath))
587
743
  content = fs.readFileSync(configPath, "utf-8");
588
- }
744
+ const original = content;
589
745
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
590
746
  // Step 1: strip LEGACY vexp-* sections (not the canonical one).
591
747
  const beforeLegacy = content.length;
592
748
  content = content.replace(LEGACY_VEXP_TOML_SECTION_RE, "");
593
- const legacyStripped = content.length !== beforeLegacy;
594
- if (legacyStripped) {
595
- process.stdout.write(` · Removed legacy codex entries from ${configPath}\n`);
749
+ if (content.length !== beforeLegacy) {
750
+ process.stdout.write(` - Removed legacy codex entries from ${configPath}\n`);
596
751
  }
597
- // Step 2: check if the canonical section already has the desired URL+token.
598
- // If yes and no legacy was stripped, this is a true no-op.
599
- const canonicalOk = content.includes(`url = "${desiredUrl}"`) &&
600
- content.includes(`Authorization = "Bearer ${token}"`);
601
- if (canonicalOk && !legacyStripped) {
752
+ // Step 2: no-clobber leave a hand-written [mcp_servers.vexp] alone.
753
+ const existingSection = extractCodexVexpSection(content);
754
+ if (isUserManagedCodexSection(existingSection)) {
755
+ if (content !== original) {
756
+ backupConfig(configPath);
757
+ fs.writeFileSync(configPath, content, "utf-8");
758
+ return true;
759
+ }
602
760
  return false;
603
761
  }
604
- // Step 3: rewrite canonical section (strip it if present, then append).
605
- content = content.replace(CANONICAL_VEXP_TOML_SECTION_RE, "");
606
- content += newSection;
762
+ const newSection = buildCodexSection({ transport, coreBinaryPath: binaryPath, workspaceRoot, mcpPort, token, home });
763
+ if (!newSection) {
764
+ if (content !== original) {
765
+ backupConfig(configPath);
766
+ fs.writeFileSync(configPath, content, "utf-8");
767
+ return true;
768
+ }
769
+ return false;
770
+ }
771
+ // Step 3: rewrite the canonical section.
772
+ content = content.replace(CANONICAL_VEXP_TOML_SECTION_RE, "") + newSection;
773
+ if (content === original)
774
+ return false;
775
+ backupConfig(configPath);
607
776
  fs.writeFileSync(configPath, content, "utf-8");
608
777
  return true;
609
778
  }
@@ -611,16 +780,13 @@ function configureCodexGlobal(binaryPath, mcpServerPath, workspaceRoot) {
611
780
  * Write .vscode/mcp.json for GitHub Copilot.
612
781
  * VS Code uses "servers" (not "mcpServers") + "type": "stdio".
613
782
  */
614
- function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
615
- let existing = {};
616
- if (fs.existsSync(p)) {
617
- try {
618
- existing = JSON.parse(fs.readFileSync(p, "utf-8"));
619
- }
620
- catch {
621
- // Malformed JSON: overwrite
622
- }
783
+ export function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
784
+ const read = readJsonConfigSafe(p);
785
+ if (!read.ok) {
786
+ warnUnparseable(p);
787
+ return false;
623
788
  }
789
+ const existing = read.data;
624
790
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
625
791
  const targetCmd = useNode ? "node" : binaryPath;
626
792
  const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
@@ -645,22 +811,21 @@ function writeVsCodeMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
645
811
  };
646
812
  existing.servers = servers;
647
813
  fs.mkdirSync(path.dirname(p), { recursive: true });
814
+ if (read.existed)
815
+ backupConfig(p);
648
816
  fs.writeFileSync(p, JSON.stringify(existing, null, 2), "utf-8");
649
817
  return true;
650
818
  }
651
819
  /**
652
820
  * Write context_servers in .zed/settings.json for Zed.
653
821
  */
654
- function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
655
- let settings = {};
656
- if (fs.existsSync(p)) {
657
- try {
658
- settings = JSON.parse(fs.readFileSync(p, "utf-8"));
659
- }
660
- catch {
661
- // Malformed JSON: overwrite
662
- }
822
+ export function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
823
+ const read = readJsonConfigSafe(p);
824
+ if (!read.ok) {
825
+ warnUnparseable(p);
826
+ return false;
663
827
  }
828
+ const settings = read.data;
664
829
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
665
830
  const targetCmd = useNode ? "node" : binaryPath;
666
831
  const targetArgs = useNode ? [mcpServerPath] : ["mcp"];
@@ -694,6 +859,8 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
694
859
  }
695
860
  settings.agent = { ...agent, tool_permissions: { ...tp, tools } };
696
861
  fs.mkdirSync(path.dirname(p), { recursive: true });
862
+ if (read.existed)
863
+ backupConfig(p);
697
864
  fs.writeFileSync(p, JSON.stringify(settings, null, 2), "utf-8");
698
865
  return true;
699
866
  }
@@ -701,20 +868,17 @@ function writeZedMcpConfig(p, binaryPath, mcpServerPath, workspaceRoot) {
701
868
  * Configure vexp MCP in ~/.claude.json (user-scope) using the Rust binary.
702
869
  * Returns true if config was written/updated.
703
870
  */
704
- function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
871
+ export function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
705
872
  const home = os.homedir();
706
873
  if (!home)
707
874
  return false;
708
875
  const configPath = path.join(home, ".claude.json");
709
- let config = {};
710
- if (fs.existsSync(configPath)) {
711
- try {
712
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
713
- }
714
- catch {
715
- // Malformed JSON: overwrite
716
- }
876
+ const read = readJsonConfigSafe(configPath);
877
+ if (!read.ok) {
878
+ warnUnparseable(configPath);
879
+ return false;
717
880
  }
881
+ const config = read.data;
718
882
  const beforeServers = config.mcpServers;
719
883
  const existing = beforeServers?.["vexp"];
720
884
  const useNode = mcpServerPath && fs.existsSync(mcpServerPath);
@@ -736,18 +900,20 @@ function configureClaudeCodeGlobal(binaryPath, mcpServerPath, workspaceRoot) {
736
900
  ...(desiredEnv ? { env: desiredEnv } : {}),
737
901
  };
738
902
  config.mcpServers = servers;
903
+ if (read.existed)
904
+ backupConfig(configPath);
739
905
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
740
906
  return true;
741
907
  }
742
908
  // ---------------------------------------------------------------------------
743
- // Claude Code PreToolUse hook blocks Grep/Glob when daemon is available
909
+ // Claude Code PreToolUse hook - blocks Grep/Glob when daemon is available
744
910
  // ---------------------------------------------------------------------------
745
911
  /**
746
912
  * Install the vexp-guard hook for Claude Code.
747
913
  * Creates .claude/hooks/vexp-guard.sh and merges hook config into .claude/settings.json.
748
914
  * Returns the action taken, or null if nothing was written.
749
915
  */
750
- function installClaudeCodeHook(workspaceRoot) {
916
+ export function installClaudeCodeHook(workspaceRoot) {
751
917
  const hookDir = path.join(workspaceRoot, ".claude", "hooks");
752
918
  const hookPath = path.join(hookDir, "vexp-guard.sh");
753
919
  const settingsPath = path.join(workspaceRoot, ".claude", "settings.json");
@@ -757,19 +923,16 @@ function installClaudeCodeHook(workspaceRoot) {
757
923
  if (existed) {
758
924
  const current = fs.readFileSync(hookPath, "utf-8");
759
925
  if (current === VEXP_GUARD_HOOK)
760
- return null; // identical skip
926
+ return null; // identical - skip
761
927
  }
762
928
  fs.writeFileSync(hookPath, VEXP_GUARD_HOOK, { mode: 0o755 });
763
929
  // 2. Merge hook config into .claude/settings.json
764
- let settings = {};
765
- if (fs.existsSync(settingsPath)) {
766
- try {
767
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
768
- }
769
- catch {
770
- // Malformed JSON: overwrite
771
- }
930
+ const read = readJsonConfigSafe(settingsPath);
931
+ if (!read.ok) {
932
+ warnUnparseable(settingsPath);
933
+ return existed ? "updated" : "created";
772
934
  }
935
+ const settings = read.data;
773
936
  const hooks = (settings.hooks ?? {});
774
937
  const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
775
938
  // Aggressively remove ALL vexp-related hook entries (old format, stale matchers, malformed)
@@ -806,6 +969,8 @@ function installClaudeCodeHook(workspaceRoot) {
806
969
  ],
807
970
  });
808
971
  settings.hooks = { ...hooks, PreToolUse: filtered };
972
+ if (read.existed)
973
+ backupConfig(settingsPath);
809
974
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
810
975
  return existed ? "updated" : "created";
811
976
  }
@@ -834,13 +999,13 @@ function generateAgentConfig(template, vars) {
834
999
  }
835
1000
  }
836
1001
  // ---------------------------------------------------------------------------
837
- // Templates copied verbatim from agent-auto-config.ts
1002
+ // Templates - copied verbatim from agent-auto-config.ts
838
1003
  // ---------------------------------------------------------------------------
839
1004
  function claudeCodeTemplate(vars) {
840
- return `## vexp Context-Aware AI Coding <!-- vexp v${vars.version} -->
1005
+ return `## vexp - Context-Aware AI Coding <!-- vexp v${vars.version} -->
841
1006
 
842
- ### MANDATORY: use vexp pipeline do NOT grep or glob the codebase
843
- For every task bug fixes, features, refactors, debugging:
1007
+ ### MANDATORY: use vexp pipeline - do NOT grep or glob the codebase
1008
+ For every task - bug fixes, features, refactors, debugging:
844
1009
  **call \`run_pipeline\` FIRST**. It executes context search + impact analysis +
845
1010
  memory recall in a single call, returning compressed results.
846
1011
 
@@ -851,47 +1016,47 @@ inspect files (detail: minimal/standard/detailed, 70-90% token savings).
851
1016
  Only use Read when you need exact raw content to edit a specific line.
852
1017
 
853
1018
  ### Primary Tool
854
- - \`run_pipeline\` **USE THIS FOR EVERYTHING**. Single call that runs
1019
+ - \`run_pipeline\` - **USE THIS FOR EVERYTHING**. Single call that runs
855
1020
  capsule + impact + memory server-side. Returns compressed results.
856
1021
  Auto-detects intent (debug/modify/refactor/explore) from your task.
857
1022
  Includes full file content for pivots.
858
1023
  Examples:
859
- - \`run_pipeline({ "task": "fix JWT validation bug" })\` auto-detect
860
- - \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\` explicit
861
- - \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` save insight in same call
1024
+ - \`run_pipeline({ "task": "fix JWT validation bug" })\` - auto-detect
1025
+ - \`run_pipeline({ "task": "refactor db layer", "preset": "refactor" })\` - explicit
1026
+ - \`run_pipeline({ "task": "add auth", "observation": "using JWT" })\` - save insight in same call
862
1027
 
863
1028
  ### Other MCP tools (use only when run_pipeline is insufficient)
864
- - \`get_skeleton\` **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings)
865
- - \`index_status\` indexing status and health check
866
- - \`expand_vexp_ref\` expand V-REF hash placeholders in v2 compact output
1029
+ - \`get_skeleton\` - **preferred over Read** for inspecting files (minimal/standard/detailed detail levels, 70-90% token savings)
1030
+ - \`index_status\` - indexing status and health check
1031
+ - \`expand_vexp_ref\` - expand V-REF hash placeholders in v2 compact output
867
1032
 
868
1033
  ### Workflow
869
- 1. \`run_pipeline("your task")\` ALWAYS FIRST. Returns pivots + impact + memories in 1 call
870
- 2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` avoid Read unless editing
1034
+ 1. \`run_pipeline("your task")\` - ALWAYS FIRST. Returns pivots + impact + memories in 1 call
1035
+ 2. Need more detail on a file? Use \`get_skeleton({ files: [...], detail: "detailed" })\` - avoid Read unless editing
871
1036
  3. Make targeted changes based on the context returned
872
1037
  4. \`run_pipeline\` again ONLY if you need more context during implementation
873
- 5. Do NOT chain multiple vexp calls one \`run_pipeline\` replaces capsule + impact + memory + observation
1038
+ 5. Do NOT chain multiple vexp calls - one \`run_pipeline\` replaces capsule + impact + memory + observation
874
1039
 
875
1040
  ### Subagent / Explore / Plan mode
876
- - Subagents CAN and MUST call \`run_pipeline\` always include the task description
1041
+ - Subagents CAN and MUST call \`run_pipeline\` - always include the task description
877
1042
  - The PreToolUse hook blocks Grep/Glob when vexp daemon is running
878
- - Do NOT spawn Agent(Explore) to freely search call \`run_pipeline\` first,
1043
+ - Do NOT spawn Agent(Explore) to freely search - call \`run_pipeline\` first,
879
1044
  then pass the returned context into the agent prompt if needed
880
- - Always: \`run_pipeline\` get context spawn agent with context
1045
+ - Always: \`run_pipeline\` -> get context -> spawn agent with context
881
1046
 
882
- ### Smart Features (automatic no action needed)
883
- - **Intent Detection**: auto-detects from your task keywords. "fix bug" Debug, "refactor" blast-radius, "add" Modify
1047
+ ### Smart Features (automatic - no action needed)
1048
+ - **Intent Detection**: auto-detects from your task keywords. "fix bug" -> Debug, "refactor" -> blast-radius, "add" -> Modify
884
1049
  - **Hybrid Search**: keyword + semantic + graph centrality ranking
885
1050
  - **Session Memory**: auto-captures observations; memories auto-surfaced in results
886
1051
  - **LSP Bridge**: VS Code captures type-resolved call edges
887
1052
  - **Change Coupling**: co-changed files included as related context
888
1053
 
889
1054
  ### Advanced Parameters
890
- - \`preset: "debug"\` forces debug mode (capsule+tests+impact+memory)
891
- - \`preset: "refactor"\` deep impact analysis (depth 5)
892
- - \`max_tokens: 12000\` increase total budget for complex tasks
893
- - \`include_tests: true\` include test files in results
894
- - \`include_file_content: false\` omit full file content (lighter response)
1055
+ - \`preset: "debug"\` - forces debug mode (capsule+tests+impact+memory)
1056
+ - \`preset: "refactor"\` - deep impact analysis (depth 5)
1057
+ - \`max_tokens: 12000\` - increase total budget for complex tasks
1058
+ - \`include_tests: true\` - include test files in results
1059
+ - \`include_file_content: false\` - omit full file content (lighter response)
895
1060
 
896
1061
  ### Fallback
897
1062
  If \`run_pipeline\` returns \`status: "degraded"\` or 0 pivots with an INDEX EMPTY warning,
@@ -905,28 +1070,28 @@ Use \`index_status\` to discover available repo aliases.
905
1070
  function cursorTemplate(vars) {
906
1071
  return `## vexp rules for Cursor <!-- vexp v${vars.version} -->
907
1072
 
908
- **MANDATORY: use \`run_pipeline\` do NOT grep, glob, or read files manually.**
1073
+ **MANDATORY: use \`run_pipeline\` - do NOT grep, glob, or read files manually.**
909
1074
  vexp returns pre-indexed, graph-ranked context in a single call.
910
1075
 
911
1076
  ### Workflow
912
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1077
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
913
1078
  2. Make targeted changes based on the context returned
914
1079
  3. \`run_pipeline\` again only if you need more context
915
1080
 
916
1081
  ### Available MCP tools
917
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1082
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
918
1083
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
919
- - \`get_skeleton\` compact file structure
920
- - \`index_status\` indexing status
921
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1084
+ - \`get_skeleton\` - compact file structure
1085
+ - \`index_status\` - indexing status
1086
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
922
1087
 
923
1088
  ### Agentic search
924
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1089
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
925
1090
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
926
1091
  rather than letting them search the codebase independently
927
1092
 
928
1093
  ### Tips
929
- - Natural language queries work well vexp uses hybrid search (keyword + semantic + graph)
1094
+ - Natural language queries work well - vexp uses hybrid search (keyword + semantic + graph)
930
1095
  - Add \`include_tests: true\` when debugging
931
1096
  - Use \`preset: "refactor"\` for deep impact analysis
932
1097
 
@@ -938,29 +1103,29 @@ Use file search and read tools directly until the index is ready.
938
1103
  \`run_pipeline\` auto-queries all indexed repos. Use \`repos: ["alias"]\` to scope. Run \`index_status\` to see aliases.
939
1104
 
940
1105
  > **Tip:** To avoid confirmation prompts on every tool call, enable Yolo mode:
941
- > Cursor Settings Features Agent Enable Yolo Mode.
1106
+ > Cursor Settings -> Features -> Agent -> Enable Yolo Mode.
942
1107
  <!-- /vexp -->`;
943
1108
  }
944
1109
  function windsurfTemplate(vars) {
945
1110
  return `## vexp for Windsurf <!-- vexp v${vars.version} -->
946
1111
 
947
- **MANDATORY: use \`run_pipeline\` do NOT grep, glob, or read files manually.**
1112
+ **MANDATORY: use \`run_pipeline\` - do NOT grep, glob, or read files manually.**
948
1113
  vexp returns pre-indexed, graph-ranked context in a single call.
949
1114
 
950
1115
  ### Workflow
951
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1116
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
952
1117
  2. Make targeted changes based on the context returned
953
1118
  3. \`run_pipeline\` again only if you need more context
954
1119
 
955
1120
  ### Available MCP tools
956
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1121
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
957
1122
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
958
- - \`get_skeleton\` compact file structure
959
- - \`index_status\` indexing status
960
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1123
+ - \`get_skeleton\` - compact file structure
1124
+ - \`index_status\` - indexing status
1125
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
961
1126
 
962
1127
  ### Agentic search
963
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1128
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
964
1129
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
965
1130
  rather than letting them search the codebase independently
966
1131
 
@@ -993,23 +1158,23 @@ function continueTemplate(vars) {
993
1158
  function augmentTemplate(vars) {
994
1159
  return `## vexp for Augment <!-- vexp v${vars.version} -->
995
1160
 
996
- **MANDATORY: use \`run_pipeline\` do NOT grep, glob, or read files manually.**
1161
+ **MANDATORY: use \`run_pipeline\` - do NOT grep, glob, or read files manually.**
997
1162
  vexp returns pre-indexed, graph-ranked context in a single call.
998
1163
 
999
1164
  When working on this codebase:
1000
- 1. \`run_pipeline\` with task description ALWAYS FIRST
1165
+ 1. \`run_pipeline\` with task description - ALWAYS FIRST
1001
1166
  2. Make targeted changes based on the context returned
1002
1167
  3. \`run_pipeline\` again only if you need more context
1003
1168
 
1004
1169
  ### Available MCP tools
1005
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1170
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1006
1171
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
1007
- - \`get_skeleton\` token-efficient file structure
1008
- - \`index_status\` indexing status
1009
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1172
+ - \`get_skeleton\` - token-efficient file structure
1173
+ - \`index_status\` - indexing status
1174
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1010
1175
 
1011
1176
  ### Agentic search
1012
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1177
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1013
1178
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1014
1179
  rather than letting them search the codebase independently
1015
1180
 
@@ -1023,23 +1188,23 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1023
1188
  function copilotTemplate(vars) {
1024
1189
  return `## vexp context tools <!-- vexp v${vars.version} -->
1025
1190
 
1026
- **MANDATORY: use \`run_pipeline\` do NOT grep, glob, or read files manually.**
1191
+ **MANDATORY: use \`run_pipeline\` - do NOT grep, glob, or read files manually.**
1027
1192
  vexp returns pre-indexed, graph-ranked context in a single call.
1028
1193
 
1029
1194
  ### Workflow
1030
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1195
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
1031
1196
  2. Make targeted changes based on the context returned
1032
1197
  3. \`run_pipeline\` again only if you need more context
1033
1198
 
1034
1199
  ### Available MCP tools
1035
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1200
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1036
1201
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
1037
- - \`get_skeleton\` compact file structure
1038
- - \`index_status\` indexing status
1039
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1202
+ - \`get_skeleton\` - compact file structure
1203
+ - \`index_status\` - indexing status
1204
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1040
1205
 
1041
1206
  ### Agentic search
1042
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1207
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1043
1208
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1044
1209
  rather than letting them search the codebase independently
1045
1210
 
@@ -1053,23 +1218,23 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1053
1218
  function zedTemplate(vars) {
1054
1219
  return `## vexp for Zed <!-- vexp v${vars.version} -->
1055
1220
 
1056
- **MANDATORY: use \`run_pipeline\` do NOT grep, glob, or read files manually.**
1221
+ **MANDATORY: use \`run_pipeline\` - do NOT grep, glob, or read files manually.**
1057
1222
  vexp returns pre-indexed, graph-ranked context in a single call.
1058
1223
 
1059
1224
  ### Workflow
1060
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1225
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
1061
1226
  2. Make targeted changes based on the context returned
1062
1227
  3. \`run_pipeline\` again only if you need more context
1063
1228
 
1064
1229
  ### Available MCP tools
1065
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1230
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1066
1231
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
1067
- - \`get_skeleton\` compact file structure
1068
- - \`index_status\` indexing status
1069
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1232
+ - \`get_skeleton\` - compact file structure
1233
+ - \`index_status\` - indexing status
1234
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1070
1235
 
1071
1236
  ### Agentic search
1072
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1237
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1073
1238
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1074
1239
  rather than letting them search the codebase independently
1075
1240
 
@@ -1083,23 +1248,23 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1083
1248
  function agentsMdTemplate(vars) {
1084
1249
  return `## vexp <!-- vexp v${vars.version} -->
1085
1250
 
1086
- **MANDATORY: use \`run_pipeline\` do NOT grep or glob the codebase.**
1251
+ **MANDATORY: use \`run_pipeline\` - do NOT grep or glob the codebase.**
1087
1252
  vexp returns pre-indexed, graph-ranked context in a single call.
1088
1253
 
1089
1254
  ### Workflow
1090
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1255
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
1091
1256
  2. Make targeted changes based on the context returned
1092
1257
  3. \`run_pipeline\` again only if you need more context
1093
1258
 
1094
1259
  ### Available MCP tools
1095
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1260
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1096
1261
  Auto-detects intent. Includes file content. Example: \`run_pipeline({ "task": "fix auth bug" })\`
1097
- - \`get_skeleton\` compact file structure
1098
- - \`index_status\` indexing status
1099
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1262
+ - \`get_skeleton\` - compact file structure
1263
+ - \`index_status\` - indexing status
1264
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1100
1265
 
1101
1266
  ### Agentic search
1102
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1267
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1103
1268
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1104
1269
  rather than letting them search the codebase independently
1105
1270
 
@@ -1113,23 +1278,23 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1113
1278
  function kiroTemplate(vars) {
1114
1279
  return `# vexp steering <!-- vexp v${vars.version} -->
1115
1280
 
1116
- **MANDATORY: use \`run_pipeline\` do NOT grep, glob, or read files manually.**
1281
+ **MANDATORY: use \`run_pipeline\` - do NOT grep, glob, or read files manually.**
1117
1282
  vexp returns pre-indexed, graph-ranked context in a single call.
1118
1283
 
1119
1284
  ## Workflow
1120
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1285
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
1121
1286
  2. Make targeted changes based on the context returned
1122
1287
  3. \`run_pipeline\` again only if you need more context
1123
1288
 
1124
1289
  ## Available vexp tools
1125
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1290
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1126
1291
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
1127
- - \`get_skeleton\` compact file structure
1128
- - \`index_status\` indexing status
1129
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1292
+ - \`get_skeleton\` - compact file structure
1293
+ - \`index_status\` - indexing status
1294
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1130
1295
 
1131
1296
  ## Agentic search
1132
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1297
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1133
1298
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1134
1299
  rather than letting them search the codebase independently
1135
1300
 
@@ -1143,23 +1308,23 @@ Intent auto-detection, hybrid ranking, session memory, auto-expanding budget.
1143
1308
  function genericTemplate(vars) {
1144
1309
  return `## vexp <!-- vexp v${vars.version} -->
1145
1310
 
1146
- **MANDATORY: use \`run_pipeline\` do NOT grep or glob the codebase.**
1311
+ **MANDATORY: use \`run_pipeline\` - do NOT grep or glob the codebase.**
1147
1312
  vexp returns pre-indexed, graph-ranked context in a single call.
1148
1313
 
1149
1314
  ### Workflow
1150
- 1. \`run_pipeline\` with your task description ALWAYS FIRST (replaces all other tools)
1315
+ 1. \`run_pipeline\` with your task description - ALWAYS FIRST (replaces all other tools)
1151
1316
  2. Make targeted changes based on the context returned
1152
1317
  3. \`run_pipeline\` again only if you need more context
1153
1318
 
1154
1319
  ### Available MCP tools
1155
- - \`run_pipeline\` **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1320
+ - \`run_pipeline\` - **PRIMARY TOOL**. Runs capsule + impact + memory in 1 call.
1156
1321
  Example: \`run_pipeline({ "task": "fix auth bug" })\`
1157
- - \`get_skeleton\` compact file structure
1158
- - \`index_status\` indexing status
1159
- - \`expand_vexp_ref\` expand V-REF placeholders in v2 output
1322
+ - \`get_skeleton\` - compact file structure
1323
+ - \`index_status\` - indexing status
1324
+ - \`expand_vexp_ref\` - expand V-REF placeholders in v2 output
1160
1325
 
1161
1326
  ### Agentic search
1162
- - Do NOT use built-in file search, grep, or codebase indexing always call \`run_pipeline\` first
1327
+ - Do NOT use built-in file search, grep, or codebase indexing - always call \`run_pipeline\` first
1163
1328
  - If you spawn sub-agents or background tasks, pass them the context from \`run_pipeline\`
1164
1329
  rather than letting them search the codebase independently
1165
1330