@event4u/agent-config 17.2.1 → 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 (225) 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/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -6,8 +6,17 @@
6
6
  * validate [--graph P] schema-gate a graph.json
7
7
  * detect [--root P] [--format json] three-state freshness verdict
8
8
  * refresh [--root P] [--out P] [--budget-seconds N]
9
- * build --update (STALE) / no-op
10
- * (FRESH) / full build (ABSENT)
9
+ * [--single-flight] [--map] build --update (STALE) / no-op
10
+ * (FRESH) / full build (ABSENT);
11
+ * a result built while HEAD moved
12
+ * is discarded and retried once
13
+ * auto-refresh on|off|status [--root P] opt this project in to the
14
+ * session-start background refresh
15
+ * projects list|add|remove|prune the user-global registry of
16
+ * projects this machine works in
17
+ * (ADR-299, `projects_cli.ts`)
18
+ * estate where-used|projects-using| estate questions from the local
19
+ * advisory-impact index alone (`estate_cli.ts`)
11
20
  *
12
21
  * `detect --format json` (Phase 2, road-to-reachable-code-memory) emits a
13
22
  * single-line, stable-key-order JSON verdict on stdout — no timestamps, safe
@@ -25,8 +34,10 @@
25
34
  * completed temp output — on budget-exceeded, build failure, or the child
26
35
  * never producing output, the rename never happens and the OLD cache is left
27
36
  * byte-for-byte untouched. The cache itself (default
28
- * `agents/runtime/state/code-graph-v1.json`) is local-only and disposable —
29
- * gitignored, rebuildable from source at any time; never a source of truth.
37
+ * `agents/runtime/state/code-graph-v1.json`; for a root without `agents/` or a
38
+ * project install, the user-global `runtime/graphs/<repo id>/state/`, ADR-299)
39
+ * is local-only and disposable — gitignored, rebuildable from source at any
40
+ * time; never a source of truth.
30
41
  *
31
42
  * impact --diff <rev> [--depth N] callers/dependents/test files
32
43
  * reachable from the changed
@@ -42,16 +53,24 @@
42
53
  * three-state staleness — so a caller can tell "no callers" from "no callers I
43
54
  * would trust", and "dead" from "dead as of a graph 40 commits behind".
44
55
  *
56
+ * Every verb that reads a graph also takes `--project <id|name|path>`: a
57
+ * project in the user-global registry, resolved to its root and graph home
58
+ * (ADR-299). Only a registered project resolves.
59
+ *
45
60
  * Query subcommands (query/path/explain/affected) are added by the Phase-3
46
61
  * query tier. Deterministic, LLM-free, no network. Exit codes:
47
62
  * 0 ok · 1 not-found / validation-failed / refresh-budget-exceeded ·
48
63
  * 2 usage · 3 internal.
49
64
  *
50
65
  * map [--root P] [--out DIR] [--open] draw the project map into
51
- * [--no-verify] [--diff <rev>] agents/map/ (ADR-297); renders it
66
+ * [--no-verify] [--diff <rev>] agents/map/ (ADR-297), or the
67
+ * graph home's map/ (ADR-299); renders it
52
68
  * [--prs] (open PRs by area, via gh)
53
69
  * at three widths when Playwright
54
70
  * is present
71
+ * map --estate [--out DIR] [--no-verify] one page of the registered projects
72
+ * and their links, under the user-global
73
+ * home; refused inside a git work tree
55
74
  *
56
75
  * suggest-verb "<question>" (Phase 9 D3) print the verb
57
76
  * (query/affected/path/explain)
@@ -62,7 +81,7 @@
62
81
  * graph.
63
82
  */
64
83
  import { execFileSync, spawnSync } from 'node:child_process';
65
- import { randomBytes } from 'node:crypto';
84
+ import { createHash, randomBytes } from 'node:crypto';
66
85
  import * as fs from 'node:fs';
67
86
  import * as path from 'node:path';
68
87
  import process from 'node:process';
@@ -70,17 +89,32 @@ import { fileURLToPath } from 'node:url';
70
89
 
71
90
  import { resolve_project_root } from '../_lib/agent_settings.js';
72
91
  import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
73
- import { buildFromRepo, isTestFile, sidecarPath } from './build.js';
92
+ import { buildFromRepo, discoverFiles, isTestFile, sidecarPath } from './build.js';
93
+ import { DOC_EXTS } from './docs_layer.js';
74
94
  import {
75
95
  computeVerdict,
76
96
  detectSources,
77
97
  type GraphState,
78
98
  graphState,
79
- NATIVE_CACHE_REL,
80
99
  pickSource,
81
100
  } from './detect.js';
101
+ import { graphHome } from './graph_home.js';
82
102
  import { suggestVerb } from './intent.js';
83
- import { buildMap, recordOutcome } from './map.js';
103
+ import { estateMain } from './estate_cli.js';
104
+ import { estateMapMain } from './estate_map.js';
105
+ import { touchHome } from './graph_budget.js';
106
+ import { resolveProjectRef } from './project_ref.js';
107
+ import { projectsMain, registerUse } from './projects_cli.js';
108
+ import { buildMap, graphGeneration, mapGeneration, recordOutcome } from './map.js';
109
+ import { cliPackage, usableCli } from './refresh_cli.js';
110
+ import {
111
+ STALE_LOCK_MS,
112
+ acquire as acquireRefreshLock,
113
+ pidAlive,
114
+ readRefreshRecords,
115
+ reclaimMutexPath,
116
+ recordRefresh,
117
+ } from './refresh_lock.js';
84
118
  import { verifyMap } from './map_verify.js';
85
119
  import {
86
120
  affected,
@@ -107,24 +141,34 @@ import {
107
141
  untested,
108
142
  type UntestedResult,
109
143
  } from './verbs.js';
144
+ import { main as lineageMain } from './lineage_cli.js';
110
145
 
111
146
  const _HERE = fileURLToPath(import.meta.url);
112
147
  export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
113
148
  /**
114
- * The cache belongs to the project it describes: `<root>/agents/runtime/state/`,
115
- * the path MCP, the context hook and `detect` already read. Anchoring it to
149
+ * The cache belongs to the project it describes: `<root>/agents/runtime/state/`
150
+ * when the root has a project home, its user-global graph home otherwise
151
+ * (ADR-299) — the path MCP, the context hook and `detect` read. Anchoring it to
116
152
  * the package tree put a consumer's graph inside `node_modules`, where `npm ci`
117
153
  * deleted it, two projects sharing an install overwrote each other, and MCP
118
154
  * answered "no graph" right after the CLI reported a fresh one.
119
155
  */
120
156
  export function cacheFor(root: string): string {
121
- return path.join(root, NATIVE_CACHE_REL);
157
+ return graphHome(root).cachePath;
158
+ }
159
+ /** A path relative to the root when it lies inside it; a graph home outside the root prints in full. */
160
+ function shown(root: string, p: string): string {
161
+ const rel = path.relative(root, p);
162
+ return rel.startsWith('..') || path.isAbsolute(rel) ? p : rel;
122
163
  }
123
164
  /** Local `tsx` binary — `refresh` spawns `build` as a child of THIS file
124
165
  * through it, so a budget-exceeded kill only ever terminates the child. */
125
166
  const TSX_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
126
167
  const DEFAULT_BUDGET_SECONDS = 60;
127
168
 
169
+ /** A refusal that names what the caller asked for wrongly; exits 1, not 3. */
170
+ class UsageError extends Error {}
171
+
128
172
  function flag(argv: string[], name: string): string | null {
129
173
  const i = argv.indexOf(name);
130
174
  return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
@@ -143,8 +187,13 @@ function flag(argv: string[], name: string): string | null {
143
187
  */
144
188
  function resolveRoot(argv: string[]): string {
145
189
  const explicit = flag(argv, '--root');
190
+ const project = flag(argv, '--project');
146
191
  let candidate: string;
147
- if (explicit !== null) {
192
+ if (project !== null) {
193
+ const r = resolveProjectRef(project);
194
+ if ('error' in r) throw new UsageError(r.error);
195
+ candidate = r.root;
196
+ } else if (explicit !== null) {
148
197
  candidate = explicit;
149
198
  } else {
150
199
  try {
@@ -183,6 +232,7 @@ async function cmdBuild(argv: string[]): Promise<number> {
183
232
  }
184
233
  const update = argv.includes('--update');
185
234
  const { graph, fileCount, reExtracted, reused } = await buildFromRepo(root, out, { update });
235
+ if (out === cacheFor(root)) registerUse(root, graph, git(root, 'rev-parse', 'HEAD'));
186
236
  const c = graph.edge_confidence_counts;
187
237
  process.stdout.write(
188
238
  `✅ code-graph ${update ? 'updated' : 'built'} — ${fileCount} files · ${graph.nodes.length} nodes · ${graph.edges.length} edges\n` +
@@ -190,7 +240,7 @@ async function cmdBuild(argv: string[]): Promise<number> {
190
240
  ` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
191
241
  ` resolved_via: ${resolvedViaHistogram(graph)}\n` +
192
242
  (update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
193
- (out ? ` cache: ${path.relative(root, out)}\n` : '') +
243
+ (out ? ` cache: ${shown(root, out)}\n` : '') +
194
244
  legacyCacheNotice(root),
195
245
  );
196
246
  return 0;
@@ -247,12 +297,12 @@ function cmdValidate(argv: string[]): number {
247
297
  // content-addressed cache key, so agreement here means the twin was emitted
248
298
  // from THIS graph and not merely from a graph.
249
299
  const twinState = twinChecksumState(p, (parsed as CodeGraph).source_checksum);
250
- const rel = path.relative(root, p);
300
+ const rel = shown(root, p);
251
301
  if (twinState.kind === 'mismatch') {
252
302
  process.stderr.write(
253
303
  `❌ SQLite twin disagrees with the canonical JSON: twin ${twinState.twin} ≠ json ${twinState.json}\n` +
254
304
  ' The twin is derived and disposable — delete it and re-run `code-graph build`.\n' +
255
- ` ${path.relative(root, sqliteTwinPath(p))}\n`,
305
+ ` ${shown(root, sqliteTwinPath(p))}\n`,
256
306
  );
257
307
  return 1;
258
308
  }
@@ -295,7 +345,8 @@ function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err:
295
345
  if (!picked) return { err: 'no code-graph source found — run `code_graph build` or ship a graph.json' };
296
346
  if (picked.kind === 'scip') return { err: `${picked.note ?? 'SCIP detected'} — run \`code-graph build\` to merge it into the native graph` };
297
347
  const staleNote = picked.stale ? ` ⚠ index is ${picked.commits_behind ?? 'N'} commit(s) behind — rebuild before trusting` : '';
298
- return { g: loadGraph(picked.path, `${picked.kind}:${path.relative(root, picked.path)}`), note: staleNote };
348
+ touchHome(root);
349
+ return { g: loadGraph(picked.path, `${picked.kind}:${shown(root, picked.path)}`), note: staleNote };
299
350
  }
300
351
 
301
352
  function render(r: QueryResult, staleNote: string): number {
@@ -415,7 +466,8 @@ function isFlagValue(argv: string[], token: string): boolean {
415
466
  */
416
467
  function stateOf(argv: string[], answeredBy?: string): GraphState {
417
468
  const explicit = flag(argv, '--graph');
418
- return graphState(resolveRoot(argv), explicit ?? answeredBy);
469
+ const root = resolveRoot(argv);
470
+ return graphState(root, explicit ?? answeredBy ?? cacheFor(root));
419
471
  }
420
472
 
421
473
  /** The two histogram lines + the staleness line, in one place so the four
@@ -621,7 +673,7 @@ function cmdDetect(argv: string[]): number {
621
673
  }
622
674
  for (const v of verdicts) {
623
675
  const fresh = v.stale === undefined ? '' : v.stale ? ` · STALE${v.commits_behind !== undefined ? ` (${v.commits_behind} behind)` : ''}` : ' · fresh';
624
- process.stdout.write(`${v.kind.padEnd(9)} ${path.relative(root, v.path)}${fresh}${v.note ? ` · ${v.note}` : ''}\n`);
676
+ process.stdout.write(`${v.kind.padEnd(9)} ${shown(root, v.path)}${fresh}${v.note ? ` · ${v.note}` : ''}\n`);
625
677
  }
626
678
  const picked = pickSource(verdicts);
627
679
  if (picked) process.stdout.write(`→ query would use: ${picked.kind}\n`);
@@ -635,10 +687,21 @@ function budgetSecondsOf(argv: string[]): number {
635
687
  return Number.isFinite(n) ? n : DEFAULT_BUDGET_SECONDS;
636
688
  }
637
689
 
638
- /** Best-effort removal of a temp build output + its sidecar. Never throws —
639
- * cleanup failure must not mask the real result (budget-exceeded / error). */
640
- function cleanupTemp(tmpOut: string): void {
641
- for (const p of [tmpOut, sidecarPath(tmpOut)]) {
690
+ /** Best-effort removal of a temp build output, its sidecar, its SQLite twin and
691
+ * the twin's own half-written temp. Never throws — cleanup failure must not mask
692
+ * the real result (budget-exceeded / error). */
693
+ export function cleanupTemp(tmpOut: string): void {
694
+ const twin = sqliteTwinPath(tmpOut);
695
+ let twinTemps: string[] = [];
696
+ try {
697
+ twinTemps = fs
698
+ .readdirSync(path.dirname(tmpOut))
699
+ .filter((f) => f.startsWith(`${path.basename(twin)}.tmp-`))
700
+ .map((f) => path.join(path.dirname(tmpOut), f));
701
+ } catch {
702
+ /* the directory is gone, so nothing in it is left to remove */
703
+ }
704
+ for (const p of [tmpOut, sidecarPath(tmpOut), twin, ...twinTemps]) {
642
705
  try {
643
706
  fs.unlinkSync(p);
644
707
  } catch {
@@ -647,6 +710,33 @@ function cleanupTemp(tmpOut: string): void {
647
710
  }
648
711
  }
649
712
 
713
+ /**
714
+ * Remove what a refresh killed before its own cleanup left behind. A file is
715
+ * only an orphan when the pid in its name is gone AND it is older than a stale
716
+ * lock: the pid check alone would race a refresh that is starting right now.
717
+ */
718
+ function sweepOrphanedTemps(out: string): void {
719
+ const dir = path.dirname(out);
720
+ const prefix = `.${path.basename(out)}.refresh-tmp-`;
721
+ let names: string[];
722
+ try {
723
+ names = fs.readdirSync(dir);
724
+ } catch {
725
+ return;
726
+ }
727
+ for (const name of names) {
728
+ if (!name.startsWith(prefix)) continue;
729
+ const pid = Number(name.slice(prefix.length).split('-')[0]);
730
+ if (!Number.isInteger(pid) || pid <= 0 || pidAlive(pid)) continue;
731
+ const full = path.join(dir, name);
732
+ try {
733
+ if (Date.now() - fs.statSync(full).mtimeMs > STALE_LOCK_MS) fs.unlinkSync(full);
734
+ } catch {
735
+ /* raced another sweeper — fine */
736
+ }
737
+ }
738
+ }
739
+
650
740
  /**
651
741
  * Run `build` (optionally `--update`) as a CHILD process against a temp
652
742
  * `--out` path, under a hard wall-clock budget. Only on a clean, in-budget
@@ -655,6 +745,64 @@ function cleanupTemp(tmpOut: string): void {
655
745
  * itself uses. Any other outcome (timeout, non-zero exit, missing output)
656
746
  * cleans up the temp files and leaves `out` untouched.
657
747
  */
748
+ function git(root: string, ...args: string[]): string | null {
749
+ try {
750
+ return execFileSync('git', ['-C', root, ...args], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], env: hardenedSpawnEnv() }).trim();
751
+ } catch {
752
+ return null;
753
+ }
754
+ }
755
+
756
+ /**
757
+ * HEAD; `''` when no ancestor of the CANONICAL root holds `.git`; `null` when
758
+ * one does and HEAD cannot be read, or when the root or an ancestor cannot be
759
+ * inspected. "Not a repository" is read from the filesystem, never from a git
760
+ * probe, and is walked from the realpath so a symlinked root cannot hide the
761
+ * repository git itself would find.
762
+ */
763
+ function headOf(root: string): string | null {
764
+ let dir: string;
765
+ try {
766
+ dir = fs.realpathSync(root);
767
+ } catch {
768
+ return null;
769
+ }
770
+ for (;;) {
771
+ try {
772
+ fs.lstatSync(path.join(dir, '.git'));
773
+ return git(dir, 'rev-parse', 'HEAD');
774
+ } catch (e) {
775
+ if ((e as NodeJS.ErrnoException).code !== 'ENOENT') return null;
776
+ }
777
+ if (path.dirname(dir) === dir) return '';
778
+ dir = path.dirname(dir);
779
+ }
780
+ }
781
+
782
+ /**
783
+ * A stamp of the files a build reads — path, size and modification time of every
784
+ * source and document file. HEAD alone misses an edit to the working tree made
785
+ * while the build ran (a second agent saving a file), which leaves a graph of
786
+ * content that never existed together. Best-effort, not definitive: an edit
787
+ * that keeps the size and lands within the filesystem's mtime resolution (or
788
+ * restores the mtime) is not seen; hashing every file's content would be.
789
+ */
790
+ function treeStamp(root: string): string {
791
+ const h = createHash('sha256');
792
+ for (const full of [...discoverFiles(root), ...discoverFiles(root, DOC_EXTS)]) {
793
+ try {
794
+ const st = fs.statSync(full);
795
+ h.update(`${full}\0${st.size}\0${st.mtimeMs}\n`);
796
+ } catch {
797
+ h.update(`${full}\0gone\n`);
798
+ }
799
+ }
800
+ return h.digest('hex');
801
+ }
802
+
803
+ /** Returned by {@link runBudgetedBuild} when HEAD or the working tree moved while the child built. */
804
+ const HEAD_MOVED = -1;
805
+
658
806
  function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeconds: number): number {
659
807
  if (!(budgetSeconds > 0)) {
660
808
  // Node's own `timeout` option treats 0 as "no limit" — a genuinely
@@ -664,6 +812,7 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
664
812
  return 1;
665
813
  }
666
814
  const dir = path.dirname(out);
815
+ sweepOrphanedTemps(out);
667
816
  const tmpOut = path.join(dir, `.${path.basename(out)}.refresh-tmp-${process.pid}-${randomBytes(4).toString('hex')}.json`);
668
817
  if (update) {
669
818
  const realSidecar = sidecarPath(out);
@@ -682,6 +831,8 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
682
831
  const [cmd, cmdArgs] = fs.existsSync(TSX_BIN)
683
832
  ? [TSX_BIN, [_HERE, ...args]]
684
833
  : [process.execPath, [...process.execArgv, _HERE, ...args]];
834
+ const headBefore = headOf(root);
835
+ const treeBefore = treeStamp(root);
685
836
  const res = spawnSync(cmd, cmdArgs, {
686
837
  env: hardenedSpawnEnv(),
687
838
  stdio: 'ignore',
@@ -705,14 +856,38 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
705
856
  process.stderr.write('code-graph: refresh produced no output — old cache kept\n');
706
857
  return 3;
707
858
  }
859
+ // A checkout or rebase that lands mid-build leaves a graph of a tree that
860
+ // existed only in passing; publishing it would label mixed content fresh.
861
+ // An unreadable HEAD is not evidence it held still, so it fails the same way.
862
+ if (headBefore === null || headOf(root) !== headBefore) {
863
+ cleanupTemp(tmpOut);
864
+ process.stderr.write('code-graph: HEAD moved (or could not be read) during the refresh — result discarded\n');
865
+ return HEAD_MOVED;
866
+ }
867
+ if (treeStamp(root) !== treeBefore) {
868
+ cleanupTemp(tmpOut);
869
+ process.stderr.write('code-graph: files changed during the refresh — result discarded\n');
870
+ return HEAD_MOVED;
871
+ }
708
872
  fs.renameSync(tmpOut, out); // same-dir → atomic; real cache only ever changes here
709
873
  const tmpSidecar = sidecarPath(tmpOut);
710
874
  if (fs.existsSync(tmpSidecar)) fs.renameSync(tmpSidecar, sidecarPath(out));
711
- process.stdout.write(`✅ code-graph refreshed — cache: ${path.relative(root, out)}\n`);
875
+ // The twin records the JSON's size and mtime; a rename keeps both, so the
876
+ // promoted twin matches the promoted JSON. Without a twin, the old one goes:
877
+ // it describes a JSON that no longer exists.
878
+ const tmpTwin = sqliteTwinPath(tmpOut);
879
+ if (fs.existsSync(tmpTwin)) fs.renameSync(tmpTwin, sqliteTwinPath(out));
880
+ else fs.rmSync(sqliteTwinPath(out), { force: true });
881
+ process.stdout.write(`✅ code-graph refreshed — cache: ${shown(root, out)}\n`);
712
882
  return 0;
713
883
  }
714
884
 
715
- function cmdRefresh(argv: string[]): number {
885
+ /**
886
+ * `--single-flight` takes the lock the git-hook refresh block takes and exits 0
887
+ * when another refresh holds it. `--map` redraws `agents/map/` afterwards, only
888
+ * when the project already has one and it is older than the cache.
889
+ */
890
+ async function cmdRefresh(argv: string[]): Promise<number> {
716
891
  const root = resolveRoot(argv);
717
892
  const out = flag(argv, '--out') ?? cacheFor(root);
718
893
  const budgetSeconds = budgetSecondsOf(argv);
@@ -720,12 +895,137 @@ function cmdRefresh(argv: string[]): number {
720
895
  process.stderr.write(`code-graph: root not found: ${root}\n`);
721
896
  return 1;
722
897
  }
723
- const v = computeVerdict(root, out);
724
- if (v.verdict === 'FRESH') {
725
- process.stdout.write('fresh — nothing to do\n');
898
+ // `--map` writes the same files a manual `map` does, so it is never drawn unlocked.
899
+ const locked = argv.includes('--single-flight') || argv.includes('--map');
900
+ const release = locked ? acquireRefreshLock(root) : () => {};
901
+ const record = (outcome: string, detail?: string): void => {
902
+ if (locked) recordRefresh(root, { by: 'refresh', outcome, ...(detail ? { detail } : {}) });
903
+ };
904
+ if (release === null) {
905
+ process.stdout.write('another refresh is running — nothing to do\n');
906
+ record('locked');
726
907
  return 0;
727
908
  }
728
- return runBudgetedBuild(root, out, v.verdict === 'STALE', budgetSeconds);
909
+ try {
910
+ let rc = HEAD_MOVED;
911
+ // One retry: a second move inside one build window means the tree is
912
+ // still in motion, and the next hook or session start will refresh it.
913
+ for (let attempt = 0; attempt < 2 && rc === HEAD_MOVED; attempt++) {
914
+ const v = computeVerdict(root, out);
915
+ if (v.verdict === 'FRESH') {
916
+ process.stdout.write('fresh — nothing to do\n');
917
+ rc = 0;
918
+ break;
919
+ }
920
+ rc = runBudgetedBuild(root, out, v.verdict === 'STALE', budgetSeconds);
921
+ }
922
+ if (rc === HEAD_MOVED) {
923
+ record('tree-moved', 'HEAD or files changed during both attempts; the next refresh retries');
924
+ return 1;
925
+ }
926
+ if (rc !== 0) {
927
+ record('failed', `build exited ${rc}`);
928
+ return rc;
929
+ }
930
+ if (out === cacheFor(root)) registerUse(root, null, git(root, 'rev-parse', 'HEAD'));
931
+ if (argv.includes('--map') && mapIsBehind(root, out)) {
932
+ const mrc = await drawMap(['--root', root, '--no-build', '--no-verify']);
933
+ record(mrc === 0 ? 'refreshed-with-map' : 'map-failed', mrc === 0 ? undefined : `map exited ${mrc}`);
934
+ return mrc;
935
+ }
936
+ record('refreshed');
937
+ return rc;
938
+ } finally {
939
+ release();
940
+ }
941
+ }
942
+
943
+ /**
944
+ * `auto-refresh on|off|status` — the explicit, local, per-project opt-in the
945
+ * `code-graph-refresh` session concern requires before it starts anything
946
+ * (ratification round of 2026-10-23: an existing cache is not consent).
947
+ */
948
+ function cmdAutoRefresh(argv: string[]): number {
949
+ const root = resolveRoot(argv);
950
+ const action = argv.find((a) => !a.startsWith('--') && !isFlagValue(argv, a)) ?? 'status';
951
+ const home = graphHome(root);
952
+ const marker = home.autoRefreshPath;
953
+ if (action === 'on') {
954
+ if (home.kind === 'global') {
955
+ // The session concern serves project homes only; its chunk cannot carry the
956
+ // user-global resolver, so an opt-in here would be a promise nothing keeps.
957
+ process.stderr.write(
958
+ `code-graph: auto-refresh needs a project home (agents/ or a project install); ${root} has none.\n` +
959
+ 'Its graph lives in the user-global home — refresh it with `code-graph refresh`.\n',
960
+ );
961
+ return 1;
962
+ }
963
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
964
+ fs.writeFileSync(marker, `${JSON.stringify({ enabled_at: new Date().toISOString() })}\n`);
965
+ const cli = usableCli(root);
966
+ process.stdout.write(
967
+ 'auto-refresh on — each agent session start refreshes the graph, and agents/map/ when it exists,\n' +
968
+ 'in the background (POSIX hosts). `code-graph auto-refresh off` stops it.\n' +
969
+ 'The opt-in is per working tree: run it again in each new git worktree.\n' +
970
+ (fs.existsSync(cacheFor(root)) ? '' : 'No graph yet: `code-graph build` or `code-graph map` creates one.\n'),
971
+ );
972
+ if ('reason' in cli) {
973
+ process.stderr.write(
974
+ cli.reason === 'no-cli'
975
+ ? 'warning: no agent-config outside this repository is on PATH, so nothing will run yet — install it globally (npm i -g @event4u/agent-config).\n'
976
+ : `warning: ${cli.cli} (version ${cliPackage(cli.cli)?.version ?? 'unknown'}) is too old for the single-flight refresh, so nothing will run yet — update it (npm i -g @event4u/agent-config).\n`,
977
+ );
978
+ }
979
+ return 0;
980
+ }
981
+ if (action === 'off') {
982
+ fs.rmSync(marker, { force: true });
983
+ process.stdout.write('auto-refresh off\n');
984
+ return 0;
985
+ }
986
+ if (action !== 'status') {
987
+ process.stderr.write('usage: code_graph/cli.ts auto-refresh on|off|status [--root P]\n');
988
+ return 2;
989
+ }
990
+ const cli = usableCli(root);
991
+ const last = readRefreshRecords(root);
992
+ const line = (r: (typeof last)['refresh']): string => (r ? `${r.outcome} at ${r.at}${r.detail ? ` (${r.detail})` : ''}` : 'never');
993
+ const mapDir = home.mapDir;
994
+ const gen = graphGeneration(cacheFor(root));
995
+ const drawnFrom = mapGeneration(mapDir);
996
+ const mutex = reclaimMutexPath(home.lockPath);
997
+ process.stdout.write(
998
+ `auto-refresh: ${fs.existsSync(marker) ? 'on' : 'off'}\n` +
999
+ `cli: ${'reason' in cli ? (cli.reason === 'no-cli' ? 'none usable — nothing will run' : `${cli.cli} ${cliPackage(cli.cli)?.version ?? '?'} — too old, nothing will run`) : `${cli.cli} ${cliPackage(cli.cli)?.version ?? '?'}`}\n` +
1000
+ `last session start: ${line(last['session-start'])}\n` +
1001
+ `last refresh: ${line(last.refresh)}\n` +
1002
+ `graph: ${computeVerdict(root, cacheFor(root)).verdict}\n` +
1003
+ `map: ${!fs.existsSync(mapDir) ? 'absent' : drawnFrom === null ? 'present (generation unknown — redrawn on the next refresh)' : drawnFrom === gen ? 'present, drawn from the current graph' : 'present, drawn from an older graph'}\n` +
1004
+ (fs.existsSync(mutex) && mutexAgeMs(mutex) > 60_000
1005
+ ? `reclaim mutex: ${shown(root, mutex)} left by a crash — stale locks stay held until it is removed by hand\n`
1006
+ : ''),
1007
+ );
1008
+ return 0;
1009
+ }
1010
+
1011
+ function mutexAgeMs(p: string): number {
1012
+ try {
1013
+ return Date.now() - fs.statSync(p).mtimeMs;
1014
+ } catch {
1015
+ return 0;
1016
+ }
1017
+ }
1018
+
1019
+ /**
1020
+ * Whether the map was drawn from a different graph than the cache now holds —
1021
+ * decided by the generation both record, never by modification times, which a
1022
+ * copied or restored file carries wrongly. No map → never draw one unbidden.
1023
+ */
1024
+ function mapIsBehind(root: string, cache: string): boolean {
1025
+ const mapDir = graphHome(root).mapDir;
1026
+ if (!fs.existsSync(mapDir)) return false;
1027
+ const gen = graphGeneration(cache);
1028
+ return gen !== null && mapGeneration(mapDir) !== gen;
729
1029
  }
730
1030
 
731
1031
  /**
@@ -734,11 +1034,39 @@ function cmdRefresh(argv: string[]): number {
734
1034
  * Builds or updates the graph first when it is absent or behind.
735
1035
  */
736
1036
  async function cmdMap(argv: string[]): Promise<number> {
1037
+ if (argv.includes('--estate')) return estateMapMain(argv);
737
1038
  const root = resolveRoot(argv);
738
1039
  if (!fs.existsSync(root)) {
739
1040
  process.stderr.write(`code-graph: root not found: ${root}\n`);
740
1041
  return 1;
741
1042
  }
1043
+ // A background refresh may be redrawing the same files; two writers would
1044
+ // interleave them. Wait for it — a session start is exactly when someone asks
1045
+ // for the map — but not beyond one refresh budget plus a redraw.
1046
+ let release = acquireRefreshLock(root);
1047
+ const deadline = Date.now() + MAP_LOCK_WAIT_MS;
1048
+ if (release === null) process.stderr.write('code-graph: waiting for a background refresh to finish…\n');
1049
+ while (release === null && Date.now() < deadline) {
1050
+ await new Promise((r) => setTimeout(r, 250));
1051
+ release = acquireRefreshLock(root);
1052
+ }
1053
+ if (release === null) {
1054
+ process.stderr.write('code-graph: a refresh is still redrawing the map — try again in a moment\n');
1055
+ return 1;
1056
+ }
1057
+ try {
1058
+ return await drawMap(argv);
1059
+ } finally {
1060
+ release();
1061
+ }
1062
+ }
1063
+
1064
+ /** How long `map` waits for a background refresh to release the lock. */
1065
+ const MAP_LOCK_WAIT_MS = Number(process.env['AGENT_CONFIG_MAP_LOCK_WAIT_MS'] ?? 90 * 1000);
1066
+
1067
+ /** The map itself; callers hold the refresh lock. */
1068
+ async function drawMap(argv: string[]): Promise<number> {
1069
+ const root = resolveRoot(argv);
742
1070
  const out = flag(argv, '--out');
743
1071
  const diffRev = flag(argv, '--diff');
744
1072
  const r = await buildMap(root, {
@@ -756,15 +1084,25 @@ async function cmdMap(argv: string[]): Promise<number> {
756
1084
  (r.model.diff
757
1085
  ? ` diff ${r.model.diff.rev}: ${r.model.diff.changed.length} changed · ${r.model.diff.impacted.length} linked into them · ${r.model.diff.areas.length} area(s) touched\n`
758
1086
  : '') +
759
- ` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n`,
1087
+ ` wrote: ${r.written.length} files under ${rel}/ — open ${path.join(rel, 'index.html')}\n` +
1088
+ (fs.existsSync(graphHome(root).autoRefreshPath)
1089
+ ? ''
1090
+ : ' keep it current at every agent session start: agent-config code-graph auto-refresh on\n'),
760
1091
  );
761
1092
  let rc = 0;
1093
+ const renderFile = path.join(r.outDir, 'map-render.json');
762
1094
  if (argv.includes('--no-verify')) {
763
- // A check from an earlier map must not sit beside this one as if it were about it.
764
- fs.rmSync(path.join(r.outDir, 'map-render.json'), { force: true });
1095
+ // A check that names the generation it checked can stay beside a newer
1096
+ // map without being read as about it; one that names none cannot.
1097
+ try {
1098
+ const prev = JSON.parse(fs.readFileSync(renderFile, 'utf-8')) as { graph_generation?: unknown };
1099
+ if (typeof prev.graph_generation !== 'string') fs.rmSync(renderFile, { force: true });
1100
+ } catch {
1101
+ fs.rmSync(renderFile, { force: true });
1102
+ }
765
1103
  } else {
766
1104
  const check = await verifyMap(path.join(r.outDir, 'index.html'));
767
- fs.writeFileSync(path.join(r.outDir, 'map-render.json'), `${JSON.stringify(check, null, 2)}\n`);
1105
+ fs.writeFileSync(renderFile, `${JSON.stringify({ ...check, graph_generation: r.meta.graph_generation }, null, 2)}\n`);
768
1106
  const problems = check.viewports
769
1107
  .flatMap((v) => [
770
1108
  ...(v.pageErrors ? [`${v.name}: ${v.pageErrors} page error(s)`] : []),
@@ -831,7 +1169,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
831
1169
  if (sub === 'build') return await cmdBuild(argv.slice(1));
832
1170
  if (sub === 'validate') return cmdValidate(argv.slice(1));
833
1171
  if (sub === 'detect') return cmdDetect(argv.slice(1));
834
- if (sub === 'refresh') return cmdRefresh(argv.slice(1));
1172
+ if (sub === 'refresh') return await cmdRefresh(argv.slice(1));
835
1173
  if (sub === 'query' || sub === 'explain' || sub === 'affected' || sub === 'path')
836
1174
  return cmdQuery(sub, argv.slice(1));
837
1175
  if (sub === 'impact') return cmdImpact(argv.slice(1));
@@ -839,14 +1177,23 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
839
1177
  if (sub === 'untested') return cmdUntested(argv.slice(1));
840
1178
  if (sub === 'dead') return cmdDead(argv.slice(1));
841
1179
  if (sub === 'suggest-verb') return cmdSuggestVerb(argv.slice(1));
1180
+ if (sub === 'lineage' || (sub === 'map' && argv.includes('--lineage')))
1181
+ return await lineageMain(sub === 'lineage' ? argv.slice(1) : ['map', ...argv.slice(1)]);
842
1182
  if (sub === 'map') return await cmdMap(argv.slice(1));
843
1183
  if (sub === 'outcome') return cmdOutcome(argv.slice(1));
1184
+ if (sub === 'auto-refresh') return cmdAutoRefresh(argv.slice(1));
1185
+ if (sub === 'projects') return projectsMain(argv.slice(1));
1186
+ if (sub === 'estate') return estateMain(argv.slice(1));
844
1187
  process.stderr.write(
845
1188
  'usage: code_graph/cli.ts <build|validate|detect|refresh|query|explain|affected|path|' +
846
- 'impact|tests-for|untested|dead|suggest-verb|map|outcome> [options]\n',
1189
+ 'impact|tests-for|untested|dead|suggest-verb|map|outcome|auto-refresh|projects|estate|lineage> [options]\n',
847
1190
  );
848
1191
  return 2;
849
1192
  } catch (e) {
1193
+ if (e instanceof UsageError) {
1194
+ process.stderr.write(`code-graph: ${e.message}\n`);
1195
+ return 1;
1196
+ }
850
1197
  process.stderr.write(`code-graph: internal error: ${(e as Error).message}\n`);
851
1198
  return 3;
852
1199
  }