@event4u/agent-config 14.21.0 → 14.22.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 (181) 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 +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * inbox_source_census — the denominator an inbox run cannot shrink by reading badly.
4
+ *
5
+ * THE GAP THIS CLOSES.
6
+ *
7
+ * `/analyze:inbox` already ends with a **point ledger**: points extracted in its
8
+ * Phase 3 against points dispositioned in Phases 4–5b, per bucket, as counts
9
+ * that must balance. That ledger proves the run answered what it read. It
10
+ * cannot prove the run read what arrived — a point nobody extracted appears in
11
+ * neither column, and the ledger balances perfectly without it. Under-extraction
12
+ * is invisible to the only completeness check the command has.
13
+ *
14
+ * Measured shape of the input that exposed it: round `inbox-2026-09-r` arrived
15
+ * as 12 topic folders, 67 files and ~55,800 lines, where one folder alone held
16
+ * six revisions of a plan beside a 58 KB transcript. Read per file, the
17
+ * revisions look like six sources; read as a set, they are one argument whose
18
+ * later revisions silently drop items the earlier ones carried.
19
+ *
20
+ * WHAT THIS COUNTS — AND WHAT IT DELIBERATELY DOES NOT.
21
+ *
22
+ * Only what is **mechanical and genre-independent**:
23
+ *
24
+ * 1. **Source sets** — files grouped by stem, so a revision family
25
+ * (`x.v1/x.v2`, `x.consolidated.v1`, `x-loop-1`, `x-2026-09-06`) reports as
26
+ * ONE set with its members ordered, instead of as N unrelated files.
27
+ * 2. **Anchors** — the four countable structures a source can carry: turn
28
+ * separators, headings, checkbox steps, and numbered items. Every class is
29
+ * counted on every file; the run picks the ones its genre uses.
30
+ * 3. **Decision ids** — the `D2` / `E1` / `K7` tokens a source argues in. If
31
+ * those do not appear in what the run emits, the emitted plan is
32
+ * undecidable by construction and no reader can tell, because the ids left
33
+ * with the source.
34
+ *
35
+ * It does NOT guess genre, judge relevance, or extract demands. Those are the
36
+ * reading, and the whole point of a denominator is that it is produced without
37
+ * one — a census that needed judgement would be as shrinkable as the reading it
38
+ * is supposed to bound.
39
+ *
40
+ * HONEST LIMIT.
41
+ *
42
+ * An anchor count is a floor on what must be accounted for, never a target and
43
+ * never a quality measure. A 400-anchor source can carry three demands and a
44
+ * 12-anchor transcript can carry twelve. What the number buys is that a run
45
+ * reporting "41 anchors → 41 accounted" can be challenged by anyone holding the
46
+ * folder, where a prose summary cannot.
47
+ *
48
+ * USAGE.
49
+ *
50
+ * ./scripts-run src/scripts/inbox_source_census --root agents/tmp/<round-id>
51
+ * ./scripts-run src/scripts/inbox_source_census --root <dir> --json
52
+ *
53
+ * Exit 2 when the root is missing, unreadable, or enumerates zero files — a
54
+ * census over an empty set is vacuous and must not read as coverage.
55
+ */
56
+
57
+ import * as fs from 'node:fs';
58
+ import * as path from 'node:path';
59
+ import { fileURLToPath, pathToFileURL } from 'node:url';
60
+
61
+ const _HERE = fileURLToPath(import.meta.url);
62
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
63
+
64
+ /** Extensions worth counting. Binaries and archives carry no anchors. */
65
+ const TEXT_EXT = new Set(['.md', '.txt', '.markdown', '.mdx']);
66
+
67
+ /**
68
+ * Version markers that make two files revisions of one source rather than two
69
+ * sources. Ordered most-specific first: `consolidated.v2` must not be stripped
70
+ * to `consolidated` by the bare `.vN` rule.
71
+ */
72
+ const VERSION_MARKERS: readonly RegExp[] = [
73
+ /[.-]consolidated[.-]v\d+$/i,
74
+ /[.-]consolidated$/i,
75
+ /[.-]loop[.-]?\d+.*$/i,
76
+ /[.-]v\d+$/i,
77
+ /[.-]\d{4}-\d{2}-\d{2}$/,
78
+ /[.-](final|draft|deep|master)$/i,
79
+ ];
80
+
81
+ export interface AnchorCounts {
82
+ /** `----`-style turn separators — the transcript spine. */
83
+ separators: number;
84
+ /** ATX headings, h1 through h4. */
85
+ headings: number;
86
+ /** `- [ ]` / `- [x]` / `- [~]` / `- [-]` roadmap steps. */
87
+ steps: number;
88
+ /** `1.`-style numbered items and `**bold lead-in**` paragraph openers. */
89
+ enumerated: number;
90
+ }
91
+
92
+ export interface FileCensus {
93
+ file: string;
94
+ bytes: number;
95
+ lines: number;
96
+ anchors: AnchorCounts;
97
+ /** Sum of the four anchor classes — the file's floor. */
98
+ total: number;
99
+ }
100
+
101
+ export interface SourceSet {
102
+ /** The shared stem the members were grouped under. */
103
+ stem: string;
104
+ /** Members, oldest-looking first (lexical on the version marker). */
105
+ members: FileCensus[];
106
+ /** True when more than one member shares the stem. */
107
+ isRevisionSet: boolean;
108
+ }
109
+
110
+ export interface TopicCensus {
111
+ topic: string;
112
+ sets: SourceSet[];
113
+ decisionIds: string[];
114
+ fileCount: number;
115
+ anchorTotal: number;
116
+ }
117
+
118
+ /**
119
+ * Strip a trailing version marker to the stem two revisions share.
120
+ *
121
+ * Applied repeatedly, most-specific first, so `road-to-x.consolidated.v3`
122
+ * reduces through `road-to-x.consolidated` to `road-to-x` — otherwise a
123
+ * `.consolidated` family and a plain `.vN` family of the same plan report as
124
+ * two sets and the diff obligation never fires on the pair.
125
+ */
126
+ export function stemOf(filename: string): string {
127
+ let s = filename.replace(/\.(md|markdown|mdx|txt)$/i, '');
128
+ let changed = true;
129
+ while (changed) {
130
+ changed = false;
131
+ for (const re of VERSION_MARKERS) {
132
+ const next = s.replace(re, '');
133
+ if (next !== s && next.length > 0) {
134
+ s = next;
135
+ changed = true;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ return s;
141
+ }
142
+
143
+ /** Count the four anchor classes. Fenced code is skipped — a fence is content. */
144
+ export function countAnchors(text: string): AnchorCounts {
145
+ const out: AnchorCounts = { separators: 0, headings: 0, steps: 0, enumerated: 0 };
146
+ let inFence = false;
147
+ for (const raw of text.split(/\r?\n/)) {
148
+ const line = raw.trimEnd();
149
+ if (/^\s*(```|~~~)/.test(line)) {
150
+ inFence = !inFence;
151
+ continue;
152
+ }
153
+ if (inFence) continue;
154
+ if (/^-{4,}$|^={4,}$|^\*{4,}$/.test(line.trim())) {
155
+ out.separators += 1;
156
+ continue;
157
+ }
158
+ if (/^#{1,4} \S/.test(line)) {
159
+ out.headings += 1;
160
+ continue;
161
+ }
162
+ if (/^\s*[-*] \[[ xX~-]\]/.test(line)) {
163
+ out.steps += 1;
164
+ continue;
165
+ }
166
+ if (/^\s*\d+[.)] \S/.test(line) || /^\*\*[^*]+\*\*/.test(line)) {
167
+ out.enumerated += 1;
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ /**
174
+ * Decision / open-question ids the source argues in — `D2`, `E1`, `K7`, `AC3`.
175
+ *
176
+ * Deliberately narrow: one to three capitals followed by one or two digits, as
177
+ * a whole token. A wider pattern matches version strings, HTTP codes and every
178
+ * `P0` in ordinary prose, and a noisy id set is worse than none — the run would
179
+ * stop reading the row.
180
+ */
181
+ export function decisionIds(text: string): string[] {
182
+ const hits = text.match(/\b[A-Z]{1,3}\d{1,2}\b/g) ?? [];
183
+ return [...new Set(hits)].sort();
184
+ }
185
+
186
+ function censusFile(abs: string, rel: string): FileCensus {
187
+ const text = fs.readFileSync(abs, 'utf-8');
188
+ const anchors = countAnchors(text);
189
+ return {
190
+ file: rel,
191
+ bytes: Buffer.byteLength(text),
192
+ lines: text.split(/\r?\n/).length,
193
+ anchors,
194
+ total: anchors.separators + anchors.headings + anchors.steps + anchors.enumerated,
195
+ };
196
+ }
197
+
198
+ /** Group one topic folder's files into source sets and count its anchors. */
199
+ export function censusTopic(dir: string, topic: string): TopicCensus {
200
+ const names = fs
201
+ .readdirSync(dir, { withFileTypes: true })
202
+ .filter((d) => d.isFile() && TEXT_EXT.has(path.extname(d.name).toLowerCase()))
203
+ .map((d) => d.name)
204
+ .sort();
205
+
206
+ const byStem = new Map<string, FileCensus[]>();
207
+ const ids = new Set<string>();
208
+ for (const name of names) {
209
+ const abs = path.join(dir, name);
210
+ const fc = censusFile(abs, name);
211
+ for (const id of decisionIds(fs.readFileSync(abs, 'utf-8'))) ids.add(id);
212
+ const stem = stemOf(name);
213
+ const bucket = byStem.get(stem);
214
+ if (bucket) bucket.push(fc);
215
+ else byStem.set(stem, [fc]);
216
+ }
217
+
218
+ const sets: SourceSet[] = [...byStem.entries()]
219
+ .map(([stem, members]) => ({
220
+ stem,
221
+ members: members.sort((a, b) => a.file.localeCompare(b.file)),
222
+ isRevisionSet: members.length > 1,
223
+ }))
224
+ .sort((a, b) => a.stem.localeCompare(b.stem));
225
+
226
+ return {
227
+ topic,
228
+ sets,
229
+ decisionIds: [...ids].sort(),
230
+ fileCount: names.length,
231
+ anchorTotal: sets.reduce(
232
+ (n, s) => n + s.members.reduce((m, f) => m + f.total, 0),
233
+ 0,
234
+ ),
235
+ };
236
+ }
237
+
238
+ /** Walk a round root: each immediate subdirectory is a topic, plus the root itself. */
239
+ export function censusRoot(root: string): TopicCensus[] {
240
+ const out: TopicCensus[] = [];
241
+ const rootLevel = censusTopic(root, '.');
242
+ if (rootLevel.fileCount > 0) out.push(rootLevel);
243
+ for (const d of fs.readdirSync(root, { withFileTypes: true })) {
244
+ if (!d.isDirectory()) continue;
245
+ const t = censusTopic(path.join(root, d.name), d.name);
246
+ if (t.fileCount > 0) out.push(t);
247
+ }
248
+ return out;
249
+ }
250
+
251
+ function render(topics: TopicCensus[], write: (s: string) => unknown): void {
252
+ let files = 0;
253
+ let anchors = 0;
254
+ let revisionSets = 0;
255
+ for (const t of topics) {
256
+ write(`\n## ${t.topic}\n`);
257
+ for (const s of t.sets) {
258
+ const tag = s.isRevisionSet ? ` — REVISION SET (${String(s.members.length)}), diff consecutive members` : '';
259
+ write(` ${s.stem}${tag}\n`);
260
+ for (const f of s.members) {
261
+ const a = f.anchors;
262
+ write(
263
+ ` ${f.file} ${String(f.bytes)}B/${String(f.lines)}L ` +
264
+ `sep ${String(a.separators)} · head ${String(a.headings)} · ` +
265
+ `step ${String(a.steps)} · enum ${String(a.enumerated)} = ${String(f.total)}\n`,
266
+ );
267
+ }
268
+ if (s.isRevisionSet) revisionSets += 1;
269
+ }
270
+ if (t.decisionIds.length > 0) write(` decision ids: ${t.decisionIds.join(' ')}\n`);
271
+ files += t.fileCount;
272
+ anchors += t.anchorTotal;
273
+ }
274
+ write(
275
+ `\ntopics ${String(topics.length)} · files ${String(files)} · ` +
276
+ `revision sets ${String(revisionSets)} · anchors ${String(anchors)}\n` +
277
+ 'Every anchor leaves the run carrying a ledger row or named in one `no-demand` line.\n' +
278
+ 'Every revision set owes a consecutive-member diff (/analyze:inbox Phase 2b).\n',
279
+ );
280
+ }
281
+
282
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
283
+ const rootIdx = argv.indexOf('--root');
284
+ const rootArg = rootIdx === -1 ? undefined : argv[rootIdx + 1];
285
+ if (rootArg === undefined) {
286
+ process.stderr.write('usage: inbox_source_census --root <dir> [--json]\n');
287
+ return 2;
288
+ }
289
+ const root = path.resolve(REPO_ROOT, rootArg);
290
+ let stat: fs.Stats;
291
+ try {
292
+ stat = fs.statSync(root);
293
+ } catch {
294
+ process.stderr.write(`❌ inbox_source_census: root not readable: ${root}\n`);
295
+ return 2;
296
+ }
297
+ if (!stat.isDirectory()) {
298
+ process.stderr.write(`❌ inbox_source_census: root is not a directory: ${root}\n`);
299
+ return 2;
300
+ }
301
+
302
+ const topics = censusRoot(root);
303
+ const files = topics.reduce((n, t) => n + t.fileCount, 0);
304
+ if (files === 0) {
305
+ process.stderr.write(
306
+ `❌ inbox_source_census: 0 text file(s) under ${root} — a census over an ` +
307
+ 'empty set is vacuous and must not read as coverage.\n',
308
+ );
309
+ return 2;
310
+ }
311
+
312
+ if (argv.includes('--json')) {
313
+ process.stdout.write(`${JSON.stringify({ root, topics }, null, 2)}\n`);
314
+ return 0;
315
+ }
316
+ render(topics, (s) => process.stdout.write(s));
317
+ return 0;
318
+ }
319
+
320
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
321
+ process.exit(main());
322
+ }
@@ -30,6 +30,18 @@ if [ -n "${AGENT_CONFIG_HOOKS_DIR+set}" ]; then
30
30
  exit 2
31
31
  fi
32
32
  HOOKS_DIR="$AGENT_CONFIG_HOOKS_DIR"
33
+ elif CONFIGURED_HOOKS_PATH="$(git -C "$PROJECT_ROOT" config --get core.hooksPath 2>/dev/null)" \
34
+ && [ -n "$CONFIGURED_HOOKS_PATH" ]; then
35
+ # core.hooksPath wins over .git/hooks WHEREVER IT IS SET — git stops looking
36
+ # in the common dir entirely once it is configured. Installing into
37
+ # .git/hooks while git reads somewhere else writes six hooks that never run,
38
+ # and the freshness gate would then compare the copy nothing executes.
39
+ # Resolved relative to PROJECT_ROOT because git interprets a relative
40
+ # core.hooksPath against the top level of the working tree, not against CWD.
41
+ case "$CONFIGURED_HOOKS_PATH" in
42
+ /*) HOOKS_DIR="$CONFIGURED_HOOKS_PATH" ;;
43
+ *) HOOKS_DIR="$PROJECT_ROOT/$CONFIGURED_HOOKS_PATH" ;;
44
+ esac
33
45
  elif COMMON_GIT_DIR="$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)"; then
34
46
  case "$COMMON_GIT_DIR" in /*) : ;; *) COMMON_GIT_DIR="$PROJECT_ROOT/$COMMON_GIT_DIR" ;; esac
35
47
  HOOKS_DIR="$COMMON_GIT_DIR/hooks"
@@ -574,3 +586,73 @@ EOF
574
586
  append_auto_sync_block "post-merge" "1"
575
587
  append_auto_sync_block "post-checkout" "3"
576
588
  echo "✅ Auto-sync block appended to post-merge / post-checkout hooks." >&2
589
+
590
+ # Code-graph freshness, from git, with no daemon ----------------------------
591
+ #
592
+ # road-to-a-graph-that-is-shipped 1.3. A code graph goes stale the moment a
593
+ # commit lands, and the only thing that knew were the reader — after it had
594
+ # already answered from a stale index. These two hooks close that without a
595
+ # resident process: git tells us HEAD moved, and a detached child rebuilds.
596
+ #
597
+ # Four properties, each load-bearing:
598
+ #
599
+ # · BACKGROUND. The refresh is `&`-detached with stdout/stderr closed, so a
600
+ # commit never waits on a parse. Git's own runtime is unchanged.
601
+ # · SINGLE-FLIGHT. `mkdir` is atomic on every POSIX filesystem, so of two
602
+ # concurrent commits exactly one creates the lock and exactly one refresh
603
+ # runs. This is Risk 3 in the roadmap's register — "git hooks pile up
604
+ # rebuilds" — and a lock file written with `>` would not have closed it,
605
+ # because two processes can both truncate-and-write the same path.
606
+ # · BUDGETED. `--budget-seconds` is the engine's own wall-clock ceiling; a
607
+ # child that exceeds it is SIGKILLed and the OLD cache is left untouched
608
+ # (cli.ts's refresh contract). Kill register K7 — never block on a stale
609
+ # graph — holds by construction here, since nothing waits at all.
610
+ # · NEVER BUILDS ONE UNBIDDEN. The block is a no-op unless a cache already
611
+ # exists. Building a graph for someone who never asked for one is the pure
612
+ # cost Risk 1 names; refreshing one they built is the service.
613
+ #
614
+ # The lock is a directory under the gitignored runtime state, so a crashed
615
+ # child leaves a stale lock that the next refresh clears by age rather than
616
+ # blocking forever.
617
+
618
+ append_code_graph_refresh_block() {
619
+ local name="$1"
620
+ cat >> "$HOOKS_DIR/$name" << EOF
621
+
622
+ # --- code-graph background refresh ------------------------------------------
623
+ EOF
624
+ if [ "$name" = "post-checkout" ]; then
625
+ cat >> "$HOOKS_DIR/$name" << 'EOF'
626
+ # A file-checkout ($3 = 0) moves no commits, so there is nothing to be behind.
627
+ if [ "${3:-1}" = "0" ]; then
628
+ exit 0
629
+ fi
630
+ EOF
631
+ fi
632
+ cat >> "$HOOKS_DIR/$name" << 'EOF'
633
+ cg_cache="agents/runtime/state/code-graph-v1.json"
634
+ cg_lock="agents/runtime/state/code-graph-refresh.lock"
635
+ if [ -f "$cg_cache" ] && [ -x ./agent-config ]; then
636
+ # Clear a lock left by a killed child: older than the largest budget any
637
+ # caller passes, so it cannot race a refresh that is legitimately running.
638
+ if [ -d "$cg_lock" ]; then
639
+ if [ -z "$(find "$cg_lock" -maxdepth 0 -mmin -5 2>/dev/null)" ]; then
640
+ rmdir "$cg_lock" 2>/dev/null || true
641
+ fi
642
+ fi
643
+ # Atomic: exactly one of N concurrent hooks wins this, and the losers do
644
+ # nothing rather than queueing a second rebuild behind the first.
645
+ if mkdir "$cg_lock" 2>/dev/null; then
646
+ (
647
+ trap 'rmdir "$cg_lock" 2>/dev/null || true' EXIT
648
+ ./agent-config code-graph refresh --budget-seconds 60 >/dev/null 2>&1 || true
649
+ ) >/dev/null 2>&1 &
650
+ fi
651
+ fi
652
+ exit 0
653
+ EOF
654
+ }
655
+
656
+ append_code_graph_refresh_block "post-commit"
657
+ append_code_graph_refresh_block "post-checkout"
658
+ echo "✅ Code-graph refresh block appended to post-commit / post-checkout hooks." >&2