@swarmvaultai/engine 3.16.1 → 3.17.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.
Files changed (139) hide show
  1. package/dist/chunk-2CH2WWS4.js +1359 -0
  2. package/dist/chunk-2PN46RDI.js +26846 -0
  3. package/dist/chunk-333AMRSV.js +1056 -0
  4. package/dist/chunk-3GVEUYQZ.js +1641 -0
  5. package/dist/chunk-4MSSM2GH.js +1476 -0
  6. package/dist/chunk-563TZ4TZ.js +26573 -0
  7. package/dist/chunk-5GEPTIZE.js +26010 -0
  8. package/dist/chunk-5HNZ2WQI.js +1341 -0
  9. package/dist/chunk-5Q4IV4O3.js +1336 -0
  10. package/dist/chunk-65IRGGXX.js +27576 -0
  11. package/dist/chunk-6MO57J5C.js +988 -0
  12. package/dist/chunk-6UPHDGEB.js +1073 -0
  13. package/dist/chunk-75BU5TQ6.js +1690 -0
  14. package/dist/chunk-7O2HJSWQ.js +1686 -0
  15. package/dist/chunk-7QHDATCQ.js +1673 -0
  16. package/dist/chunk-B3FC4J3P.js +1214 -0
  17. package/dist/chunk-BTWPJEP2.js +1421 -0
  18. package/dist/chunk-CG67P2HB.js +1420 -0
  19. package/dist/chunk-CSPDMCON.js +26846 -0
  20. package/dist/chunk-CVFY54CF.js +24893 -0
  21. package/dist/chunk-CWLDFLH2.js +1163 -0
  22. package/dist/chunk-DAJAZPPO.js +26865 -0
  23. package/dist/chunk-EEWB4WGH.js +1056 -0
  24. package/dist/chunk-EXD4RWT3.js +1131 -0
  25. package/dist/chunk-F7HZZ3VM.js +931 -0
  26. package/dist/chunk-FD3LJQ4T.js +1216 -0
  27. package/dist/chunk-G2TH6ZTA.js +1468 -0
  28. package/dist/chunk-H3CDZYRE.js +1701 -0
  29. package/dist/chunk-HFU5S5NO.js +838 -0
  30. package/dist/chunk-HKU2T5JX.js +25213 -0
  31. package/dist/chunk-HOJ7NSYC.js +937 -0
  32. package/dist/chunk-HORJDLXV.js +27614 -0
  33. package/dist/chunk-HRRPWXRZ.js +1335 -0
  34. package/dist/chunk-HW72C7O2.js +1690 -0
  35. package/dist/chunk-IAEYFTUS.js +1159 -0
  36. package/dist/chunk-IHMJCCXR.js +1146 -0
  37. package/dist/chunk-JEWLYIHN.js +27619 -0
  38. package/dist/chunk-JJDJF2P3.js +27012 -0
  39. package/dist/chunk-JTRE7C7P.js +26062 -0
  40. package/dist/chunk-L7DKPPV4.js +27339 -0
  41. package/dist/chunk-LEUV6TWJ.js +1131 -0
  42. package/dist/chunk-MB7HPUTR.js +1364 -0
  43. package/dist/chunk-MZSUYTSL.js +998 -0
  44. package/dist/chunk-N56FAH4N.js +1404 -0
  45. package/dist/chunk-NCSZ4AKP.js +1057 -0
  46. package/dist/chunk-NECZ4MUE.js +1416 -0
  47. package/dist/chunk-NHGS4LOI.js +1346 -0
  48. package/dist/chunk-NUWZUYE7.js +1701 -0
  49. package/dist/chunk-OK5752AP.js +1325 -0
  50. package/dist/chunk-QMW7OISM.js +1063 -0
  51. package/dist/chunk-RN56HUXA.js +26972 -0
  52. package/dist/chunk-RSQRF4FV.js +1424 -0
  53. package/dist/chunk-S2E65WRI.js +26062 -0
  54. package/dist/chunk-SRHM3HP4.js +944 -0
  55. package/dist/chunk-U7JO257M.js +25017 -0
  56. package/dist/chunk-UQCF65BN.js +1623 -0
  57. package/dist/chunk-USSP4GVB.js +25064 -0
  58. package/dist/chunk-V7KX3AQD.js +26010 -0
  59. package/dist/chunk-VSDBQVSE.js +27584 -0
  60. package/dist/chunk-WOA5LSNB.js +26559 -0
  61. package/dist/chunk-WWP3VPEJ.js +26080 -0
  62. package/dist/chunk-YFKWMXJ6.js +26066 -0
  63. package/dist/chunk-Z552HHPV.js +26846 -0
  64. package/dist/chunk-ZQ5T64AR.js +1365 -0
  65. package/dist/hooks/claude.js +236 -19
  66. package/dist/hooks/codex.js +134 -6
  67. package/dist/hooks/copilot.js +95 -3
  68. package/dist/hooks/gemini.js +153 -5
  69. package/dist/hooks/opencode.js +4 -2
  70. package/dist/index.d.ts +19 -1
  71. package/dist/index.js +237 -3
  72. package/dist/memory-A4VPLUBA.js +32 -0
  73. package/dist/memory-DNSQCDHC.js +32 -0
  74. package/dist/memory-ECS3TSGC.js +32 -0
  75. package/dist/memory-FVIBFROA.js +32 -0
  76. package/dist/memory-G6I3DBW4.js +32 -0
  77. package/dist/memory-GFOW2QWQ.js +32 -0
  78. package/dist/memory-GSCQ6F53.js +32 -0
  79. package/dist/memory-HE6VWUPV.js +32 -0
  80. package/dist/memory-HEA7XNKB.js +32 -0
  81. package/dist/memory-HMP3Y4PQ.js +32 -0
  82. package/dist/memory-JRYTVHNH.js +32 -0
  83. package/dist/memory-K3NL5E3K.js +32 -0
  84. package/dist/memory-KANI73CX.js +32 -0
  85. package/dist/memory-KI5G2A4C.js +32 -0
  86. package/dist/memory-PK55JUKG.js +32 -0
  87. package/dist/memory-PK5JJNAG.js +32 -0
  88. package/dist/memory-PQWSJ4RR.js +32 -0
  89. package/dist/memory-QCVKS3H4.js +32 -0
  90. package/dist/memory-SAQPBIB4.js +32 -0
  91. package/dist/memory-SVGRP5KS.js +32 -0
  92. package/dist/memory-TQ46BGCI.js +32 -0
  93. package/dist/memory-YKQWWIVY.js +32 -0
  94. package/dist/memory-Z7BP5OSC.js +32 -0
  95. package/dist/registry-2QC3VN7M.js +12 -0
  96. package/dist/registry-2REAPKPO.js +12 -0
  97. package/dist/registry-2XHXZDGH.js +12 -0
  98. package/dist/registry-4C55ZCPL.js +12 -0
  99. package/dist/registry-4QRMVAHX.js +12 -0
  100. package/dist/registry-5SYH3Y3U.js +12 -0
  101. package/dist/registry-6KZMA3XM.js +12 -0
  102. package/dist/registry-7QACDJQQ.js +12 -0
  103. package/dist/registry-B7UXRBW3.js +12 -0
  104. package/dist/registry-FKEREVDO.js +12 -0
  105. package/dist/registry-FLSGGY2R.js +12 -0
  106. package/dist/registry-G7NSRYCO.js +12 -0
  107. package/dist/registry-GH4O3A7H.js +12 -0
  108. package/dist/registry-IBH6K2KK.js +12 -0
  109. package/dist/registry-ILDEBNCW.js +12 -0
  110. package/dist/registry-JFEW5RUP.js +12 -0
  111. package/dist/registry-JQYQOZYN.js +12 -0
  112. package/dist/registry-JR5WY22P.js +12 -0
  113. package/dist/registry-KLO5YIHP.js +12 -0
  114. package/dist/registry-KVJAO5DF.js +12 -0
  115. package/dist/registry-MYJX6AEE.js +12 -0
  116. package/dist/registry-NBLIJHZT.js +12 -0
  117. package/dist/registry-NLRWSN5J.js +12 -0
  118. package/dist/registry-NMXDBYIZ.js +12 -0
  119. package/dist/registry-OUB6W3LM.js +12 -0
  120. package/dist/registry-P5KRT66L.js +12 -0
  121. package/dist/registry-PGZWRXMD.js +12 -0
  122. package/dist/registry-QAG2ZYH3.js +12 -0
  123. package/dist/registry-SUXWCWB4.js +12 -0
  124. package/dist/registry-SYCRRA65.js +12 -0
  125. package/dist/registry-TYROWPR5.js +12 -0
  126. package/dist/registry-U23ML76I.js +12 -0
  127. package/dist/registry-U76DBOV3.js +12 -0
  128. package/dist/registry-UA42LQUQ.js +12 -0
  129. package/dist/registry-W6ZFRI73.js +12 -0
  130. package/dist/registry-X5PMZTZY.js +12 -0
  131. package/dist/registry-XIL5F33J.js +12 -0
  132. package/dist/registry-XOPLQNZY.js +12 -0
  133. package/dist/registry-YDXVCE4Q.js +12 -0
  134. package/dist/registry-YGVTLIZH.js +12 -0
  135. package/dist/registry-ZNW3FDED.js +12 -0
  136. package/dist/viewer/assets/{index-Cq5HAlrV.js → index-BZE-2FtS.js} +37 -37
  137. package/dist/viewer/index.html +1 -1
  138. package/dist/viewer/lib.js +29 -1
  139. package/package.json +1 -1
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/hooks/claude.ts
4
+ import { spawn } from "child_process";
5
+
3
6
  // src/hooks/marker-state.ts
4
7
  import crypto from "crypto";
5
8
  import fs from "fs/promises";
@@ -13,6 +16,23 @@ function markerState(cwd, agentKey) {
13
16
  markerPath: path.join(dir, "report-read")
14
17
  };
15
18
  }
19
+ function flagPath(cwd, agentKey, name) {
20
+ const safeName = name.replaceAll(/[^a-z0-9-]/gi, "-");
21
+ return path.join(markerState(cwd, agentKey).dir, safeName);
22
+ }
23
+ async function markFlag(cwd, agentKey, name) {
24
+ const target = flagPath(cwd, agentKey, name);
25
+ await fs.mkdir(path.dirname(target), { recursive: true });
26
+ await fs.writeFile(target, "seen\n", "utf8");
27
+ }
28
+ async function hasFlag(cwd, agentKey, name) {
29
+ try {
30
+ await fs.access(flagPath(cwd, agentKey, name));
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
16
36
  function isReportPath(value, cwd) {
17
37
  if (typeof value !== "string" || value.length === 0) {
18
38
  return false;
@@ -57,6 +77,10 @@ function resolveToolName(input) {
57
77
  const shaped = input ?? {};
58
78
  return String(shaped.toolName ?? shaped.tool_name ?? shaped.tool?.name ?? shaped.name ?? "");
59
79
  }
80
+ function resolveToolInput(input) {
81
+ const shaped = input ?? {};
82
+ return shaped.toolInput ?? shaped.tool_input ?? {};
83
+ }
60
84
  async function hasReport(cwd) {
61
85
  try {
62
86
  await fs.access(reportPath(cwd));
@@ -135,6 +159,100 @@ function isBroadSearchInput(input) {
135
159
  }
136
160
  return collectCommandCandidates(input).some(commandLooksLikeBroadSearch);
137
161
  }
162
+ var VAULT_ARTIFACT_SEGMENTS = ["wiki", "raw", "state", "agent", "inbox"];
163
+ function isVaultArtifactSearch(input, cwd) {
164
+ const artifactRoot = artifactRootDir(cwd);
165
+ const candidates = [...collectCandidatePaths(input), ...collectCommandCandidates(input)];
166
+ return candidates.some((candidate) => {
167
+ if (typeof candidate !== "string" || candidate.length === 0) {
168
+ return false;
169
+ }
170
+ const normalized = candidate.replaceAll("\\", "/");
171
+ if (VAULT_ARTIFACT_SEGMENTS.some(
172
+ (segment) => normalized.includes(`${segment}/`) && normalized.match(new RegExp(`(^|[\\s'"=/])${segment}/`))
173
+ )) {
174
+ return true;
175
+ }
176
+ const resolved = path.resolve(cwd, candidate);
177
+ return VAULT_ARTIFACT_SEGMENTS.some(
178
+ (segment) => resolved.startsWith(path.join(artifactRoot, segment) + path.sep) || resolved === path.join(artifactRoot, segment)
179
+ );
180
+ });
181
+ }
182
+ async function isNarrowSearch(input) {
183
+ const toolInput = resolveToolInput(input);
184
+ const candidate = toolInput?.path;
185
+ if (typeof candidate !== "string" || candidate.length === 0) {
186
+ return false;
187
+ }
188
+ try {
189
+ const stats = await fs.stat(candidate);
190
+ return stats.isFile();
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ async function resolveGraphFirstMode(cwd) {
196
+ const fromEnv = process.env.SWARMVAULT_GRAPH_FIRST?.trim().toLowerCase();
197
+ if (fromEnv === "deny" || fromEnv === "context" || fromEnv === "off") {
198
+ return fromEnv;
199
+ }
200
+ try {
201
+ const raw = await fs.readFile(path.join(cwd, "swarmvault.config.json"), "utf8");
202
+ const parsed = JSON.parse(raw);
203
+ const fromConfig = typeof parsed?.hooks?.graphFirst === "string" ? parsed.hooks.graphFirst.toLowerCase() : "";
204
+ if (fromConfig === "deny" || fromConfig === "context" || fromConfig === "off") {
205
+ return fromConfig;
206
+ }
207
+ } catch {
208
+ }
209
+ return "deny";
210
+ }
211
+ async function readWatchStaleness(cwd) {
212
+ const watchDir = path.join(artifactRootDir(cwd), "state", "watch");
213
+ let lastRunAt;
214
+ let lastRunSuccess;
215
+ let pendingCount = 0;
216
+ let found = false;
217
+ try {
218
+ const raw = await fs.readFile(path.join(watchDir, "status.json"), "utf8");
219
+ const parsed = JSON.parse(raw);
220
+ lastRunAt = typeof parsed?.lastRun?.finishedAt === "string" ? parsed.lastRun.finishedAt : void 0;
221
+ lastRunSuccess = typeof parsed?.lastRun?.success === "boolean" ? parsed.lastRun.success : void 0;
222
+ found = true;
223
+ } catch {
224
+ }
225
+ try {
226
+ const raw = await fs.readFile(path.join(watchDir, "pending-semantic-refresh.json"), "utf8");
227
+ const parsed = JSON.parse(raw);
228
+ if (Array.isArray(parsed)) {
229
+ pendingCount = parsed.length;
230
+ found = true;
231
+ }
232
+ } catch {
233
+ }
234
+ if (!found) {
235
+ return null;
236
+ }
237
+ return { lastRunAt, lastRunSuccess, pendingSemanticRefreshCount: pendingCount };
238
+ }
239
+ function collectEditedFilePaths(input, cwd) {
240
+ const toolInput = resolveToolInput(input);
241
+ const candidates = [];
242
+ for (const key of ["file_path", "filePath", "path", "notebook_path", "notebookPath"]) {
243
+ const value = toolInput?.[key];
244
+ if (typeof value === "string" && value.length > 0) {
245
+ candidates.push(value);
246
+ }
247
+ }
248
+ const artifactRoot = artifactRootDir(cwd);
249
+ const resolved = candidates.map((candidate) => path.resolve(cwd, candidate)).filter(
250
+ (absolutePath) => !VAULT_ARTIFACT_SEGMENTS.some(
251
+ (segment) => absolutePath === path.join(artifactRoot, segment) || absolutePath.startsWith(path.join(artifactRoot, segment) + path.sep)
252
+ )
253
+ );
254
+ return [...new Set(resolved)];
255
+ }
138
256
  async function readHookInput() {
139
257
  let body = "";
140
258
  for await (const chunk of process.stdin) {
@@ -149,7 +267,52 @@ async function readHookInput() {
149
267
  return {};
150
268
  }
151
269
  }
152
- var REPORT_NOTE = "SwarmVault graph report exists at wiki/graph/report.md, or at $SWARMVAULT_OUT/wiki/graph/report.md when SWARMVAULT_OUT is set. Read it before broad grep/glob searching.";
270
+ var GRAPH_FIRST_COMMANDS = [
271
+ '- `swarmvault graph query "<seed>"` \u2014 top matches with page paths plus an inline excerpt of the best page; usually answers where-is/what-calls in one command',
272
+ '- `swarmvault graph explain "<node>"` \u2014 compact node summary with neighbors and its wiki page',
273
+ "- `swarmvault graph blast <target>` \u2014 reverse-import impact analysis for change-impact questions",
274
+ "- `wiki/graph/report.md` \u2014 orientation report (architecture, communities, key nodes)",
275
+ "Do not add `--json` to these \u2014 the plain output is far smaller and already structured.",
276
+ "Trust the graph/wiki answer for orientation questions; verify in source only when you are about to edit or the evidence conflicts. Answer directly in chat \u2014 do not write answer files unless asked for a durable artifact."
277
+ ];
278
+ function buildGraphFirstNote(staleness) {
279
+ const lines = [
280
+ "This repo has a SwarmVault code graph. To save tokens, answer code-understanding questions (where is X, what calls Y, how is Z structured, impact of changing W) from the graph instead of reading or grepping source files:",
281
+ ...GRAPH_FIRST_COMMANDS,
282
+ "Read source files directly only when you are about to edit them, or when the graph lacks the detail you need.",
283
+ "After your edits the SwarmVault hook refreshes the graph automatically."
284
+ ];
285
+ if (staleness?.pendingSemanticRefreshCount) {
286
+ lines.push(
287
+ `Note: ${staleness.pendingSemanticRefreshCount} non-code change(s) await semantic refresh \u2014 run \`swarmvault compile\` when convenient.`
288
+ );
289
+ }
290
+ if (staleness?.lastRunSuccess === false) {
291
+ lines.push(
292
+ "Note: the last graph refresh failed \u2014 run `swarmvault graph status` then `swarmvault graph update` before relying on the graph."
293
+ );
294
+ }
295
+ return lines.join("\n");
296
+ }
297
+ function extractSearchTerm(input) {
298
+ const toolInput = resolveToolInput(input);
299
+ for (const key of ["pattern", "query", "regex"]) {
300
+ const value = toolInput?.[key];
301
+ if (typeof value === "string" && value.length > 0) {
302
+ return value;
303
+ }
304
+ }
305
+ return "<your term>";
306
+ }
307
+ function buildDenyReason(toolName, input) {
308
+ const term = extractSearchTerm(input).slice(0, 120);
309
+ return [
310
+ `SwarmVault graph-first: this repo has a compiled code graph that answers structure questions in far fewer tokens than ${toolName || "broad search"}.`,
311
+ `Run: swarmvault graph query "${term}" \u2014 it prints the top matches with page paths plus an inline excerpt of the best page, which usually answers the question without reading source. Add --context calls for caller/impact questions. Do not add --json (much larger output).`,
312
+ "Trust that answer for orientation questions instead of re-verifying in source files.",
313
+ "If the graph does not answer, repeat this exact search \u2014 it will be allowed for the rest of the session."
314
+ ].join(" ");
315
+ }
153
316
 
154
317
  // src/hooks/claude.ts
155
318
  var AGENT_KEY = "claude";
@@ -157,38 +320,92 @@ function emit(value) {
157
320
  process.stdout.write(`${JSON.stringify(value)}
158
321
  `);
159
322
  }
160
- async function main() {
161
- const mode = process.argv[2] ?? "";
162
- const input = await readHookInput();
163
- const cwd = resolveInputCwd(input);
164
- if (!await hasReport(cwd)) {
323
+ function denyFlagName(toolName) {
324
+ return `deny-search-${(toolName || "unknown").toLowerCase()}`;
325
+ }
326
+ async function handleSessionStart(cwd) {
327
+ await resetSession(cwd, AGENT_KEY);
328
+ const staleness = await readWatchStaleness(cwd);
329
+ emit({
330
+ hookSpecificOutput: {
331
+ hookEventName: "SessionStart",
332
+ additionalContext: buildGraphFirstNote(staleness)
333
+ }
334
+ });
335
+ }
336
+ async function handlePostEdit(cwd, input) {
337
+ const editedPaths = collectEditedFilePaths(input, cwd);
338
+ if (editedPaths.length > 0) {
339
+ try {
340
+ const child = spawn("swarmvault", ["graph", "update", ...editedPaths.flatMap((p) => ["--file", p]), "--json"], {
341
+ cwd,
342
+ detached: true,
343
+ stdio: "ignore"
344
+ });
345
+ child.unref();
346
+ } catch {
347
+ }
348
+ }
349
+ emit({});
350
+ }
351
+ async function handlePreToolUse(cwd, input) {
352
+ if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
353
+ await markReportRead(cwd, AGENT_KEY);
165
354
  emit({});
166
- process.exit(0);
355
+ return;
167
356
  }
168
- if (mode === "session-start") {
169
- await resetSession(cwd, AGENT_KEY);
357
+ const mode = await resolveGraphFirstMode(cwd);
358
+ if (mode === "off" || !isBroadSearchInput(input)) {
359
+ emit({});
360
+ return;
361
+ }
362
+ if (isVaultArtifactSearch(input, cwd) || await isNarrowSearch(input)) {
363
+ emit({});
364
+ return;
365
+ }
366
+ const toolName = resolveToolName(input);
367
+ const flag = denyFlagName(toolName);
368
+ if (mode === "deny" && !await hasFlag(cwd, AGENT_KEY, flag)) {
369
+ await markFlag(cwd, AGENT_KEY, flag);
170
370
  emit({
171
371
  hookSpecificOutput: {
172
- hookEventName: "SessionStart",
173
- additionalContext: REPORT_NOTE
372
+ hookEventName: "PreToolUse",
373
+ permissionDecision: "deny",
374
+ permissionDecisionReason: buildDenyReason(toolName, input)
174
375
  }
175
376
  });
176
- process.exit(0);
377
+ return;
177
378
  }
178
- if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
379
+ if (!await hasSeenReport(cwd, AGENT_KEY)) {
179
380
  await markReportRead(cwd, AGENT_KEY);
180
- emit({});
181
- process.exit(0);
182
- }
183
- if (isBroadSearchInput(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
184
381
  emit({
185
382
  hookSpecificOutput: {
186
383
  hookEventName: "PreToolUse",
187
- additionalContext: REPORT_NOTE
384
+ permissionDecision: "allow",
385
+ additionalContext: buildDenyReason(toolName, input)
188
386
  }
189
387
  });
190
- process.exit(0);
388
+ return;
191
389
  }
192
390
  emit({});
193
391
  }
392
+ async function main() {
393
+ const mode = process.argv[2] ?? "";
394
+ const input = await readHookInput();
395
+ const cwd = resolveInputCwd(input);
396
+ if (!await hasReport(cwd) || await resolveGraphFirstMode(cwd) === "off") {
397
+ emit({});
398
+ process.exit(0);
399
+ }
400
+ if (mode === "session-start") {
401
+ await handleSessionStart(cwd);
402
+ process.exit(0);
403
+ }
404
+ if (mode === "post-edit") {
405
+ await handlePostEdit(cwd, input);
406
+ process.exit(0);
407
+ }
408
+ await handlePreToolUse(cwd, input);
409
+ process.exit(0);
410
+ }
194
411
  await main();
@@ -57,6 +57,10 @@ function resolveToolName(input) {
57
57
  const shaped = input ?? {};
58
58
  return String(shaped.toolName ?? shaped.tool_name ?? shaped.tool?.name ?? shaped.name ?? "");
59
59
  }
60
+ function resolveToolInput(input) {
61
+ const shaped = input ?? {};
62
+ return shaped.toolInput ?? shaped.tool_input ?? {};
63
+ }
60
64
  async function hasReport(cwd) {
61
65
  try {
62
66
  await fs.access(reportPath(cwd));
@@ -135,6 +139,83 @@ function isBroadSearchInput(input) {
135
139
  }
136
140
  return collectCommandCandidates(input).some(commandLooksLikeBroadSearch);
137
141
  }
142
+ var VAULT_ARTIFACT_SEGMENTS = ["wiki", "raw", "state", "agent", "inbox"];
143
+ function isVaultArtifactSearch(input, cwd) {
144
+ const artifactRoot = artifactRootDir(cwd);
145
+ const candidates = [...collectCandidatePaths(input), ...collectCommandCandidates(input)];
146
+ return candidates.some((candidate) => {
147
+ if (typeof candidate !== "string" || candidate.length === 0) {
148
+ return false;
149
+ }
150
+ const normalized = candidate.replaceAll("\\", "/");
151
+ if (VAULT_ARTIFACT_SEGMENTS.some(
152
+ (segment) => normalized.includes(`${segment}/`) && normalized.match(new RegExp(`(^|[\\s'"=/])${segment}/`))
153
+ )) {
154
+ return true;
155
+ }
156
+ const resolved = path.resolve(cwd, candidate);
157
+ return VAULT_ARTIFACT_SEGMENTS.some(
158
+ (segment) => resolved.startsWith(path.join(artifactRoot, segment) + path.sep) || resolved === path.join(artifactRoot, segment)
159
+ );
160
+ });
161
+ }
162
+ async function isNarrowSearch(input) {
163
+ const toolInput = resolveToolInput(input);
164
+ const candidate = toolInput?.path;
165
+ if (typeof candidate !== "string" || candidate.length === 0) {
166
+ return false;
167
+ }
168
+ try {
169
+ const stats = await fs.stat(candidate);
170
+ return stats.isFile();
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+ async function resolveGraphFirstMode(cwd) {
176
+ const fromEnv = process.env.SWARMVAULT_GRAPH_FIRST?.trim().toLowerCase();
177
+ if (fromEnv === "deny" || fromEnv === "context" || fromEnv === "off") {
178
+ return fromEnv;
179
+ }
180
+ try {
181
+ const raw = await fs.readFile(path.join(cwd, "swarmvault.config.json"), "utf8");
182
+ const parsed = JSON.parse(raw);
183
+ const fromConfig = typeof parsed?.hooks?.graphFirst === "string" ? parsed.hooks.graphFirst.toLowerCase() : "";
184
+ if (fromConfig === "deny" || fromConfig === "context" || fromConfig === "off") {
185
+ return fromConfig;
186
+ }
187
+ } catch {
188
+ }
189
+ return "deny";
190
+ }
191
+ async function readWatchStaleness(cwd) {
192
+ const watchDir = path.join(artifactRootDir(cwd), "state", "watch");
193
+ let lastRunAt;
194
+ let lastRunSuccess;
195
+ let pendingCount = 0;
196
+ let found = false;
197
+ try {
198
+ const raw = await fs.readFile(path.join(watchDir, "status.json"), "utf8");
199
+ const parsed = JSON.parse(raw);
200
+ lastRunAt = typeof parsed?.lastRun?.finishedAt === "string" ? parsed.lastRun.finishedAt : void 0;
201
+ lastRunSuccess = typeof parsed?.lastRun?.success === "boolean" ? parsed.lastRun.success : void 0;
202
+ found = true;
203
+ } catch {
204
+ }
205
+ try {
206
+ const raw = await fs.readFile(path.join(watchDir, "pending-semantic-refresh.json"), "utf8");
207
+ const parsed = JSON.parse(raw);
208
+ if (Array.isArray(parsed)) {
209
+ pendingCount = parsed.length;
210
+ found = true;
211
+ }
212
+ } catch {
213
+ }
214
+ if (!found) {
215
+ return null;
216
+ }
217
+ return { lastRunAt, lastRunSuccess, pendingSemanticRefreshCount: pendingCount };
218
+ }
138
219
  async function readHookInput() {
139
220
  let body = "";
140
221
  for await (const chunk of process.stdin) {
@@ -149,7 +230,52 @@ async function readHookInput() {
149
230
  return {};
150
231
  }
151
232
  }
152
- var REPORT_NOTE = "SwarmVault graph report exists at wiki/graph/report.md, or at $SWARMVAULT_OUT/wiki/graph/report.md when SWARMVAULT_OUT is set. Read it before broad grep/glob searching.";
233
+ var GRAPH_FIRST_COMMANDS = [
234
+ '- `swarmvault graph query "<seed>"` \u2014 top matches with page paths plus an inline excerpt of the best page; usually answers where-is/what-calls in one command',
235
+ '- `swarmvault graph explain "<node>"` \u2014 compact node summary with neighbors and its wiki page',
236
+ "- `swarmvault graph blast <target>` \u2014 reverse-import impact analysis for change-impact questions",
237
+ "- `wiki/graph/report.md` \u2014 orientation report (architecture, communities, key nodes)",
238
+ "Do not add `--json` to these \u2014 the plain output is far smaller and already structured.",
239
+ "Trust the graph/wiki answer for orientation questions; verify in source only when you are about to edit or the evidence conflicts. Answer directly in chat \u2014 do not write answer files unless asked for a durable artifact."
240
+ ];
241
+ function buildGraphFirstNote(staleness) {
242
+ const lines = [
243
+ "This repo has a SwarmVault code graph. To save tokens, answer code-understanding questions (where is X, what calls Y, how is Z structured, impact of changing W) from the graph instead of reading or grepping source files:",
244
+ ...GRAPH_FIRST_COMMANDS,
245
+ "Read source files directly only when you are about to edit them, or when the graph lacks the detail you need.",
246
+ "After your edits the SwarmVault hook refreshes the graph automatically."
247
+ ];
248
+ if (staleness?.pendingSemanticRefreshCount) {
249
+ lines.push(
250
+ `Note: ${staleness.pendingSemanticRefreshCount} non-code change(s) await semantic refresh \u2014 run \`swarmvault compile\` when convenient.`
251
+ );
252
+ }
253
+ if (staleness?.lastRunSuccess === false) {
254
+ lines.push(
255
+ "Note: the last graph refresh failed \u2014 run `swarmvault graph status` then `swarmvault graph update` before relying on the graph."
256
+ );
257
+ }
258
+ return lines.join("\n");
259
+ }
260
+ function extractSearchTerm(input) {
261
+ const toolInput = resolveToolInput(input);
262
+ for (const key of ["pattern", "query", "regex"]) {
263
+ const value = toolInput?.[key];
264
+ if (typeof value === "string" && value.length > 0) {
265
+ return value;
266
+ }
267
+ }
268
+ return "<your term>";
269
+ }
270
+ function buildDenyReason(toolName, input) {
271
+ const term = extractSearchTerm(input).slice(0, 120);
272
+ return [
273
+ `SwarmVault graph-first: this repo has a compiled code graph that answers structure questions in far fewer tokens than ${toolName || "broad search"}.`,
274
+ `Run: swarmvault graph query "${term}" \u2014 it prints the top matches with page paths plus an inline excerpt of the best page, which usually answers the question without reading source. Add --context calls for caller/impact questions. Do not add --json (much larger output).`,
275
+ "Trust that answer for orientation questions instead of re-verifying in source files.",
276
+ "If the graph does not answer, repeat this exact search \u2014 it will be allowed for the rest of the session."
277
+ ].join(" ");
278
+ }
153
279
 
154
280
  // src/hooks/codex.ts
155
281
  var AGENT_KEY = "codex";
@@ -157,10 +283,10 @@ function emit(value) {
157
283
  process.stdout.write(`${JSON.stringify(value)}
158
284
  `);
159
285
  }
160
- function note() {
286
+ function note(message) {
161
287
  return {
162
288
  priority: "IMPORTANT",
163
- message: REPORT_NOTE
289
+ message
164
290
  };
165
291
  }
166
292
  async function main() {
@@ -173,7 +299,7 @@ async function main() {
173
299
  }
174
300
  if (mode === "session-start") {
175
301
  await resetSession(cwd, AGENT_KEY);
176
- emit(note());
302
+ emit(note(buildGraphFirstNote(await readWatchStaleness(cwd))));
177
303
  process.exit(0);
178
304
  }
179
305
  if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
@@ -181,8 +307,10 @@ async function main() {
181
307
  emit({});
182
308
  process.exit(0);
183
309
  }
184
- if (isBroadSearchInput(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
185
- emit(note());
310
+ const graphFirstMode = await resolveGraphFirstMode(cwd);
311
+ if (graphFirstMode !== "off" && isBroadSearchInput(input) && !isVaultArtifactSearch(input, cwd) && !await isNarrowSearch(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
312
+ await markReportRead(cwd, AGENT_KEY);
313
+ emit(note(buildDenyReason(resolveToolName(input), input)));
186
314
  process.exit(0);
187
315
  }
188
316
  emit({});
@@ -57,6 +57,10 @@ function resolveToolName(input) {
57
57
  const shaped = input ?? {};
58
58
  return String(shaped.toolName ?? shaped.tool_name ?? shaped.tool?.name ?? shaped.name ?? "");
59
59
  }
60
+ function resolveToolInput(input) {
61
+ const shaped = input ?? {};
62
+ return shaped.toolInput ?? shaped.tool_input ?? {};
63
+ }
60
64
  async function hasReport(cwd) {
61
65
  try {
62
66
  await fs.access(reportPath(cwd));
@@ -96,6 +100,74 @@ async function resetSession(cwd, agentKey) {
96
100
  function isBroadSearchTool(toolName) {
97
101
  return /grep|glob|search|find/i.test(toolName);
98
102
  }
103
+ function collectCommandCandidates(node, acc = []) {
104
+ if (!node || typeof node !== "object") {
105
+ return acc;
106
+ }
107
+ if (Array.isArray(node)) {
108
+ for (const item of node) {
109
+ collectCommandCandidates(item, acc);
110
+ }
111
+ return acc;
112
+ }
113
+ for (const [key, value] of Object.entries(node)) {
114
+ if (["command", "cmd", "script", "bash", "shell"].includes(key) && typeof value === "string") {
115
+ acc.push(value);
116
+ continue;
117
+ }
118
+ collectCommandCandidates(value, acc);
119
+ }
120
+ return acc;
121
+ }
122
+ var VAULT_ARTIFACT_SEGMENTS = ["wiki", "raw", "state", "agent", "inbox"];
123
+ function isVaultArtifactSearch(input, cwd) {
124
+ const artifactRoot = artifactRootDir(cwd);
125
+ const candidates = [...collectCandidatePaths(input), ...collectCommandCandidates(input)];
126
+ return candidates.some((candidate) => {
127
+ if (typeof candidate !== "string" || candidate.length === 0) {
128
+ return false;
129
+ }
130
+ const normalized = candidate.replaceAll("\\", "/");
131
+ if (VAULT_ARTIFACT_SEGMENTS.some(
132
+ (segment) => normalized.includes(`${segment}/`) && normalized.match(new RegExp(`(^|[\\s'"=/])${segment}/`))
133
+ )) {
134
+ return true;
135
+ }
136
+ const resolved = path.resolve(cwd, candidate);
137
+ return VAULT_ARTIFACT_SEGMENTS.some(
138
+ (segment) => resolved.startsWith(path.join(artifactRoot, segment) + path.sep) || resolved === path.join(artifactRoot, segment)
139
+ );
140
+ });
141
+ }
142
+ async function isNarrowSearch(input) {
143
+ const toolInput = resolveToolInput(input);
144
+ const candidate = toolInput?.path;
145
+ if (typeof candidate !== "string" || candidate.length === 0) {
146
+ return false;
147
+ }
148
+ try {
149
+ const stats = await fs.stat(candidate);
150
+ return stats.isFile();
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+ async function resolveGraphFirstMode(cwd) {
156
+ const fromEnv = process.env.SWARMVAULT_GRAPH_FIRST?.trim().toLowerCase();
157
+ if (fromEnv === "deny" || fromEnv === "context" || fromEnv === "off") {
158
+ return fromEnv;
159
+ }
160
+ try {
161
+ const raw = await fs.readFile(path.join(cwd, "swarmvault.config.json"), "utf8");
162
+ const parsed = JSON.parse(raw);
163
+ const fromConfig = typeof parsed?.hooks?.graphFirst === "string" ? parsed.hooks.graphFirst.toLowerCase() : "";
164
+ if (fromConfig === "deny" || fromConfig === "context" || fromConfig === "off") {
165
+ return fromConfig;
166
+ }
167
+ } catch {
168
+ }
169
+ return "deny";
170
+ }
99
171
  async function readHookInput() {
100
172
  let body = "";
101
173
  for await (const chunk of process.stdin) {
@@ -110,7 +182,25 @@ async function readHookInput() {
110
182
  return {};
111
183
  }
112
184
  }
113
- var REPORT_NOTE = "SwarmVault graph report exists at wiki/graph/report.md, or at $SWARMVAULT_OUT/wiki/graph/report.md when SWARMVAULT_OUT is set. Read it before broad grep/glob searching.";
185
+ function extractSearchTerm(input) {
186
+ const toolInput = resolveToolInput(input);
187
+ for (const key of ["pattern", "query", "regex"]) {
188
+ const value = toolInput?.[key];
189
+ if (typeof value === "string" && value.length > 0) {
190
+ return value;
191
+ }
192
+ }
193
+ return "<your term>";
194
+ }
195
+ function buildDenyReason(toolName, input) {
196
+ const term = extractSearchTerm(input).slice(0, 120);
197
+ return [
198
+ `SwarmVault graph-first: this repo has a compiled code graph that answers structure questions in far fewer tokens than ${toolName || "broad search"}.`,
199
+ `Run: swarmvault graph query "${term}" \u2014 it prints the top matches with page paths plus an inline excerpt of the best page, which usually answers the question without reading source. Add --context calls for caller/impact questions. Do not add --json (much larger output).`,
200
+ "Trust that answer for orientation questions instead of re-verifying in source files.",
201
+ "If the graph does not answer, repeat this exact search \u2014 it will be allowed for the rest of the session."
202
+ ].join(" ");
203
+ }
114
204
 
115
205
  // src/hooks/copilot.ts
116
206
  var AGENT_KEY = "copilot";
@@ -139,10 +229,12 @@ async function main() {
139
229
  emit({});
140
230
  process.exit(0);
141
231
  }
142
- if (isBroadSearchTool(toolName) && !await hasSeenReport(cwd, AGENT_KEY)) {
232
+ const graphFirstMode = await resolveGraphFirstMode(cwd);
233
+ if (graphFirstMode === "deny" && isBroadSearchTool(toolName) && !isVaultArtifactSearch(input, cwd) && !await isNarrowSearch(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
234
+ await markReportRead(cwd, AGENT_KEY);
143
235
  emit({
144
236
  permissionDecision: "deny",
145
- permissionDecisionReason: REPORT_NOTE
237
+ permissionDecisionReason: buildDenyReason(toolName, input)
146
238
  });
147
239
  process.exit(0);
148
240
  }