@event4u/agent-config 17.3.0 → 17.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.
Files changed (137) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +164 -342
  5. package/CREDITS.md +2 -2
  6. package/README.md +1 -1
  7. package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
  8. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  9. package/dist/cli/registry.js +2 -2
  10. package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
  11. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  12. package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
  13. package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
  14. package/dist/cli-delegate/cmd_conformance.js +175 -183
  15. package/dist/cli-delegate/cmd_doctor.js +5 -5
  16. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  17. package/dist/cli-delegate/cmd_explain.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  22. package/dist/cli-delegate/cmd_update.js +24 -13
  23. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  24. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  25. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  26. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  27. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +3 -3
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
  38. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
  39. package/dist/hooks/dispatch.js +982 -929
  40. package/dist/hooks/dispatch.sha256 +1 -1
  41. package/dist/hooks/egress_engine.chunk.cjs +56 -6
  42. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  43. package/dist/hooks/update_notice.chunk.cjs +914 -0
  44. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  45. package/dist/mcp/registry-manifest.json +1 -1
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/mcp/server.mjs +71259 -66437
  48. package/dist/scripts/condense.js +9 -6
  49. package/package.json +4 -1
  50. package/src/config/evaluator-budgets.json +9 -8
  51. package/src/config/gate-violation-baselines.json +2 -2
  52. package/src/config/hook-bundle-budget.json +31 -6
  53. package/src/config/packed-binary-manifest.json +9 -0
  54. package/src/config/sigstore/npm_keys.json +50 -0
  55. package/src/config/sigstore/trust-material.json +9 -0
  56. package/src/config/sigstore/trusted_root.json +126 -0
  57. package/src/config/skill-body-budget.json +7 -1
  58. package/src/scripts/_cli/cmd_update.ts +48 -20
  59. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  60. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  61. package/src/scripts/_dispatch.bash +3 -2
  62. package/src/scripts/_lib/confusables.ts +0 -19
  63. package/src/scripts/_lib/egress_normalize.ts +53 -23
  64. package/src/scripts/_lib/egress_text.ts +5 -3
  65. package/src/scripts/_lib/egress_token.ts +7 -5
  66. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  67. package/src/scripts/_lib/git_common_dir.ts +10 -0
  68. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  69. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  70. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  71. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  72. package/src/scripts/_lib/lineage_map.ts +55 -0
  73. package/src/scripts/_lib/lineage_query.ts +140 -0
  74. package/src/scripts/_lib/lineage_seed.ts +116 -0
  75. package/src/scripts/_lib/lineage_watch.ts +249 -0
  76. package/src/scripts/_lib/link_crypto.ts +8 -4
  77. package/src/scripts/_lib/production_reach.ts +3 -3
  78. package/src/scripts/_lib/project_registry.ts +227 -0
  79. package/src/scripts/_lib/provenance_verify.ts +14 -242
  80. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  81. package/src/scripts/_lib/source_id.ts +48 -0
  82. package/src/scripts/_lib/source_shape.ts +4 -0
  83. package/src/scripts/_lib/unattended_gates.ts +6 -1
  84. package/src/scripts/advisories.ts +26 -10
  85. package/src/scripts/ai_council/council_egress.ts +5 -6
  86. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  87. package/src/scripts/check_dependency_floors.ts +7 -0
  88. package/src/scripts/check_egress_mutations.ts +1 -0
  89. package/src/scripts/code_graph/build.ts +67 -3
  90. package/src/scripts/code_graph/cli.ts +131 -28
  91. package/src/scripts/code_graph/conventions_default.ts +20 -1
  92. package/src/scripts/code_graph/detect.ts +5 -5
  93. package/src/scripts/code_graph/estate_cli.ts +135 -0
  94. package/src/scripts/code_graph/estate_index.ts +394 -0
  95. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  96. package/src/scripts/code_graph/estate_map.ts +141 -0
  97. package/src/scripts/code_graph/extract.ts +4 -1
  98. package/src/scripts/code_graph/extract_more.ts +137 -2
  99. package/src/scripts/code_graph/graph_budget.ts +158 -0
  100. package/src/scripts/code_graph/graph_home.ts +80 -0
  101. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  102. package/src/scripts/code_graph/map.ts +22 -13
  103. package/src/scripts/code_graph/map_model.ts +67 -12
  104. package/src/scripts/code_graph/map_verify.ts +68 -5
  105. package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
  106. package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
  107. package/src/scripts/code_graph/project_ref.ts +25 -0
  108. package/src/scripts/code_graph/projects_cli.ts +254 -0
  109. package/src/scripts/code_graph/providers/catalog.ts +14 -5
  110. package/src/scripts/code_graph/providers/django.ts +52 -9
  111. package/src/scripts/code_graph/providers/index.ts +2 -0
  112. package/src/scripts/code_graph/providers/rails.ts +204 -0
  113. package/src/scripts/code_graph/providers/spring.ts +38 -1
  114. package/src/scripts/code_graph/query.ts +9 -9
  115. package/src/scripts/code_graph/refresh_lock.ts +12 -8
  116. package/src/scripts/code_graph/repo_identity.ts +169 -0
  117. package/src/scripts/code_graph/sqlite_store.ts +22 -25
  118. package/src/scripts/code_graph/types.ts +6 -2
  119. package/src/scripts/condense.ts +6 -6
  120. package/src/scripts/hook_chunks.mjs +7 -0
  121. package/src/scripts/hook_manifest.json +1 -1
  122. package/src/scripts/hook_manifest.yaml +20 -17
  123. package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
  124. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  125. package/src/scripts/hooks/host_semantics.ts +72 -0
  126. package/src/scripts/hooks/lazy_concern.ts +4 -0
  127. package/src/scripts/hooks/user_notice.ts +105 -0
  128. package/src/scripts/lint_harvest_provenance.ts +137 -11
  129. package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
  130. package/src/scripts/mcp_server/graph_tools.ts +161 -41
  131. package/src/scripts/network_sources.ts +24 -6
  132. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  133. package/src/scripts/update_notice_hook.ts +75 -7
  134. package/src/vendor/grammars/README.md +7 -6
  135. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  136. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  137. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -237,6 +237,9 @@
237
237
  "properties": {
238
238
  "area": {
239
239
  "type": "string"
240
+ },
241
+ "project": {
242
+ "type": "string"
240
243
  }
241
244
  },
242
245
  "additionalProperties": false
@@ -247,7 +250,7 @@
247
250
  },
248
251
  {
249
252
  "name": "graph_dead",
250
- "description": "Symbols with no accepted reference that no declared entry point names. REFUSES (status `refused`) while a source is unreadable: with no exportedness recorded, an exported-but-unimported symbol is indistinguishable from a dead one. Never a deletion recommendation.",
253
+ "description": "Symbols no accepted reference and no declared entry point names. REFUSES (status `refused`) while a source is unreadable: without recorded exportedness an exported-but-unimported symbol looks dead. Never a deletion recommendation.",
251
254
  "side_effect": "ro",
252
255
  "implemented_on": [
253
256
  "stdio"
@@ -257,12 +260,46 @@
257
260
  "properties": {
258
261
  "entry_points": {
259
262
  "type": "string",
260
- "description": "Path under this root to a newline-separated id/label list; `#` comments ok."
263
+ "description": "Path under this root: newline-separated ids or labels, `#` comments ok."
261
264
  },
262
265
  "accept_missing_exports": {
263
266
  "type": "boolean",
264
267
  "default": false,
265
268
  "description": "Answer anyway, accepting the gap. Never a default."
269
+ },
270
+ "project": {
271
+ "type": "string"
272
+ }
273
+ },
274
+ "additionalProperties": false
275
+ },
276
+ "annotations": {
277
+ "readOnlyHint": true
278
+ }
279
+ },
280
+ {
281
+ "name": "graph_estate",
282
+ "description": "Estate index across registered projects: package users, where a name is used, advisory reach. Other graph tools take `project`.",
283
+ "side_effect": "ro",
284
+ "implemented_on": [
285
+ "stdio"
286
+ ],
287
+ "input_schema": {
288
+ "type": "object",
289
+ "required": [
290
+ "query"
291
+ ],
292
+ "properties": {
293
+ "query": {
294
+ "type": "string",
295
+ "enum": [
296
+ "where-used",
297
+ "projects-using",
298
+ "advisory-impact"
299
+ ]
300
+ },
301
+ "name": {
302
+ "type": "string"
266
303
  }
267
304
  },
268
305
  "additionalProperties": false
@@ -273,7 +310,7 @@
273
310
  },
274
311
  {
275
312
  "name": "graph_impact",
276
- "description": "What a diff can break: callers, dependents and test files reaching the changed symbols. Walks only stated-fact edges, never a same-name guess, and reports the guesses refused. Runs `git diff` for the rev.",
313
+ "description": "What a diff can break: callers, dependents and test files reaching the changed symbols, over stated-fact edges only (refused guesses reported). Runs `git diff` for the rev.",
277
314
  "side_effect": "shell",
278
315
  "implemented_on": [
279
316
  "stdio"
@@ -298,6 +335,9 @@
298
335
  "type": "boolean",
299
336
  "default": false,
300
337
  "description": "Instead return changed symbols no test imports."
338
+ },
339
+ "project": {
340
+ "type": "string"
301
341
  }
302
342
  },
303
343
  "additionalProperties": false
@@ -315,7 +355,11 @@
315
355
  ],
316
356
  "input_schema": {
317
357
  "type": "object",
318
- "properties": {},
358
+ "properties": {
359
+ "project": {
360
+ "type": "string"
361
+ }
362
+ },
319
363
  "additionalProperties": false
320
364
  },
321
365
  "annotations": {
@@ -324,7 +368,7 @@
324
368
  },
325
369
  {
326
370
  "name": "graph_node",
327
- "description": "One symbol in full: where it is declared, what reaches it, what it reaches, and how connected it is. Resolves EXACTLY (node id, then label) and refuses anything it cannot — it never scores free text, so an answer is always about the node you named.",
371
+ "description": "One symbol in full: where it is declared, what reaches it, what it reaches, how connected it is. Resolves EXACTLY (node id, then label) and refuses anything else — never scores free text.",
328
372
  "side_effect": "ro",
329
373
  "implemented_on": [
330
374
  "stdio"
@@ -347,7 +391,7 @@
347
391
  "both"
348
392
  ],
349
393
  "default": "both",
350
- "description": "Which edges to walk. `in` = who reaches it, `out` = what it reaches."
394
+ "description": "`in` = who reaches it, `out` = what it reaches."
351
395
  },
352
396
  "depth": {
353
397
  "type": "integer",
@@ -375,7 +419,10 @@
375
419
  "injects",
376
420
  "documents"
377
421
  ],
378
- "description": "Restrict to one relation. Omit for all of them."
422
+ "description": "One relation only; omit for all."
423
+ },
424
+ "project": {
425
+ "type": "string"
379
426
  }
380
427
  },
381
428
  "additionalProperties": false
@@ -386,7 +433,7 @@
386
433
  },
387
434
  {
388
435
  "name": "graph_path",
389
- "description": "The shortest relation chain between two symbols, direction preserved. Answers \"how does A reach B\" without reading the files between. `(no path found)` when unconnected is an answer.",
436
+ "description": "The shortest relation chain between two symbols, direction kept: how A reaches B without reading the files between. `(no path found)` is an answer.",
390
437
  "side_effect": "ro",
391
438
  "implemented_on": [
392
439
  "stdio"
@@ -409,8 +456,10 @@
409
456
  "budget": {
410
457
  "type": "integer",
411
458
  "minimum": 1,
412
- "default": 1500,
413
- "description": "Token budget for lines."
459
+ "default": 1500
460
+ },
461
+ "project": {
462
+ "type": "string"
414
463
  }
415
464
  },
416
465
  "additionalProperties": false
@@ -421,7 +470,7 @@
421
470
  },
422
471
  {
423
472
  "name": "graph_query",
424
- "description": "A symbol's direct outgoing relations — calls, imports, uses, inherits — each line carrying its confidence and resolution mechanism. For \"what does this touch\" before reading a file.",
473
+ "description": "A symbol's direct outgoing relations (calls, imports, uses, inherits), each with its confidence and resolution mechanism: \"what does this touch\" before reading a file.",
425
474
  "side_effect": "ro",
426
475
  "implemented_on": [
427
476
  "stdio"
@@ -439,8 +488,10 @@
439
488
  "budget": {
440
489
  "type": "integer",
441
490
  "minimum": 1,
442
- "default": 1500,
443
- "description": "Token budget for lines."
491
+ "default": 1500
492
+ },
493
+ "project": {
494
+ "type": "string"
444
495
  }
445
496
  },
446
497
  "additionalProperties": false
@@ -451,7 +502,7 @@
451
502
  },
452
503
  {
453
504
  "name": "graph_tests_for",
454
- "description": "Which test files exercise a symbol, derived from a test file importing it or its module. Empty means no test imports it — a finding, not an error.",
505
+ "description": "Test files that exercise a symbol: a test file imports it or its module. Empty means none does — a finding, not an error.",
455
506
  "side_effect": "ro",
456
507
  "implemented_on": [
457
508
  "stdio"
@@ -464,7 +515,10 @@
464
515
  "properties": {
465
516
  "symbol": {
466
517
  "type": "string",
467
- "description": "Node id `<relpath>#<sym>`, label, or free text."
518
+ "description": "Node id, label, or free text."
519
+ },
520
+ "project": {
521
+ "type": "string"
468
522
  }
469
523
  },
470
524
  "additionalProperties": false
@@ -42,7 +42,14 @@ import * as fs from 'node:fs';
42
42
  import * as path from 'node:path';
43
43
 
44
44
  import { changedFiles } from '../code_graph/cli.js';
45
- import { detectSources, graphState, NATIVE_CACHE_REL, pickSource } from '../code_graph/detect.js';
45
+ import { detectSources, graphState, pickSource } from '../code_graph/detect.js';
46
+ import { graphHome } from '../code_graph/graph_home.js';
47
+ import { advisoryImpact, whereUsed } from '../code_graph/estate_cli.js';
48
+ import { openEstate } from '../code_graph/estate_index.js';
49
+ import { touchHome } from '../code_graph/graph_budget.js';
50
+ import { resolveProjectRef } from '../code_graph/project_ref.js';
51
+ import { repoIdentity } from '../code_graph/repo_identity.js';
52
+ import { readRegistry, repoIdOf } from '../_lib/project_registry.js';
46
53
  import { resolvePath } from './path_util.js';
47
54
  import { isTestFile } from '../code_graph/build.js';
48
55
  import {
@@ -65,8 +72,49 @@ import {
65
72
  import { areaReport, graphGeneration, readMap } from '../code_graph/map.js';
66
73
  import type { BuiltinTool } from './tools.js';
67
74
 
68
- /** The `no graph here` answer, in one place so all five agree on its shape. */
75
+ /**
76
+ * Registered projects whose graph exists, for a root that has none (5.2). Read
77
+ * from the local registry only; the graph files themselves are not opened.
78
+ *
79
+ * Any MCP caller can reach this answer, so it never names another repository's
80
+ * path: only another checkout of the server root's own repository is listed
81
+ * with its root, and other projects appear — by id and name — only while the
82
+ * owner has switched the estate index on (O1).
83
+ */
84
+ function projectsWithGraph(root: string): Array<{ id: string; name: string; root?: string }> {
85
+ const reg = readRegistry();
86
+ const self = repoIdentity(root).repoId;
87
+ let real: string;
88
+ try {
89
+ real = fs.realpathSync(root);
90
+ } catch {
91
+ real = path.resolve(root);
92
+ }
93
+ const out: Array<{ id: string; name: string; root?: string }> = [];
94
+ for (const [id, e] of Object.entries(reg.projects)) {
95
+ const same = repoIdOf(id) === self;
96
+ if (!same && !reg.index) continue;
97
+ if (e.root === real || !fs.existsSync(e.root) || !fs.existsSync(graphHome(e.root).cachePath)) continue;
98
+ out.push(same ? { id, name: path.basename(e.root), root: e.root } : { id, name: path.basename(e.root) });
99
+ if (out.length === 20) break;
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** The `no graph here` answer, in one place so all the tools agree on its shape. */
69
105
  function noGraph(root: string): Record<string, unknown> {
106
+ const registered = projectsWithGraph(root);
107
+ if (registered.length) {
108
+ return {
109
+ status: 'unavailable',
110
+ staleness: 'absent',
111
+ error:
112
+ 'no code-graph source under this root — pass `project` to ask a registered project below, ' +
113
+ 'or run `agent-config code-graph build` here',
114
+ root,
115
+ registered_projects: registered,
116
+ };
117
+ }
70
118
  return {
71
119
  status: 'unavailable',
72
120
  staleness: 'absent',
@@ -88,9 +136,10 @@ function withGraph<T extends Record<string, unknown>>(
88
136
  root: string,
89
137
  fn: (g: LoadedGraph) => T,
90
138
  ): Record<string, unknown> {
91
- const picked = pickSource(detectSources(root, path.join(root, NATIVE_CACHE_REL)));
139
+ const picked = pickSource(detectSources(root, graphHome(root).cachePath));
92
140
  if (!picked || picked.kind === 'scip') return noGraph(root);
93
- const g = loadGraph(picked.path, `${picked.kind}:${path.relative(root, picked.path)}`);
141
+ touchHome(root);
142
+ const g = loadGraph(picked.path, `${picked.kind}:${shown(root, picked.path)}`);
94
143
  try {
95
144
  const out = fn(g);
96
145
  // `status` is DERIVED from the answer, never prefixed onto it. An
@@ -107,6 +156,12 @@ function withGraph<T extends Record<string, unknown>>(
107
156
  }
108
157
  }
109
158
 
159
+ /** Relative inside the root, absolute for a graph home outside it (ADR-299). */
160
+ function shown(root: string, p: string): string {
161
+ const rel = path.relative(root, p);
162
+ return rel.startsWith('..') || path.isAbsolute(rel) ? p : rel;
163
+ }
164
+
110
165
  function str(args: Record<string, unknown>, key: string): string {
111
166
  const v = args[key];
112
167
  return typeof v === 'string' ? v.trim() : '';
@@ -143,14 +198,49 @@ function num(args: Record<string, unknown>, key: string, dflt: number): number {
143
198
  return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : dflt;
144
199
  }
145
200
 
146
- export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
201
+ /**
202
+ * Every graph tool takes an optional `project` (5.1): a registered repo id,
203
+ * name or path, resolved through the registry and its graph home. Without it
204
+ * the tool answers about the server root, exactly as before.
205
+ */
206
+ function withProjectParam(tools: Record<string, BuiltinTool>): Record<string, BuiltinTool> {
207
+ const out: Record<string, BuiltinTool> = {};
208
+ for (const [name, t] of Object.entries(tools)) {
209
+ // The estate tool answers across every project already.
210
+ if (name === 'graph_estate') {
211
+ out[name] = t;
212
+ continue;
213
+ }
214
+ const schema = t.input_schema as { properties?: Record<string, unknown> };
215
+ out[name] = {
216
+ ...t,
217
+ input_schema: {
218
+ ...t.input_schema,
219
+ properties: {
220
+ ...(schema.properties ?? {}),
221
+ project: { type: 'string' },
222
+ },
223
+ },
224
+ handler: async (args, root) => {
225
+ const ref = str(args, 'project');
226
+ if (ref === '') return t.handler(args, root);
227
+ const r = resolveProjectRef(ref);
228
+ if ('error' in r) return { status: 'error', error: r.error, candidates: r.candidates };
229
+ return t.handler(args, r.root);
230
+ },
231
+ };
232
+ }
233
+ return out;
234
+ }
235
+
236
+ export const GRAPH_TOOLS: Record<string, BuiltinTool> = withProjectParam({
147
237
  graph_impact: {
148
238
  name: 'graph_impact',
149
239
  side_effect: 'shell',
150
240
  description:
151
241
  'What a diff can break: callers, dependents and test files reaching the ' +
152
- 'changed symbols. Walks only stated-fact edges, never a same-name guess, ' +
153
- 'and reports the guesses refused. Runs `git diff` for the rev.',
242
+ 'changed symbols, over stated-fact edges only (refused guesses reported). ' +
243
+ 'Runs `git diff` for the rev.',
154
244
  input_schema: {
155
245
  type: 'object',
156
246
  required: ['diff'],
@@ -170,7 +260,7 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
170
260
  if (rev === '') return { status: 'error', error: 'diff must be a non-empty git rev' };
171
261
  const files = changedFiles(root, rev);
172
262
  if (files === null) return { status: 'error', error: `cannot resolve rev '${rev}' in ${root}` };
173
- const state = graphState(root);
263
+ const state = graphState(root, graphHome(root).cachePath);
174
264
  return withGraph(root, (g) =>
175
265
  args['untested_only'] === true
176
266
  ? { ...untested(g, files, state), changed_files: files.length }
@@ -183,20 +273,20 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
183
273
  name: 'graph_tests_for',
184
274
  side_effect: 'ro',
185
275
  description:
186
- 'Which test files exercise a symbol, derived from a test file importing ' +
187
- 'it or its module. Empty means no test imports it — a finding, not an error.',
276
+ 'Test files that exercise a symbol: a test file imports it or its module. ' +
277
+ 'Empty means none does — a finding, not an error.',
188
278
  input_schema: {
189
279
  type: 'object',
190
280
  required: ['symbol'],
191
281
  properties: {
192
- symbol: { type: 'string', description: 'Node id `<relpath>#<sym>`, label, or free text.' },
282
+ symbol: { type: 'string', description: 'Node id, label, or free text.' },
193
283
  },
194
284
  additionalProperties: false,
195
285
  },
196
286
  handler: async (args, root) => {
197
287
  const symbol = str(args, 'symbol');
198
288
  if (symbol === '') return { status: 'error', error: 'symbol must be a non-empty string' };
199
- const state = graphState(root);
289
+ const state = graphState(root, graphHome(root).cachePath);
200
290
  return withGraph(root, (g) => ({ ...testsFor(g, symbol, state) }));
201
291
  },
202
292
  },
@@ -205,16 +295,15 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
205
295
  name: 'graph_dead',
206
296
  side_effect: 'ro',
207
297
  description:
208
- 'Symbols with no accepted reference that no declared entry point names. ' +
209
- 'REFUSES (status `refused`) while a source is unreadable: with no ' +
210
- 'exportedness recorded, an exported-but-unimported symbol is ' +
211
- 'indistinguishable from a dead one. Never a deletion recommendation.',
298
+ 'Symbols no accepted reference and no declared entry point names. REFUSES ' +
299
+ '(status `refused`) while a source is unreadable: without recorded exportedness ' +
300
+ 'an exported-but-unimported symbol looks dead. Never a deletion recommendation.',
212
301
  input_schema: {
213
302
  type: 'object',
214
303
  properties: {
215
304
  entry_points: {
216
305
  type: 'string',
217
- description: 'Path under this root to a newline-separated id/label list; `#` comments ok.',
306
+ description: 'Path under this root: newline-separated ids or labels, `#` comments ok.',
218
307
  },
219
308
  accept_missing_exports: {
220
309
  type: 'boolean',
@@ -225,7 +314,7 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
225
314
  additionalProperties: false,
226
315
  },
227
316
  handler: async (args, root) => {
228
- const state = graphState(root);
317
+ const state = graphState(root, graphHome(root).cachePath);
229
318
  const listArg = str(args, 'entry_points');
230
319
  let sources = repoEntryPoints(root);
231
320
  if (listArg !== '') {
@@ -281,22 +370,21 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
281
370
  name: 'graph_query',
282
371
  side_effect: 'ro',
283
372
  description:
284
- "A symbol's direct outgoing relations — calls, imports, uses, inherits — " +
285
- 'each line carrying its confidence and resolution mechanism. For "what ' +
286
- 'does this touch" before reading a file.',
373
+ "A symbol's direct outgoing relations (calls, imports, uses, inherits), each " +
374
+ 'with its confidence and resolution mechanism: "what does this touch" before reading a file.',
287
375
  input_schema: {
288
376
  type: 'object',
289
377
  required: ['symbol'],
290
378
  properties: {
291
379
  symbol: { type: 'string', description: 'Node id, label, or free text.' },
292
- budget: { type: 'integer', minimum: 1, default: 1500, description: 'Token budget for lines.' },
380
+ budget: { type: 'integer', minimum: 1, default: 1500 },
293
381
  },
294
382
  additionalProperties: false,
295
383
  },
296
384
  handler: async (args, root) => {
297
385
  const symbol = str(args, 'symbol');
298
386
  if (symbol === '') return { status: 'error', error: 'symbol must be a non-empty string' };
299
- const staleness = graphState(root);
387
+ const staleness = graphState(root, graphHome(root).cachePath);
300
388
  return withGraph(root, (g) => ({
301
389
  staleness,
302
390
  ...graphQueryVerb(g, symbol, num(args, 'budget', 1500)),
@@ -308,10 +396,9 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
308
396
  name: 'graph_node',
309
397
  side_effect: 'ro',
310
398
  description:
311
- 'One symbol in full: where it is declared, what reaches it, what it ' +
312
- 'reaches, and how connected it is. Resolves EXACTLY (node id, then ' +
313
- 'label) and refuses anything it cannot — it never scores free text, ' +
314
- 'so an answer is always about the node you named.',
399
+ 'One symbol in full: where it is declared, what reaches it, what it reaches, ' +
400
+ 'how connected it is. Resolves EXACTLY (node id, then label) and refuses ' +
401
+ 'anything else — never scores free text.',
315
402
  input_schema: {
316
403
  type: 'object',
317
404
  required: ['id'],
@@ -321,13 +408,13 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
321
408
  type: 'string',
322
409
  enum: ['in', 'out', 'both'],
323
410
  default: 'both',
324
- description: 'Which edges to walk. `in` = who reaches it, `out` = what it reaches.',
411
+ description: '`in` = who reaches it, `out` = what it reaches.',
325
412
  },
326
413
  depth: { type: 'integer', minimum: 1, maximum: 3, default: 1, description: 'Hop limit.' },
327
414
  relation: {
328
415
  type: 'string',
329
416
  enum: RELATIONS,
330
- description: 'Restrict to one relation. Omit for all of them.',
417
+ description: 'One relation only; omit for all.',
331
418
  },
332
419
  },
333
420
  additionalProperties: false,
@@ -335,7 +422,7 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
335
422
  handler: async (args, root) => {
336
423
  const id = str(args, 'id');
337
424
  if (id === '') return { status: 'error', error: 'id must be a non-empty string' };
338
- const staleness = graphState(root);
425
+ const staleness = graphState(root, graphHome(root).cachePath);
339
426
  const opts: NodeOptions = {};
340
427
  const dir = str(args, 'direction');
341
428
  if (dir === 'in' || dir === 'out' || dir === 'both') opts.direction = dir;
@@ -353,16 +440,15 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
353
440
  name: 'graph_path',
354
441
  side_effect: 'ro',
355
442
  description:
356
- 'The shortest relation chain between two symbols, direction preserved. ' +
357
- 'Answers "how does A reach B" without reading the files between. ' +
358
- '`(no path found)` when unconnected is an answer.',
443
+ 'The shortest relation chain between two symbols, direction kept: how A reaches ' +
444
+ 'B without reading the files between. `(no path found)` is an answer.',
359
445
  input_schema: {
360
446
  type: 'object',
361
447
  required: ['from', 'to'],
362
448
  properties: {
363
449
  from: { type: 'string', description: 'Start node id, label, or free text.' },
364
450
  to: { type: 'string', description: 'Goal node id, label, or free text.' },
365
- budget: { type: 'integer', minimum: 1, default: 1500, description: 'Token budget for lines.' },
451
+ budget: { type: 'integer', minimum: 1, default: 1500 },
366
452
  },
367
453
  additionalProperties: false,
368
454
  },
@@ -370,7 +456,7 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
370
456
  const from = str(args, 'from');
371
457
  const to = str(args, 'to');
372
458
  if (from === '' || to === '') return { status: 'error', error: 'from and to must be non-empty strings' };
373
- const staleness = graphState(root);
459
+ const staleness = graphState(root, graphHome(root).cachePath);
374
460
  return withGraph(root, (g) => ({
375
461
  staleness,
376
462
  ...graphPathVerb(g, from, to, num(args, 'budget', 1500)),
@@ -383,7 +469,7 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
383
469
  description: 'Is there a code map (agents/map/)? Its freshness, counts and MAP.md path.',
384
470
  input_schema: { type: 'object', properties: {}, additionalProperties: false },
385
471
  handler: async (_args, root) => {
386
- const staleness = graphState(root);
472
+ const staleness = graphState(root, graphHome(root).cachePath);
387
473
  const map = readMap(root);
388
474
  if (!map) {
389
475
  return {
@@ -394,6 +480,8 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
394
480
  };
395
481
  }
396
482
  const m = map.model;
483
+ const home = graphHome(root);
484
+ const mapRel = home.kind === 'project' ? 'agents/map' : home.mapDir;
397
485
  return {
398
486
  status: 'ok',
399
487
  staleness,
@@ -401,7 +489,7 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
401
489
  drawn_at_commit: map.meta.commit,
402
490
  graph_state_when_drawn: map.meta.state,
403
491
  // Same generation → the map shows exactly the graph the queries read.
404
- map_matches_graph: map.meta.graph_generation !== undefined && map.meta.graph_generation === graphGeneration(path.join(root, NATIVE_CACHE_REL)),
492
+ map_matches_graph: map.meta.graph_generation !== undefined && map.meta.graph_generation === graphGeneration(home.cachePath),
405
493
  files: m.files.length,
406
494
  areas: m.areas.length,
407
495
  trusted_links: m.analyses.trustedEdges,
@@ -409,8 +497,8 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
409
497
  request_paths: m.analyses.requestPathsTotal,
410
498
  providers: (m.providers ?? []).map((p) => `${p.id} (${p.status})`),
411
499
  largest_areas: m.areas.slice(0, 5).map((a) => `${a.id} ${a.name} (${a.files.length})`),
412
- map_md: 'agents/map/MAP.md',
413
- index_html: 'agents/map/index.html',
500
+ map_md: `${mapRel}/MAP.md`,
501
+ index_html: `${mapRel}/index.html`,
414
502
  };
415
503
  },
416
504
  },
@@ -431,9 +519,41 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
431
519
  if (q === '') return { status: 'error', error: 'area must be a non-empty string' };
432
520
  const map = readMap(root);
433
521
  if (!map) return { status: 'unavailable', error: 'no map — run `agent-config code-graph map`' };
434
- const r = areaReport(map.model, q);
522
+ const home = graphHome(root);
523
+ const r = areaReport(map.model, q, home.kind === 'project' ? undefined : home.mapDir);
435
524
  if ('refusal' in r) return { status: 'refused', refusal: r.refusal, candidates: r.candidates };
436
525
  return { status: 'ok', drawn_at_commit: map.meta.commit, ...r };
437
526
  },
438
527
  },
439
- };
528
+ graph_estate: {
529
+ name: 'graph_estate',
530
+ side_effect: 'ro',
531
+ description:
532
+ 'Estate index across registered projects: package users, where a name is used, advisory ' +
533
+ 'reach. Other graph tools take `project`.',
534
+ input_schema: {
535
+ type: 'object',
536
+ required: ['query'],
537
+ properties: {
538
+ query: { type: 'string', enum: ['where-used', 'projects-using', 'advisory-impact'] },
539
+ name: { type: 'string' },
540
+ },
541
+ additionalProperties: false,
542
+ },
543
+ handler: async (args) => {
544
+ const q = str(args, 'query');
545
+ const name = str(args, 'name');
546
+ if (q !== 'advisory-impact' && name === '') return { status: 'error', error: `${q || 'query'} needs a name` };
547
+ const db = openEstate();
548
+ if (!db) return { status: 'unavailable', error: 'no estate index — `agent-config code-graph projects index on`, then refresh each project' };
549
+ try {
550
+ if (q === 'projects-using') return { status: 'ok', projects: db.projectsUsing(name) };
551
+ if (q === 'where-used') return { status: 'ok', ...whereUsed(db, name) };
552
+ if (q === 'advisory-impact') return { status: 'ok', impact: advisoryImpact(db, name === '' ? null : name) };
553
+ return { status: 'error', error: `unknown query: ${q}` };
554
+ } finally {
555
+ db.close();
556
+ }
557
+ },
558
+ },
559
+ });
@@ -24,6 +24,7 @@ import { isIP } from 'node:net';
24
24
  import * as path from 'node:path';
25
25
 
26
26
  import { encrypt } from './_lib/link_crypto.js';
27
+ import { sourceId } from './_lib/source_id.js';
27
28
  import { sanitize_text } from './_lib/retrieval_sanitize.js';
28
29
  import { SourceError, resolveSource, type SourceRef } from './source_adapters.js';
29
30
 
@@ -207,26 +208,43 @@ export function quotedAnchor(text: string, needle: string): { line: number; quot
207
208
  return { line: i + 1, quote: words.slice(start, start + MAX_QUOTE_WORDS).join(' ') };
208
209
  }
209
210
 
210
- /** Codes source identities S1, S2, …; the identities leave only as ENC1 tokens in the round's intake.md. */
211
+ /**
212
+ * Codes source identities; the identities leave only as ENC1 tokens in the round's intake.md.
213
+ * With an id key the codes are stable `src_…` ids (`_lib/source_id.ts`), equal across rounds;
214
+ * without one they are `S1, S2, …`, meaningful inside this round only.
215
+ */
211
216
  export class SourceCodebook {
212
217
  private readonly ids: string[] = [];
218
+ private readonly codes: string[] = [];
213
219
  private readonly tokens: string[] = [];
220
+ private readonly idKey: string | null;
214
221
 
215
- constructor(private readonly key: string) {}
222
+ constructor(
223
+ private readonly key: string,
224
+ opts: { idKey?: string | null } = {},
225
+ ) {
226
+ this.idKey = opts.idKey ? opts.idKey : null;
227
+ }
216
228
 
217
229
  code(identity: string): string {
218
- let i = this.ids.indexOf(identity);
230
+ const stable = this.idKey === null ? null : sourceId(identity, this.idKey);
231
+ let i = stable === null ? this.ids.indexOf(identity) : this.codes.indexOf(stable);
219
232
  if (i === -1) {
220
233
  this.ids.push(identity);
234
+ this.codes.push(stable ?? `S${String(this.ids.length)}`);
221
235
  this.tokens.push(encrypt(identity, this.key));
222
236
  i = this.ids.length - 1;
223
237
  }
224
- return `S${String(i + 1)}`;
238
+ return this.codes[i] as string;
225
239
  }
226
240
 
227
241
  intakeMarkdown(): string {
228
- const rows = this.tokens.map((token, i) => `- S${String(i + 1)}: ${token}`);
229
- return ['# Sources', '', 'Identities are encrypted (`link_crypto`); tracked output names only the codes.', '', ...rows, ''].join('\n');
242
+ const rows = this.tokens.map((token, i) => `- ${this.codes[i] as string}: ${token}`);
243
+ const scope =
244
+ this.idKey === null
245
+ ? 'Codes are per-round (S1, S2, …) — no id key was available, so they do not match another round.'
246
+ : 'Codes are stable source ids (src_…), equal across rounds.';
247
+ return ['# Sources', '', 'Identities are encrypted (`link_crypto`); tracked output names only the codes.', scope, '', ...rows, ''].join('\n');
230
248
  }
231
249
 
232
250
  writeIntake(roundDir: string): void {