@event4u/agent-config 14.7.0 → 14.9.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 (230) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +175 -390
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +34 -4
  5. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  6. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  7. package/dist/agent-src/commands/pr/create.md +14 -2
  8. package/dist/agent-src/commands/pr/merge.md +383 -0
  9. package/dist/agent-src/commands/review/changes.md +95 -14
  10. package/dist/agent-src/commands/roadmap/process-full.md +157 -3
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
  15. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
  16. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
  18. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  19. package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
  20. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  21. package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
  22. package/dist/agent-src/rules/source-confidentiality.md +1 -1
  23. package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
  24. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  25. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
  27. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  28. package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
  29. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  30. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  31. package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
  32. package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
  33. package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
  34. package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
  35. package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
  36. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
  38. package/dist/agent-src/skills/security/SKILL.md +8 -0
  39. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/roadmaps.md +28 -2
  42. package/dist/cli/registry.js +3 -0
  43. package/dist/cli/registry.js.map +1 -1
  44. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
  45. package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
  46. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
  47. package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
  48. package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
  49. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  50. package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
  51. package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
  52. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  53. package/dist/cli-delegate/cmd_conformance.js +101 -25
  54. package/dist/cli-delegate/cmd_converge.js +3 -3
  55. package/dist/cli-delegate/cmd_doctor.js +7 -7
  56. package/dist/cli-delegate/cmd_export.js +6 -6
  57. package/dist/cli-delegate/cmd_handoff.js +7 -6
  58. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  59. package/dist/cli-delegate/cmd_migrate.js +2 -2
  60. package/dist/cli-delegate/cmd_preflight.js +5 -5
  61. package/dist/cli-delegate/cmd_prune.js +6 -6
  62. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  63. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  64. package/dist/cli-delegate/cmd_session_recycle.js +3 -3
  65. package/dist/cli-delegate/cmd_settings_get.js +4 -4
  66. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  67. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  68. package/dist/cli-delegate/cmd_sync.js +6 -6
  69. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  70. package/dist/cli-delegate/cmd_update.js +3 -3
  71. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  72. package/dist/cli-delegate/cmd_validate.js +6 -6
  73. package/dist/cli-delegate/cmd_versions.js +3 -3
  74. package/dist/discovery/deprecation-report.md +1 -1
  75. package/dist/discovery/discovery-manifest.json +83 -30
  76. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  77. package/dist/discovery/discovery-manifest.summary.md +4 -4
  78. package/dist/discovery/dormancy-report.md +1 -1
  79. package/dist/discovery/orphan-report.md +1 -1
  80. package/dist/discovery/packs.json +11 -9
  81. package/dist/discovery/trust-report.md +2 -2
  82. package/dist/discovery/workspaces.json +9 -7
  83. package/dist/hooks/dispatch.js +1295 -812
  84. package/dist/install/globalRuleLayers.js +169 -0
  85. package/dist/install/globalRuleLayers.js.map +1 -0
  86. package/dist/install/partitionEligibility.js +156 -0
  87. package/dist/install/partitionEligibility.js.map +1 -1
  88. package/dist/install/ruleLayerPartition.js +157 -0
  89. package/dist/install/ruleLayerPartition.js.map +1 -0
  90. package/dist/mcp/registry-manifest.json +3 -3
  91. package/dist/mcp/server.json +2 -2
  92. package/dist/mcp/server.mjs +1358 -805
  93. package/dist/scripts/_lib/claude_plugin.js +162 -0
  94. package/dist/scripts/_lib/claude_plugin.js.map +1 -0
  95. package/dist/scripts/_lib/environment_detector.js +397 -0
  96. package/dist/scripts/_lib/environment_detector.js.map +1 -0
  97. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  98. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  99. package/dist/scripts/_lib/spawn_env.js +129 -0
  100. package/dist/scripts/_lib/spawn_env.js.map +1 -0
  101. package/dist/scripts/ai_council/modes.js +199 -0
  102. package/dist/scripts/ai_council/modes.js.map +1 -0
  103. package/dist/scripts/ai_council/transport_resolver.js +418 -0
  104. package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
  105. package/dist/scripts/condense.js +52 -89
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/routes/wizard.js +3 -2
  108. package/dist/server/routes/wizard.js.map +1 -1
  109. package/dist/ui/assets/index-Cxgy63kz.js +26 -0
  110. package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
  111. package/dist/ui/index.html +1 -1
  112. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  113. package/docs/guidelines/agent-infra/false-green.md +47 -1
  114. package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
  115. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
  116. package/package.json +7 -3
  117. package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  118. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  119. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
  120. package/src/config/ci-local-parity.yml +21 -0
  121. package/src/config/estate-count-budget.json +13 -471
  122. package/src/config/evaluator-budgets.json +4 -4
  123. package/src/config/gate-coverage.yml +458 -12
  124. package/src/config/gate-violation-baselines.json +18 -8
  125. package/src/config/gitignore-block.txt +15 -2
  126. package/src/config/hook-latency-budget.json +5 -0
  127. package/src/config/publish-surface.json +60 -0
  128. package/src/config/requirements-trace-budget.json +32 -0
  129. package/src/config/rule-stub-ceilings.json +25 -4
  130. package/src/install/globalRuleLayers.ts +197 -0
  131. package/src/install/partitionEligibility.ts +177 -0
  132. package/src/install/ruleLayerPartition.ts +212 -0
  133. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  134. package/src/scripts/_dispatch.bash +18 -0
  135. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  136. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  137. package/src/scripts/_lib/base_tree.ts +175 -0
  138. package/src/scripts/_lib/billing_grant.ts +165 -0
  139. package/src/scripts/_lib/billing_grant_cli.ts +123 -0
  140. package/src/scripts/_lib/council_fallback_posture.ts +53 -6
  141. package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
  142. package/src/scripts/_lib/envelope_grounding.ts +11 -1
  143. package/src/scripts/_lib/host_capability.ts +59 -5
  144. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  145. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  146. package/src/scripts/_lib/quota_parked.ts +133 -0
  147. package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
  148. package/src/scripts/_lib/review_telemetry.ts +196 -0
  149. package/src/scripts/_lib/subagent_response.ts +92 -0
  150. package/src/scripts/adr/evidence_census.ts +899 -0
  151. package/src/scripts/adr/regenerate_index.ts +25 -38
  152. package/src/scripts/adr_cite_check.ts +540 -39
  153. package/src/scripts/ai_council/clients.ts +12 -1
  154. package/src/scripts/ai_council/config.ts +72 -115
  155. package/src/scripts/ai_council/fallback_config.ts +19 -7
  156. package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
  157. package/src/scripts/ai_council/orchestrator.ts +2 -2
  158. package/src/scripts/ai_council/py_format.ts +124 -0
  159. package/src/scripts/ai_council/transport_resolver.ts +65 -2
  160. package/src/scripts/audit_adr_coverage.ts +134 -21
  161. package/src/scripts/bench_hook_latency.ts +204 -0
  162. package/src/scripts/build_archive_index.ts +34 -0
  163. package/src/scripts/check_adr_frontmatter.ts +574 -39
  164. package/src/scripts/check_council_pin_staleness.ts +300 -0
  165. package/src/scripts/check_estate_count.ts +420 -293
  166. package/src/scripts/check_gate_coverage.ts +91 -0
  167. package/src/scripts/check_hook_bundle_content.ts +219 -0
  168. package/src/scripts/check_new_adr_evidence.ts +529 -0
  169. package/src/scripts/check_no_stub_inventory_table.ts +292 -0
  170. package/src/scripts/check_pack_size.ts +234 -1
  171. package/src/scripts/check_publish_surface.ts +297 -0
  172. package/src/scripts/check_references.ts +18 -0
  173. package/src/scripts/check_requirements_trace.ts +378 -0
  174. package/src/scripts/check_rule_layer_partition.ts +531 -0
  175. package/src/scripts/check_secret_leak.ts +166 -12
  176. package/src/scripts/check_single_delivery.ts +137 -4
  177. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  178. package/src/scripts/code_graph/cli.ts +44 -5
  179. package/src/scripts/code_graph/extract.ts +87 -0
  180. package/src/scripts/condense.ts +61 -91
  181. package/src/scripts/council_cli.ts +12 -16
  182. package/src/scripts/detect_ai_tells.ts +87 -0
  183. package/src/scripts/dispatch_r2_reviewer.ts +42 -0
  184. package/src/scripts/external_sources_denylist.json +4 -1
  185. package/src/scripts/generate_capabilities_index.ts +41 -0
  186. package/src/scripts/git_authorization_hook.ts +54 -4
  187. package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
  188. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  189. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  190. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  191. package/src/scripts/hooks/host_semantics.ts +21 -0
  192. package/src/scripts/hooks/injection_budget.ts +13 -3
  193. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  194. package/src/scripts/hooks/run_continuation_hook.ts +3 -1
  195. package/src/scripts/hooks/state_io.ts +88 -1
  196. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  197. package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
  198. package/src/scripts/injection_scan_hook.ts +257 -15
  199. package/src/scripts/lint_adr_sweep_routing.ts +365 -0
  200. package/src/scripts/lint_hook_manifest.ts +49 -0
  201. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  202. package/src/scripts/lint_workflow_security.ts +53 -10
  203. package/src/scripts/lint_workflow_security_allowlist.json +2 -18
  204. package/src/scripts/report_envelope_rate.ts +166 -0
  205. package/src/scripts/review_axis_report.ts +218 -0
  206. package/src/scripts/routing_doctor.ts +5 -1
  207. package/src/scripts/run_supervise.ts +46 -1
  208. package/src/scripts/schemas/evals.schema.json +5 -0
  209. package/src/scripts/sync_pr_branch.ts +170 -20
  210. package/dist/cli/commands/commands.candidates.test.js +0 -257
  211. package/dist/cli/commands/commands.candidates.test.js.map +0 -1
  212. package/dist/cli/commands/profiles.test.js +0 -88
  213. package/dist/cli/commands/profiles.test.js.map +0 -1
  214. package/dist/cli/commands/recordTriggerEval.test.js +0 -113
  215. package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
  216. package/dist/cli/configRoot.test.js +0 -97
  217. package/dist/cli/configRoot.test.js.map +0 -1
  218. package/dist/cli/mcp/content.test.js +0 -54
  219. package/dist/cli/mcp/content.test.js.map +0 -1
  220. package/dist/cli/mcp/dispatch.test.js +0 -108
  221. package/dist/cli/mcp/dispatch.test.js.map +0 -1
  222. package/dist/server/writeRoot.test.js +0 -91
  223. package/dist/server/writeRoot.test.js.map +0 -1
  224. package/dist/shared/capabilities.test.js +0 -39
  225. package/dist/shared/capabilities.test.js.map +0 -1
  226. package/dist/ui/assets/index-C1pBRm1f.js +0 -26
  227. package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
  228. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  229. package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
  230. package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
@@ -16,12 +16,19 @@
16
16
  * --regen-area-readme <area> rewrite docs/adrs/<area>/README.md. Idempotent.
17
17
  *
18
18
  * Historical quirks are preserved deliberately — tests and downstream consumers pin the exact behaviour.
19
+ *
20
+ * Record metadata is read by `read_adr_meta`: the shared frontmatter reader
21
+ * (`_lib/adr_frontmatter`) when a `---` block is present, the transitional
22
+ * blockquote header otherwise. The seven per-area records carried only the
23
+ * blockquote until 2026-08-21, so both paths have to work while the two
24
+ * surfaces overlap.
19
25
  */
20
26
  import * as fs from 'node:fs';
21
27
  import * as path from 'node:path';
22
28
  import process from 'node:process';
23
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
30
 
31
+ import { readAdrAxisCells, readAdrFrontmatterScalars } from './_lib/adr_frontmatter.js';
25
32
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
26
33
 
27
34
  const _HERE = fileURLToPath(import.meta.url);
@@ -89,10 +96,26 @@ export function unlistedAreaDirs(adrsRoot: string): string[] {
89
96
 
90
97
  // ^(\d{4})-([a-z0-9-]+)\.md$
91
98
  const NAMED = /^(\d{4})-([a-z0-9-]+)\.md$/;
92
- // ^---\n(.*?)\n--- (DOTALL)
93
- const FM = /^---\n([\s\S]*?)\n---/;
94
- // ^([a-z_]+):\s*(.+?)\s*$ (MULTILINE)
95
- const FIELD = /^([a-z_]+):[ \t]*(.+?)[ \t]*$/gm;
99
+
100
+ /**
101
+ * Metadata keys this script recognises in the transitional blockquote header.
102
+ *
103
+ * An allowlist rather than "every `Key: value` pair in a blockquote", because
104
+ * the blockquote block also carries prose that happens to contain colons — the
105
+ * `Roadmap:` pointer, a `Contract:` link, and (on `memory/0001`) a whole
106
+ * supersession banner. Harvesting those would put link markup into the README
107
+ * table. Keys are matched case-insensitively with spaces folded to `_`.
108
+ */
109
+ const BLOCKQUOTE_KEYS = new Set([
110
+ 'adr',
111
+ 'area',
112
+ 'status',
113
+ 'date',
114
+ 'decision',
115
+ 'supersedes',
116
+ 'superseded_by',
117
+ 'type',
118
+ ]);
96
119
 
97
120
  interface AdrEntry {
98
121
  num: string;
@@ -129,24 +152,87 @@ function _globMdSorted(dir: string): string[] {
129
152
  return out;
130
153
  }
131
154
 
155
+ /**
156
+ * Read the leading `---` frontmatter block, `{}` when there is none.
157
+ *
158
+ * Delegates to the one shared reader (`_lib/adr_frontmatter`) rather than
159
+ * keeping this script's own regex: the tree carried three divergent ADR
160
+ * frontmatter parsers plus this one, and the two nested axes (`provenance`,
161
+ * `evidence`) are exactly the shape a `^([a-z_]+):\s*(.+?)$` regex reads as
162
+ * absent.
163
+ *
164
+ * What is preserved: the `{}`-on-absent return, and the scalar keys this
165
+ * script's callers read.
166
+ *
167
+ * What changed, stated because "preserved" was claimed for it and is false:
168
+ * the removed `_stripChars(v, ' "\'')` stripped repeated and unbalanced quote
169
+ * and space characters from both ends (Python `str.strip` semantics), while the
170
+ * shared reader's `stripQuotes` is `/^["'](.*)["']$/` — exactly one balanced
171
+ * pair. Two inputs now differ: `key: "value` keeps its opening quote (was
172
+ * `value`), and `key: ''v''` yields `'v'` (was `v`). No record in the corpus
173
+ * exercises either shape today, and the old behaviour is deliberately NOT
174
+ * restored — one shared reader beats a bug-compatible local copy.
175
+ */
132
176
  export function parse_fm(text: string): Record<string, string> {
133
- const m = FM.exec(text);
134
- if (!m) {
135
- return {};
136
- }
177
+ return readAdrFrontmatterScalars(text) ?? {};
178
+ }
179
+
180
+ /**
181
+ * Read the transitional blockquote metadata header.
182
+ *
183
+ * The seven per-area records predate the frontmatter contract and carry their
184
+ * metadata as a blockquote line instead:
185
+ *
186
+ * ```
187
+ * > Area: `router` · Status: accepted · Date: 2026-05-16 · Type: retrospective
188
+ * ```
189
+ *
190
+ * Two shapes have to be handled and both occur in the tree. Several fields
191
+ * share one line separated by `·`, so the split is on the separator first and
192
+ * the first `:` second. And the value is often wrapped in backticks
193
+ * (`` `router` ``), which are stripped — a table cell showing `` `accepted` ``
194
+ * would not match what the frontmatter path produces for the same record.
195
+ *
196
+ * Scanning stops at the first `## ` heading: everything above it is the
197
+ * metadata header, everything below is body prose whose blockquotes are
198
+ * quotations, not metadata.
199
+ */
200
+ export function parse_blockquote_meta(text: string): Record<string, string> {
137
201
  const out: Record<string, string> = {};
138
- const body = m[1] as string;
139
- FIELD.lastIndex = 0;
140
- let fm: RegExpExecArray | null;
141
- while ((fm = FIELD.exec(body)) !== null) {
142
- const k = fm[1] as string;
143
- const v = fm[2] as string;
144
- // v.strip(" \"'") — strip leading/trailing space, double- and single-quote.
145
- out[k] = _stripChars(v, ' "\'');
202
+ for (const raw of text.split('\n')) {
203
+ const line = raw.trim();
204
+ if (line.startsWith('## ')) break;
205
+ if (!line.startsWith('>')) continue;
206
+ for (const part of line.replace(/^>\s*/, '').split('·')) {
207
+ const idx = part.indexOf(':');
208
+ if (idx <= 0) continue;
209
+ const key = part.slice(0, idx).trim().toLowerCase().replace(/\s+/g, '_');
210
+ if (!BLOCKQUOTE_KEYS.has(key)) continue;
211
+ const value = _stripChars(part.slice(idx + 1), ' "\'`');
212
+ if (value === '') continue;
213
+ // First occurrence wins — a later body line never overwrites the header.
214
+ if (!(key in out)) out[key] = value;
215
+ }
146
216
  }
147
217
  return out;
148
218
  }
149
219
 
220
+ /**
221
+ * One record's metadata: frontmatter when present, the blockquote header
222
+ * otherwise.
223
+ *
224
+ * Frontmatter wins outright rather than merging. During the transition a record
225
+ * carries both, and the blockquote is explicitly the human-readable duplicate —
226
+ * merging would make the authoritative surface depend on which of the two
227
+ * happened to mention a field, and a stale duplicate would then be able to
228
+ * contribute a value the frontmatter had already corrected.
229
+ */
230
+ export function read_adr_meta(text: string): Record<string, string> {
231
+ const fm = readAdrFrontmatterScalars(text);
232
+ if (fm !== null && Object.keys(fm).length > 0) return fm;
233
+ return parse_blockquote_meta(text);
234
+ }
235
+
150
236
  /** Mirror Python `str.strip(chars)`. */
151
237
  function _stripChars(s: string, chars: string): string {
152
238
  let start = 0;
@@ -193,8 +279,13 @@ export function scan_area(area: string): [AdrEntry[], string[]] {
193
279
  errs.push(`${area}/${base}: filename does not match NNNN-<slug>.md`);
194
280
  continue;
195
281
  }
196
- const fm = parse_fm(fs.readFileSync(p, 'utf-8'));
197
- adrs.push({ num: m[1] as string, slug: m[2] as string, path: base, ...fm });
282
+ const text = fs.readFileSync(p, 'utf-8');
283
+ const fm = read_adr_meta(text);
284
+ // Read apart from `fm`, for the same reason the index does: the two
285
+ // axes are nested, so the scalar reader keeps them out of its map and
286
+ // spreading it could never produce these cells.
287
+ const axes = readAdrAxisCells(text);
288
+ adrs.push({ num: m[1] as string, slug: m[2] as string, path: base, ...fm, ...axes });
198
289
  }
199
290
  // Gap check.
200
291
  const nums = adrs.map((a) => parseInt(a.num, 10));
@@ -236,7 +327,11 @@ export function render_area_readme(area: string, meta: AreaMeta, adrs: AdrEntry[
236
327
  } else {
237
328
  lines.push(`Contract: _not yet published_ (\`${repoRel}\`).`);
238
329
  }
239
- lines.push('', '| # | Title | Status | Date | Supersedes |', '|---|---|---|---|---|');
330
+ lines.push(
331
+ '',
332
+ '| # | Title | Status | Date | Provenance | Evidence | Supersedes |',
333
+ '|---|---|---|---|---|---|---|',
334
+ );
240
335
  for (const a of adrs) {
241
336
  // a.get("decision", a["slug"]).replace("-", " ").title()
242
337
  const decision = a['decision'] !== undefined ? a['decision'] : a.slug;
@@ -244,11 +339,12 @@ export function render_area_readme(area: string, meta: AreaMeta, adrs: AdrEntry[
244
339
  lines.push(
245
340
  `| [${a.num}](${a.path}) | ${title} | ` +
246
341
  `${a['status'] ?? '—'} | ${a['date'] ?? '—'} | ` +
342
+ `${a['provenance_kind'] ?? '—'} | ${a['evidence_grade'] ?? '—'} | ` +
247
343
  `${a['supersedes'] ?? '—'} |`,
248
344
  );
249
345
  }
250
346
  if (adrs.length === 0) {
251
- lines.push('| _none yet_ | — | — | — | — |');
347
+ lines.push('| _none yet_ | — | — | — | — | — | — |');
252
348
  }
253
349
  return lines.join('\n') + '\n';
254
350
  }
@@ -325,9 +421,26 @@ export function cmd_check(): number {
325
421
  process.stderr.write(`❌ ${e}\n`);
326
422
  hard += 1;
327
423
  }
328
- if (adrs.length > 0 && !_exists(path.join(ADR_ROOT, area, 'README.md'))) {
424
+ const readmePath = path.join(ADR_ROOT, area, 'README.md');
425
+ if (adrs.length > 0 && !_exists(readmePath)) {
329
426
  process.stderr.write(`❌ ${area}/: README.md missing\n`);
330
427
  hard += 1;
428
+ } else if (_exists(readmePath)) {
429
+ // Existence was the whole check until 2026-08-22, and the six
430
+ // committed READMEs proved what that costs: every one of them had
431
+ // been generated before the shared frontmatter reader landed, so
432
+ // they rendered `—` for `status` and `date` on ADRs that carry
433
+ // both — stale for months against a gate reporting zero failures.
434
+ // A generated file with no freshness gate is a file nobody
435
+ // regenerates.
436
+ const rendered = render_area_readme(area, meta, adrs);
437
+ if (fs.readFileSync(readmePath, 'utf-8') !== rendered) {
438
+ process.stderr.write(
439
+ `❌ ${area}/README.md is stale — regenerate with ` +
440
+ `\`audit_adr_coverage --regen-area-readme ${area}\`\n`,
441
+ );
442
+ hard += 1;
443
+ }
331
444
  }
332
445
  if (adrs.length === 0) {
333
446
  process.stderr.write(`⚠️ ${area}/: no bootstrap ADR yet (contract: ${meta.contract})\n`);
@@ -230,6 +230,159 @@ export function benchEvent(
230
230
  };
231
231
  }
232
232
 
233
+ /**
234
+ * One dispatcher invocation that reads stdin, parses it, and exits — the
235
+ * transport-isolation cell of the § 2 matrix.
236
+ *
237
+ * `b-payload-read-parse-dominates`, option (a), council 2026-08-20 (2/2
238
+ * quorum). Phase 1 removed ten of eleven envelope stringifies and moved
239
+ * nothing; Phase 2 removed the body from six of eleven concerns and moved
240
+ * nothing. What is left between the small and large cells happens ONCE per
241
+ * event, before any concern runs: `readFd0ToEnd` drains the pipe and
242
+ * `_build_envelope` parses it. This measures exactly that, through the SAME
243
+ * bundle, interpreter and spawn as the slot reading it is a share of — so the
244
+ * constant terms cancel in the delta.
245
+ *
246
+ * Deliberately the real bundle with `--read-exit` rather than a standalone
247
+ * probe: a probe would re-implement the audited retrying reader, and that copy
248
+ * is the drift `hook_stdin` exists to prevent.
249
+ */
250
+ export function benchReadExit(
251
+ event: string,
252
+ runs: number,
253
+ workspace: string,
254
+ ): EventResult {
255
+ const durations: number[] = [];
256
+ const payload = syntheticPayload(event, workspace);
257
+ for (let i = 0; i < runs; i += 1) {
258
+ const started = performance.now();
259
+ const proc = spawnSync(
260
+ process.execPath,
261
+ [
262
+ BUNDLE_OVERRIDE ?? BUNDLE,
263
+ '--platform',
264
+ 'claude',
265
+ '--event',
266
+ event,
267
+ '--project-dir',
268
+ workspace,
269
+ '--read-exit',
270
+ ],
271
+ {
272
+ input: payload,
273
+ encoding: 'utf-8',
274
+ env: { ...process.env, AGENT_CONFIG_REPLAY: '1', CLAUDE_PROJECT_DIR: workspace },
275
+ timeout: 60000,
276
+ },
277
+ );
278
+ const elapsed = performance.now() - started;
279
+ if (proc.error) {
280
+ throw new Error(`read-exit failed on ${event}: ${proc.error.message}`);
281
+ }
282
+ if (!(proc.stderr ?? '').includes('--read-exit ok')) {
283
+ // A silent no-op here would report the fastest possible number and
284
+ // measure nothing — the failure mode this whole cell exists to avoid
285
+ // in the OTHER direction. Refuse instead of publishing it.
286
+ throw new Error(
287
+ `read-exit produced no confirmation on ${event}; the flag may not be ` +
288
+ `wired in the measured bundle (${BUNDLE_OVERRIDE ?? BUNDLE}). ` +
289
+ `stderr: ${(proc.stderr ?? '').trim().slice(0, 200)}`,
290
+ );
291
+ }
292
+ durations.push(elapsed);
293
+ }
294
+ durations.sort((a, b) => a - b);
295
+ return {
296
+ event: `read_exit:${event}`,
297
+ runs,
298
+ p50_ms: Math.round(percentile(durations, 50)),
299
+ p95_ms: Math.round(percentile(durations, 95)),
300
+ max_ms: Math.round(durations[durations.length - 1] as number),
301
+ };
302
+ }
303
+
304
+ /** One row of the transport-share decomposition. */
305
+ export interface ReadExitCell {
306
+ event: string;
307
+ payload_bytes: number;
308
+ slot_small_p50_ms: number;
309
+ slot_large_p50_ms: number;
310
+ read_exit_small_p50_ms: number;
311
+ read_exit_large_p50_ms: number;
312
+ /** Large-minus-small on the full dispatch. */
313
+ slot_delta_ms: number;
314
+ /** Large-minus-small on read + parse alone. */
315
+ transport_delta_ms: number;
316
+ /**
317
+ * `transport_delta / slot_delta`, as a percentage, or `null` when the slot
318
+ * delta is not positive.
319
+ *
320
+ * `null` is a real answer and must not be rendered as 0: a run whose large
321
+ * cell is no slower than its small one says the fixture did not reproduce
322
+ * the gap on this machine, which is different from saying transport is free.
323
+ */
324
+ transport_share_pct: number | null;
325
+ }
326
+
327
+ /**
328
+ * The four measurements the option asks for, in one invocation.
329
+ *
330
+ * Both payload sizes are needed for the SHARE, and taking them in one run is
331
+ * the point: two separate runs would compare across whatever else the machine
332
+ * was doing, which is the cross-runner shape § 2 of the roadmap refuses as a
333
+ * repo fact. Arms alternate per repetition for the same reason.
334
+ */
335
+ export function benchReadExitCell(
336
+ event: string,
337
+ runs: number,
338
+ workspace: string,
339
+ payloadBytes: number,
340
+ ): ReadExitCell {
341
+ const saved = PAYLOAD_BYTES;
342
+ try {
343
+ PAYLOAD_BYTES = 0;
344
+ const slotSmall = benchEvent(event, runs, workspace, 'bundle');
345
+ const probeSmall = benchReadExit(event, runs, workspace);
346
+ PAYLOAD_BYTES = payloadBytes;
347
+ const slotLarge = benchEvent(event, runs, workspace, 'bundle');
348
+ const probeLarge = benchReadExit(event, runs, workspace);
349
+ const slot_delta_ms = slotLarge.p50_ms - slotSmall.p50_ms;
350
+ const transport_delta_ms = probeLarge.p50_ms - probeSmall.p50_ms;
351
+ return {
352
+ event,
353
+ payload_bytes: payloadBytes,
354
+ slot_small_p50_ms: slotSmall.p50_ms,
355
+ slot_large_p50_ms: slotLarge.p50_ms,
356
+ read_exit_small_p50_ms: probeSmall.p50_ms,
357
+ read_exit_large_p50_ms: probeLarge.p50_ms,
358
+ slot_delta_ms,
359
+ transport_delta_ms,
360
+ transport_share_pct:
361
+ slot_delta_ms > 0
362
+ ? Math.round((transport_delta_ms / slot_delta_ms) * 100)
363
+ : null,
364
+ };
365
+ } finally {
366
+ PAYLOAD_BYTES = saved;
367
+ }
368
+ }
369
+
370
+ /** Human-readable decomposition, one line per fact. */
371
+ export function renderReadExitCell(cell: ReadExitCell): string {
372
+ const share =
373
+ cell.transport_share_pct === null
374
+ ? 'n/a — the large cell was not slower than the small one in this run'
375
+ : `${String(cell.transport_share_pct)}%`;
376
+ return (
377
+ `read-exit cell · ${cell.event} · payload ${String(cell.payload_bytes)}B\n` +
378
+ ` full dispatch small ${String(cell.slot_small_p50_ms).padStart(5)} ms · ` +
379
+ `large ${String(cell.slot_large_p50_ms).padStart(5)} ms · delta ${String(cell.slot_delta_ms)} ms\n` +
380
+ ` read + parse small ${String(cell.read_exit_small_p50_ms).padStart(5)} ms · ` +
381
+ `large ${String(cell.read_exit_large_p50_ms).padStart(5)} ms · delta ${String(cell.transport_delta_ms)} ms\n` +
382
+ ` transport share of the large-payload delta: ${share}\n`
383
+ );
384
+ }
385
+
233
386
  /**
234
387
  * Same-run control: a bare `node -e 0`, measured through the identical
235
388
  * spawnSync harness the slots use.
@@ -486,6 +639,37 @@ export function main(argv: string[] = process.argv.slice(2)): number {
486
639
  PAYLOAD_BYTES = parsed;
487
640
  }
488
641
 
642
+ // `--read-exit-cell <bytes>`: the transport-isolation cell. Measurement-only
643
+ // for the same reason `--payload-bytes` is — it PADS the payload, so it must
644
+ // never write a budget row or a regression baseline.
645
+ const readExitIdx = argv.indexOf('--read-exit-cell');
646
+ let readExitBytes: number | null = null;
647
+ if (readExitIdx >= 0) {
648
+ const raw = argv[readExitIdx + 1];
649
+ const parsed = raw === undefined ? Number.NaN : Number.parseInt(raw, 10);
650
+ if (!Number.isFinite(parsed) || parsed <= 0) {
651
+ process.stderr.write(
652
+ 'bench_hook_latency: --read-exit-cell requires a positive integer byte count\n',
653
+ );
654
+ return 2;
655
+ }
656
+ if (gate || update || baselineIdx >= 0) {
657
+ process.stderr.write(
658
+ 'bench_hook_latency: --read-exit-cell is measurement-only — a padded payload must ' +
659
+ "never write this tree's budget row or regression baseline\n",
660
+ );
661
+ return 2;
662
+ }
663
+ if (viaCli) {
664
+ process.stderr.write(
665
+ 'bench_hook_latency: --read-exit-cell drives the bundle directly and does not use ' +
666
+ 'the --via-cli shim path — pick one\n',
667
+ );
668
+ return 2;
669
+ }
670
+ readExitBytes = parsed;
671
+ }
672
+
489
673
  const bundleIdx = argv.indexOf('--bundle');
490
674
  if (bundleIdx >= 0) {
491
675
  const raw = argv[bundleIdx + 1];
@@ -539,6 +723,26 @@ export function main(argv: string[] = process.argv.slice(2)): number {
539
723
  return 2;
540
724
  }
541
725
 
726
+ if (readExitBytes !== null) {
727
+ // Its own mode: the cell alternates two payload sizes internally, so
728
+ // running it alongside the ordinary sweep would report a padded number
729
+ // under an unpadded label.
730
+ const cellWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ac-hook-readexit-'));
731
+ try {
732
+ // `post_tool_use` only. That is where the large cell lives: a
733
+ // `tool_response` does not exist on a `PreToolUse` payload, which is
734
+ // the same objection `syntheticPayload` raises against padding `stop`.
735
+ const cell = benchReadExitCell('post_tool_use', runs, cellWorkspace, readExitBytes);
736
+ process.stdout.write(renderReadExitCell(cell));
737
+ process.stdout.write(
738
+ 'ℹ️ measurement only (no gate) — this cell never writes a budget row\n',
739
+ );
740
+ } finally {
741
+ fs.rmSync(cellWorkspace, { recursive: true, force: true });
742
+ }
743
+ return 0;
744
+ }
745
+
542
746
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ac-hook-bench-'));
543
747
  const results: EventResult[] = [];
544
748
  let control: EventResult | null = null;
@@ -45,6 +45,7 @@ import * as path from 'node:path';
45
45
  import process from 'node:process';
46
46
  import { fileURLToPath, pathToFileURL } from 'node:url';
47
47
 
48
+ import { evaluateDashboardOnDisk, parseModeToken, type DashboardMode } from '../agent-src/scripts/dashboard_mode.js';
48
49
  import { GateLedger } from './_lib/gate_ledger.js';
49
50
  import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
50
51
  import { parse_frontmatter } from './validate_frontmatter.js';
@@ -390,6 +391,39 @@ export function main(argv: readonly string[]): number {
390
391
  const json = renderJson(entries);
391
392
 
392
393
  if (argv.includes('--check')) {
394
+ // `--untracked-mode` is the same three-state table the dashboard's
395
+ // `--check` already runs, on a second generated artefact: in-index is a
396
+ // failure, absent is the declared state, present is still compared
397
+ // against a fresh render. Untracked never means unchecked. The table
398
+ // itself lives in `dashboard_mode.ts` and is imported rather than
399
+ // restated, so the two artefacts cannot drift into different verdicts
400
+ // for the same observable state.
401
+ const mode: DashboardMode =
402
+ argv.map(parseModeToken).find((m) => m !== null) ?? 'tracked';
403
+ if (mode === 'untracked') {
404
+ let failed = false;
405
+ for (const [abs, rel, rendered] of [
406
+ [mdPath, `${ARCHIVE_REL}/${INDEX_MD}`, md],
407
+ [jsonPath, `${ARCHIVE_REL}/${INDEX_JSON}`, json],
408
+ ] as const) {
409
+ const v = evaluateDashboardOnDisk({
410
+ mode,
411
+ target: abs,
412
+ repo_root: ROOT,
413
+ rendered,
414
+ rel,
415
+ regen: 'Run `task build-archive-index` to regenerate.',
416
+ noun: 'archive index',
417
+ });
418
+ if (v.stale) {
419
+ failed = true;
420
+ process.stderr.write(v.error ?? '');
421
+ } else if (!quiet && v.ok !== null) {
422
+ process.stdout.write(v.ok);
423
+ }
424
+ }
425
+ return failed ? 1 : 0;
426
+ }
393
427
  const stale = [
394
428
  _read(mdPath) === md ? null : `${ARCHIVE_REL}/${INDEX_MD}`,
395
429
  _read(jsonPath) === json ? null : `${ARCHIVE_REL}/${INDEX_JSON}`,