@event4u/agent-config 14.6.0 → 14.8.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 (258) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +255 -145
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  7. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  8. package/dist/agent-src/commands/pr/create.md +14 -2
  9. package/dist/agent-src/commands/pr/merge.md +379 -0
  10. package/dist/agent-src/commands/roadmap/process-full.md +272 -32
  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/roadmap-execution-contract.md +95 -2
  15. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
  16. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  17. package/dist/agent-src/rules/context-hygiene.md +2 -0
  18. package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
  19. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  21. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  22. package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
  23. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  24. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  25. package/dist/agent-src/rules/skill-quality.md +0 -2
  26. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  27. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  28. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  29. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  30. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  31. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  32. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  33. package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
  34. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  35. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  36. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  37. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  38. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  39. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  40. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  41. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  42. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  43. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  44. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  45. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  46. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  47. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  48. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  49. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  50. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  51. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  52. package/dist/agent-src/skills/security/SKILL.md +8 -0
  53. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  54. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  55. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  56. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  57. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  58. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  59. package/dist/agent-src/templates/roadmaps.md +65 -4
  60. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  61. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  62. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  63. package/dist/cli/registry.js +2 -1
  64. package/dist/cli/registry.js.map +1 -1
  65. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  66. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
  67. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  68. package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
  69. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
  70. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  71. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
  72. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  73. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
  74. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  75. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  76. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
  77. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  78. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  79. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  80. package/dist/cli-delegate/cmd_conformance.js +466 -716
  81. package/dist/cli-delegate/cmd_converge.js +3 -3
  82. package/dist/cli-delegate/cmd_doctor.js +10 -10
  83. package/dist/cli-delegate/cmd_explain.js +1 -1
  84. package/dist/cli-delegate/cmd_export.js +10 -9
  85. package/dist/cli-delegate/cmd_handoff.js +10 -9
  86. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  89. package/dist/cli-delegate/cmd_preflight.js +5 -5
  90. package/dist/cli-delegate/cmd_prune.js +11 -10
  91. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  92. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  93. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  94. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  95. package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
  96. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  97. package/dist/cli-delegate/cmd_sync.js +11 -10
  98. package/dist/cli-delegate/cmd_uninstall.js +13 -11
  99. package/dist/cli-delegate/cmd_update.js +4 -4
  100. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  101. package/dist/cli-delegate/cmd_validate.js +11 -10
  102. package/dist/cli-delegate/cmd_versions.js +3 -3
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +111 -37
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +5 -5
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +15 -12
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +11 -8
  112. package/dist/hooks/dispatch.js +3165 -2192
  113. package/dist/install/claudePathsPlan.js +187 -0
  114. package/dist/install/claudePathsPlan.js.map +1 -0
  115. package/dist/install/globalRuleLayers.js +169 -0
  116. package/dist/install/globalRuleLayers.js.map +1 -0
  117. package/dist/install/hostLayerFingerprint.js +136 -0
  118. package/dist/install/hostLayerFingerprint.js.map +1 -0
  119. package/dist/install/install.mjs +553 -425
  120. package/dist/install/partitionEligibility.js +432 -0
  121. package/dist/install/partitionEligibility.js.map +1 -0
  122. package/dist/install/ruleLayerPartition.js +157 -0
  123. package/dist/install/ruleLayerPartition.js.map +1 -0
  124. package/dist/mcp/registry-manifest.json +3 -3
  125. package/dist/mcp/server.json +2 -2
  126. package/dist/mcp/server.mjs +1863 -1234
  127. package/dist/router.json +1 -1
  128. package/dist/scripts/_lib/agent_src.js +41 -0
  129. package/dist/scripts/_lib/agent_src.js.map +1 -1
  130. package/dist/scripts/_lib/install_layout.js +83 -0
  131. package/dist/scripts/_lib/install_layout.js.map +1 -0
  132. package/dist/scripts/_lib/installed_lock.js +444 -0
  133. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  134. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  135. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  136. package/dist/scripts/condense.js +63 -251
  137. package/dist/scripts/condense.js.map +1 -1
  138. package/dist/server/schemas/settings.js +4 -0
  139. package/dist/server/schemas/settings.js.map +1 -1
  140. package/docs/contracts/settings-classes.md +12 -6
  141. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  142. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  143. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  144. package/docs/guidelines/agent-infra/false-green.md +47 -1
  145. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  146. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  147. package/package.json +1 -1
  148. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  149. package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  150. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  151. package/src/agent-src/scripts/gate_budget.ts +282 -0
  152. package/src/agent-src/scripts/gate_execute.ts +72 -31
  153. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  154. package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
  155. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  156. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  157. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  158. package/src/config/agent-settings.template.yml +27 -2
  159. package/src/config/ci-local-parity.yml +39 -0
  160. package/src/config/estate-count-budget.json +612 -3
  161. package/src/config/evaluator-budgets.json +3 -3
  162. package/src/config/gate-coverage.yml +220 -12
  163. package/src/config/gate-violation-baselines.json +73 -63
  164. package/src/config/gitignore-block.txt +15 -2
  165. package/src/config/hook-latency-budget.json +5 -0
  166. package/src/config/rule-activation-census.json +14 -6
  167. package/src/config/rule-stub-ceilings.json +34 -7
  168. package/src/install/claudePathsPlan.ts +196 -0
  169. package/src/install/globalRuleLayers.ts +197 -0
  170. package/src/install/hostLayerFingerprint.ts +145 -0
  171. package/src/install/partitionEligibility.ts +502 -0
  172. package/src/install/ruleLayerPartition.ts +212 -0
  173. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  174. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  175. package/src/scripts/_dispatch.bash +17 -1
  176. package/src/scripts/_lib/ac_heading.ts +30 -0
  177. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  178. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  179. package/src/scripts/_lib/agent_src.ts +42 -0
  180. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  181. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  182. package/src/scripts/_lib/envelope_grounding.ts +49 -9
  183. package/src/scripts/_lib/installed_lock.ts +50 -5
  184. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  185. package/src/scripts/_lib/md_section.ts +129 -0
  186. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  187. package/src/scripts/_lib/scope_guard.sh +26 -3
  188. package/src/scripts/_lib/self_repair.ts +19 -0
  189. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  190. package/src/scripts/adr/evidence_census.ts +899 -0
  191. package/src/scripts/adr/regenerate_index.ts +12 -32
  192. package/src/scripts/adr_cite_check.ts +540 -39
  193. package/src/scripts/ai_council/cli_help.ts +55 -0
  194. package/src/scripts/audit_adr_coverage.ts +103 -17
  195. package/src/scripts/before_complete_hook.ts +153 -28
  196. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  197. package/src/scripts/bench_hook_latency.ts +204 -0
  198. package/src/scripts/build_archive_index.ts +34 -0
  199. package/src/scripts/check_adr_frontmatter.ts +574 -39
  200. package/src/scripts/check_hook_bundle_content.ts +219 -0
  201. package/src/scripts/check_new_adr_evidence.ts +529 -0
  202. package/src/scripts/check_references.ts +78 -0
  203. package/src/scripts/check_release_published.ts +32 -11
  204. package/src/scripts/check_rule_layer_partition.ts +531 -0
  205. package/src/scripts/check_single_delivery.ts +194 -17
  206. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  207. package/src/scripts/ci_settle.ts +170 -0
  208. package/src/scripts/condense.ts +79 -259
  209. package/src/scripts/council_cli.ts +3 -3
  210. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  211. package/src/scripts/external_sources_denylist.json +4 -1
  212. package/src/scripts/hook_manifest.json +1 -1
  213. package/src/scripts/hook_manifest.yaml +77 -14
  214. package/src/scripts/hooks/block_no_verify.ts +61 -10
  215. package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
  216. package/src/scripts/hooks/concern_registry.ts +4 -0
  217. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  218. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  219. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  220. package/src/scripts/hooks/host_semantics.ts +21 -0
  221. package/src/scripts/hooks/injection_budget.ts +13 -3
  222. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  223. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  224. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  225. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  226. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  227. package/src/scripts/hooks/state_io.ts +870 -21
  228. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  229. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  230. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  231. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  232. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  233. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  234. package/src/scripts/injection_scan_hook.ts +137 -5
  235. package/src/scripts/install.ts +16 -11
  236. package/src/scripts/language_mirror_hook.ts +561 -25
  237. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  238. package/src/scripts/lint_hook_manifest.ts +49 -0
  239. package/src/scripts/lint_plan_risk_register.ts +4 -3
  240. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  241. package/src/scripts/lint_skill_router_head.ts +4 -4
  242. package/src/scripts/mcp_server/tools.ts +2 -2
  243. package/src/scripts/new_roadmap.ts +136 -0
  244. package/src/scripts/pack_mcp_content.ts +20 -5
  245. package/src/scripts/release.ts +37 -38
  246. package/src/scripts/report_layer_overlap.ts +6 -0
  247. package/src/scripts/routing_doctor.ts +5 -1
  248. package/src/scripts/self_repair_hook.ts +37 -5
  249. package/src/scripts/session_register_hook.ts +11 -0
  250. package/src/scripts/skill_usage_collect.ts +11 -1
  251. package/src/scripts/skill_usage_report.ts +278 -8
  252. package/src/scripts/sync_pr_branch.ts +170 -20
  253. package/src/server/schemas/settings.ts +8 -0
  254. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  255. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  256. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  257. package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
  258. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
@@ -41,6 +41,7 @@ import {
41
41
  } from '../_lib/subagent_capsule.js';
42
42
  import { collectGrounding } from '../_lib/envelope_grounding.js';
43
43
  import { atomic_write_json } from '../hooks/state_io.js';
44
+ import { env_session_id } from '../sessions_cli.js';
44
45
 
45
46
  export interface RecycleResult {
46
47
  code: 0 | 1 | 2;
@@ -185,7 +186,14 @@ export function runSessionRecycle(
185
186
  // composed. A model-written branch is a claim; this is a reading. The
186
187
  // composer cannot override them — a "next_task" it got wrong is a
187
188
  // proposal, a "head" it got wrong is a silent stale resume.
188
- const grounding = collectGrounding(projectRoot);
189
+ // The session id is the ONLY channel to this session's verify state, which
190
+ // the producer keys per session. `env_session_id` is the canonical resolver
191
+ // (package variable first, then the host's `CLAUDE_CODE_SESSION_ID`) and is
192
+ // imported rather than re-implemented — a second copy of that precedence
193
+ // order would drift from the one a test covers. A host that exports neither
194
+ // yields `null`, and `last_verify` is then simply absent from the envelope
195
+ // (the delete-then-set below already treats an unreadable fact as absent).
196
+ const grounding = collectGrounding(projectRoot, env_session_id());
189
197
  // Drop the composer's factual keys UNCONDITIONALLY first. Guarding each
190
198
  // assignment on `!== null` would leave a model-composed branch or head
191
199
  // standing whenever the git read fails — the consumer would then compare
@@ -220,6 +220,7 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
220
220
  (deliberate recycle instead of auto-compact). Flags:
221
221
  --file <json> | --template; default reads stdin
222
222
  roadmap:progress Regenerate agents/roadmaps-progress.md from open roadmaps
223
+ (archives completed roadmaps; --no-archive to skip)
223
224
  roadmap:progress-check Fail if agents/roadmaps-progress.md is stale (for CI)
224
225
  roadmap:archive Archive completed roadmaps (branch-touched by default;
225
226
  --all for every complete one; --dry-run to preview)
@@ -237,6 +238,12 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
237
238
  capabilities:index Regenerate CAPABILITIES.yaml — the package coverage index
238
239
  (capability area → coverage → backing skills/commands → gaps).
239
240
  Pass --check to fail if stale (for CI). Reads src/ (package repo).
241
+ adr:effective Effective state of one decision record: status, the
242
+ Decision section verbatim, clauses its own amendments
243
+ superseded, active amendments, provenance, evidence,
244
+ review trigger and trigger state. Read-only, and it
245
+ authorizes nothing. Exit 1 when the record still
246
+ asserts what its own amendment retired. Flag: --json
240
247
  settings:check Validate .agent-settings.yml against the YAML-subset contract
241
248
  (docs/contracts/settings-sync-yaml-subset.md). Read-only.
242
249
  Exit 0 clean, 1 finding(s), 2 file absent / unreadable.
@@ -717,10 +724,14 @@ cmd_brand_status() {
717
724
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_brand_status.ts" "$@"
718
725
  }
719
726
 
727
+ # `--archive`: a completed roadmap is archived here rather than reported as a
728
+ # warning the caller has to act on. `roadmap:progress-check` never passes it —
729
+ # a CI gate must not mutate the tree it is checking. Pass `--no-archive` to get
730
+ # the dashboard-only behaviour back.
720
731
  cmd_roadmap_progress() {
721
732
  local script
722
733
  script="$(resolve_script "dist/agent-src/scripts/update_roadmap_progress.ts" ".augment/scripts/update_roadmap_progress.ts")"
723
- exec_ts "$script" "$@"
734
+ exec_ts "$script" --archive "$@"
724
735
  }
725
736
 
726
737
  cmd_roadmap_progress_check() {
@@ -749,6 +760,10 @@ cmd_capabilities_index() {
749
760
  exec_ts "$script" "$@"
750
761
  }
751
762
 
763
+ cmd_adr_effective() {
764
+ exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_adr_effective.ts" "$@"
765
+ }
766
+
752
767
  cmd_first_run() {
753
768
  local script
754
769
  script="$(resolve_script "src/scripts/first-run.sh")"
@@ -1446,6 +1461,7 @@ main() {
1446
1461
  roadmap:archive) cmd_roadmap_archive "$@" ;;
1447
1462
  gates) cmd_gates "$@" ;;
1448
1463
  capabilities:index) cmd_capabilities_index "$@" ;;
1464
+ adr:effective) cmd_adr_effective "$@" ;;
1449
1465
  hooks:install) cmd_hooks_install "$@" ;;
1450
1466
  keys:install-anthropic) cmd_keys_install_anthropic "$@" ;;
1451
1467
  keys:install-openai) cmd_keys_install_openai "$@" ;;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The one matcher for a roadmap's `## Acceptance criteria` heading.
3
+ *
4
+ * It lives here because the same defect has now been found three times, in
5
+ * three separately-authored copies of this regex:
6
+ *
7
+ * 1. `dispatch_r2_reviewer.extractAcceptanceCriteria` was case-sensitive, so
8
+ * `## Acceptance criteria` extracted nothing (zcs-close R2 review, 2026-08-09).
9
+ * 2. The same function was end-anchored, so a heading carrying a qualifier —
10
+ * `## Acceptance criteria (per phase, on promotion to ready)` — extracted
11
+ * nothing (measured 2026-08-18).
12
+ * 3. `lint_plan_risk_register.extractFeatures` carried BOTH faults after the
13
+ * other two were fixed, so the substantial-change heuristic hashed the empty
14
+ * string for eight of the thirty-two ready roadmaps — including the roadmap
15
+ * that repaired it. Any edit to those roadmaps' criteria was invisible to the
16
+ * gate, which is the silent half of the failure: the gate still exited 0.
17
+ *
18
+ * Two copies drift. Three copies drift twice. The predicate is a shared constant
19
+ * so the next author cannot fix one call site and leave another blind.
20
+ *
21
+ * `\b` after `criteria` is the deliberate near-miss guard: `## Acceptance
22
+ * criteriaXYZ` is a different heading and must not match. It is asserted from
23
+ * both directions in `tests/scripts/lint_plan_risk_register.test.ts`.
24
+ */
25
+ export const AC_HEADING_RE = /^##\s+acceptance criteria\b/i;
26
+
27
+ /** True when `line` is a roadmap's acceptance-criteria section heading. */
28
+ export function isAcceptanceCriteriaHeading(line: string): boolean {
29
+ return AC_HEADING_RE.test(line);
30
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Shared ADR frontmatter reader — coverage.
3
+ *
4
+ * The load-bearing test is `corpus equivalence`: the shared reader is run over
5
+ * every real ADR alongside a verbatim copy of each parser it replaces, and the
6
+ * scalar views must match key-for-key. A refactor of a parser three call sites
7
+ * depend on is only safe if it is provably the same function on the input that
8
+ * exists, and a hand-written fixture cannot establish that — the corpus can.
9
+ *
10
+ * The nested cases then cover what the old parsers could NOT do, and the
11
+ * negative cases pin the discriminations that make the new behaviour a
12
+ * decision rather than an accident.
13
+ */
14
+ import { readdirSync, readFileSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ import { describe, expect, it } from 'vitest';
19
+
20
+ import {
21
+ evidenceOf,
22
+ isLowEvidenceAccepted,
23
+ provenanceOf,
24
+ readAdrFrontmatter,
25
+ readAdrFrontmatterScalars,
26
+ } from './adr_frontmatter.js';
27
+
28
+ /**
29
+ * Keys the shared reader parses as NESTED, where equivalence with the legacy
30
+ * parsers was never the promise.
31
+ *
32
+ * The legacy fold produced a meaningless string for these — ADR-239's evidence
33
+ * block came back as `"strength: E3 discovery: complete basis: - url1 - url2"`,
34
+ * a value nothing read and nothing could use. The equivalence contract is about
35
+ * SCALAR keys, which is where the three parsers actually diverged in ways a
36
+ * caller could observe. Naming the exception here rather than filtering it
37
+ * silently, and asserting below that the legacy value really was garbage — a
38
+ * skip nobody checks is how a real divergence hides behind an intended one.
39
+ */
40
+ const NESTED_KEYS = ['provenance', 'evidence'];
41
+
42
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
43
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
44
+ const DECISIONS = path.join(REPO_ROOT, 'docs', 'decisions');
45
+
46
+ /** Verbatim copy of the parser `check_adr_frontmatter.ts` carried. */
47
+ function legacyStrictParser(text: string): Record<string, string> | null {
48
+ if (!text.startsWith('---\n')) return null;
49
+ const end = text.indexOf('\n---\n', 4);
50
+ if (end === -1) return null;
51
+ const out: Record<string, string> = {};
52
+ let key: string | null = null;
53
+ for (const raw of text.slice(4, end).split('\n')) {
54
+ const line = raw.replace(/\s+$/, '');
55
+ if (!line || line.trimStart().startsWith('#')) continue;
56
+ if (/^\s/.test(line) && key) {
57
+ out[key] = `${out[key]} ${line.trim()}`.trim();
58
+ continue;
59
+ }
60
+ const idx = line.indexOf(':');
61
+ if (idx === -1) continue;
62
+ key = line.slice(0, idx).trim();
63
+ out[key] = line
64
+ .slice(idx + 1)
65
+ .trim()
66
+ .replace(/^["'](.*)["']$/, '$1');
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /** Verbatim copy of the parser `adr_cite_check.ts` carried (adds the `>-` strip). */
72
+ function legacyCiteParser(text: string): Record<string, string> | null {
73
+ if (!text.startsWith('---\n')) return null;
74
+ const end = text.indexOf('\n---\n', 4);
75
+ if (end === -1) return null;
76
+ const out: Record<string, string> = {};
77
+ let key: string | null = null;
78
+ for (const raw of text.slice(4, end).split('\n')) {
79
+ const line = raw.replace(/\s+$/, '');
80
+ if (!line || line.trimStart().startsWith('#')) continue;
81
+ if (/^\s/.test(line) && key !== null) {
82
+ out[key] = `${out[key]} ${line.trim()}`.trim();
83
+ continue;
84
+ }
85
+ const idx = line.indexOf(':');
86
+ if (idx === -1) continue;
87
+ key = line.slice(0, idx).trim();
88
+ out[key] = line
89
+ .slice(idx + 1)
90
+ .trim()
91
+ .replace(/^["'](.*)["']$/, '$1')
92
+ .replace(/^>-?$/, '');
93
+ }
94
+ return out;
95
+ }
96
+
97
+ function adrFiles(): string[] {
98
+ return readdirSync(DECISIONS)
99
+ .filter((name) => /^ADR-.*\.md$/.test(name))
100
+ .sort()
101
+ .map((name) => path.join(DECISIONS, name));
102
+ }
103
+
104
+ describe('corpus equivalence with the parsers it replaces', () => {
105
+ const files = adrFiles();
106
+
107
+ it('finds the corpus (guards against a silently empty sweep)', () => {
108
+ expect(files.length).toBeGreaterThan(150);
109
+ });
110
+
111
+ it('matches the cite parser key-for-key on every real ADR', () => {
112
+ const divergent: string[] = [];
113
+ for (const file of files) {
114
+ const text = readFileSync(file, 'utf8');
115
+ const legacy = legacyCiteParser(text);
116
+ const shared = readAdrFrontmatterScalars(text);
117
+ if (legacy === null || shared === null) {
118
+ if (legacy !== shared) divergent.push(`${path.basename(file)}: null-ness differs`);
119
+ continue;
120
+ }
121
+ for (const [key, value] of Object.entries(legacy)) {
122
+ if (NESTED_KEYS.includes(key)) continue;
123
+ if (shared[key] !== value) {
124
+ divergent.push(`${path.basename(file)}#${key}: ${JSON.stringify(value)} vs ${JSON.stringify(shared[key])}`);
125
+ }
126
+ }
127
+ }
128
+ expect(divergent).toEqual([]);
129
+ });
130
+
131
+ it('the legacy value for a nested key really was garbage — so the exception is earned', () => {
132
+ // If a legacy parser ever produced something usable for a nested key,
133
+ // excluding it above would be hiding a real divergence. Assert the
134
+ // premise instead of trusting it.
135
+ let checked = 0;
136
+ for (const file of files) {
137
+ const text = readFileSync(file, 'utf8');
138
+ const legacy = legacyCiteParser(text);
139
+ if (legacy === null) continue;
140
+ for (const key of NESTED_KEYS) {
141
+ const value = legacy[key];
142
+ if (value === undefined) continue;
143
+ checked += 1;
144
+ // A usable scalar would not carry an inner `key: value` pair or
145
+ // a bare list marker. The legacy fold always does.
146
+ expect(value).toMatch(/(\w+:\s)|(^|\s)- /);
147
+ }
148
+ }
149
+ // At least one record must exercise this, or the assertion is vacuous.
150
+ expect(checked).toBeGreaterThan(0);
151
+ });
152
+
153
+ it('matches the strict parser on every real ADR, modulo the documented `>-` strip', () => {
154
+ const divergent: string[] = [];
155
+ for (const file of files) {
156
+ const text = readFileSync(file, 'utf8');
157
+ const legacy = legacyStrictParser(text);
158
+ const shared = readAdrFrontmatterScalars(text);
159
+ if (legacy === null || shared === null) continue;
160
+ for (const [key, value] of Object.entries(legacy)) {
161
+ // The strict parser kept the fold marker and then folded the
162
+ // block onto it (`>- Reopen when …`); the cite parser stripped
163
+ // it, and the shared reader keeps the cite behaviour. That is
164
+ // the one intended divergence, so it is normalised here and
165
+ // named rather than smoothed away.
166
+ if (NESTED_KEYS.includes(key)) continue;
167
+ const normalised = value.replace(/^\|[-+]?\s*|^>[-+]?\s*/, '');
168
+ if (shared[key] !== normalised) {
169
+ divergent.push(`${path.basename(file)}#${key}`);
170
+ }
171
+ }
172
+ }
173
+ expect(divergent).toEqual([]);
174
+ });
175
+ });
176
+
177
+ describe('scalar behaviour preserved', () => {
178
+ it('reads plain scalars and strips quotes', () => {
179
+ const fm = readAdrFrontmatter('---\nadr: 238\nstatus: "accepted"\n---\nbody\n');
180
+ expect(fm?.scalars.adr).toBe('238');
181
+ expect(fm?.scalars.status).toBe('accepted');
182
+ });
183
+
184
+ it('folds a >- block into one scalar', () => {
185
+ const fm = readAdrFrontmatter(
186
+ '---\nreview_trigger: >-\n Reopen when the standard is withdrawn\n or the contract is retired.\n---\n',
187
+ );
188
+ expect(fm?.scalars.review_trigger).toBe('Reopen when the standard is withdrawn or the contract is retired.');
189
+ });
190
+
191
+ it('folds an unmarked indented continuation, as the old readers did', () => {
192
+ const fm = readAdrFrontmatter('---\ndecision: a-long\n slug-continued\n---\n');
193
+ expect(fm?.scalars.decision).toBe('a-long slug-continued');
194
+ });
195
+
196
+ it('returns null when there is no frontmatter at all', () => {
197
+ expect(readAdrFrontmatter('# ADR 0001 — per-area\n\n> Area: `router`\n')).toBeNull();
198
+ });
199
+
200
+ it('skips comment lines', () => {
201
+ const fm = readAdrFrontmatter('---\n# a comment\nadr: 1\n---\n');
202
+ expect(fm?.scalars.adr).toBe('1');
203
+ expect(fm?.keyOrder).toEqual(['adr']);
204
+ });
205
+ });
206
+
207
+ describe('nested axes — what three parsers could not read', () => {
208
+ const doc = [
209
+ '---',
210
+ 'adr: 238',
211
+ 'status: accepted',
212
+ 'provenance:',
213
+ ' kind: agentic',
214
+ ' decision_makers: [claude-sonnet-4-5, gpt-4o]',
215
+ ' human_directed: false',
216
+ ' agentic_mode: council',
217
+ 'evidence:',
218
+ ' strength: E1',
219
+ ' discovery: incomplete',
220
+ ' basis:',
221
+ ' - docs/CLAIMS.md#code-graph-retrieval-null',
222
+ ' - ADR-229:52',
223
+ 'authority_basis: evidence',
224
+ '---',
225
+ 'body',
226
+ '',
227
+ ].join('\n');
228
+
229
+ it('parses a nested map', () => {
230
+ const p = provenanceOf(readAdrFrontmatter(doc)!);
231
+ expect(p).toEqual({
232
+ kind: 'agentic',
233
+ decisionMakers: ['claude-sonnet-4-5', 'gpt-4o'],
234
+ humanDirected: 'false',
235
+ agenticMode: 'council',
236
+ });
237
+ });
238
+
239
+ it('parses an inline list and a block list in the same record', () => {
240
+ const e = evidenceOf(readAdrFrontmatter(doc)!);
241
+ expect(e?.strength).toBe('E1');
242
+ expect(e?.discovery).toBe('incomplete');
243
+ expect(e?.basis).toEqual(['docs/CLAIMS.md#code-graph-retrieval-null', 'ADR-229:52']);
244
+ });
245
+
246
+ it('keeps nested keys out of the scalar map', () => {
247
+ const fm = readAdrFrontmatter(doc)!;
248
+ expect(fm.scalars.provenance).toBeUndefined();
249
+ expect(fm.scalars.evidence).toBeUndefined();
250
+ expect(fm.scalars.status).toBe('accepted');
251
+ expect(fm.scalars.authority_basis).toBe('evidence');
252
+ });
253
+
254
+ it('round-trips a nested record through the scalar view without losing the key', () => {
255
+ // The index reader only knows scalars; an absent key and an empty key
256
+ // print differently there, so the folded form has to be non-empty.
257
+ const flat = readAdrFrontmatterScalars(doc)!;
258
+ expect(flat.provenance).toContain('kind: agentic');
259
+ expect(flat.evidence).toContain('strength: E1');
260
+ });
261
+
262
+ it('reads an empty inline list as an empty list, not as absent', () => {
263
+ const fm = readAdrFrontmatter('---\nevidence:\n strength: E0\n basis: []\n---\n')!;
264
+ expect(evidenceOf(fm)?.basis).toEqual([]);
265
+ expect(evidenceOf(fm)?.strength).toBe('E0');
266
+ });
267
+ });
268
+
269
+ describe('negative cases — the discriminations are decisions, not accidents', () => {
270
+ it('does not turn a folded scalar into a map even when its prose contains a colon', () => {
271
+ const fm = readAdrFrontmatter(
272
+ '---\nreview_trigger: >-\n Reopen when: the standard is withdrawn.\n---\n',
273
+ )!;
274
+ expect(fm.nested.review_trigger).toBeUndefined();
275
+ expect(fm.scalars.review_trigger).toBe('Reopen when: the standard is withdrawn.');
276
+ });
277
+
278
+ it('reports a missing axis as null rather than inventing a default', () => {
279
+ const fm = readAdrFrontmatter('---\nadr: 1\nstatus: accepted\n---\n')!;
280
+ expect(provenanceOf(fm)).toBeNull();
281
+ expect(evidenceOf(fm)).toBeNull();
282
+ });
283
+
284
+ it('reports a malformed axis as null rather than half-reading it', () => {
285
+ const fm = readAdrFrontmatter('---\nprovenance: agentic\n---\n')!;
286
+ // A scalar where a map belongs: the accessor refuses it, and the raw
287
+ // value stays visible in `scalars` so a linter can report the shape.
288
+ expect(provenanceOf(fm)).toBeNull();
289
+ expect(fm.scalars.provenance).toBe('agentic');
290
+ });
291
+
292
+ it('drops an orphan indented line instead of attaching it to nothing', () => {
293
+ const fm = readAdrFrontmatter('---\n orphan: value\nadr: 1\n---\n')!;
294
+ expect(fm.scalars.orphan).toBeUndefined();
295
+ expect(fm.scalars.adr).toBe('1');
296
+ });
297
+ });
298
+
299
+ describe('low-evidence notice — provenance-independent by design', () => {
300
+ function build(status: string, kind: string, strength: string): string {
301
+ return [
302
+ '---',
303
+ `status: ${status}`,
304
+ 'provenance:',
305
+ ` kind: ${kind}`,
306
+ 'evidence:',
307
+ ` strength: ${strength}`,
308
+ ' discovery: incomplete',
309
+ '---',
310
+ '',
311
+ ].join('\n');
312
+ }
313
+
314
+ it('fires on an accepted agentic E0 record', () => {
315
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E0'))!)).toBe(true);
316
+ });
317
+
318
+ it('fires on an accepted agentic E1 record', () => {
319
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E1'))!)).toBe(true);
320
+ });
321
+
322
+ // The regression this rename exists for. The predicate used to require
323
+ // `kind === 'agentic'`, so an identical human E0 record printed nothing —
324
+ // while the notice is a statement about EVIDENCE strength, which a human
325
+ // snapshot has exactly as little of.
326
+ it('fires on an accepted HUMAN E0 record — the notice is about evidence, not authorship', () => {
327
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'human', 'E0'))!)).toBe(true);
328
+ });
329
+
330
+ it('fires on mixed and unknown provenance too', () => {
331
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'mixed', 'E1'))!)).toBe(true);
332
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'unknown', 'E0'))!)).toBe(true);
333
+ });
334
+
335
+ it('does NOT fire on E2 — the grade boundary is the whole point', () => {
336
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E2'))!)).toBe(false);
337
+ });
338
+
339
+ it('does NOT fire on E3 or E4', () => {
340
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'agentic', 'E3'))!)).toBe(false);
341
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('accepted', 'human', 'E4'))!)).toBe(false);
342
+ });
343
+
344
+ it('does NOT fire on a superseded record', () => {
345
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(build('superseded', 'agentic', 'E0'))!)).toBe(false);
346
+ });
347
+
348
+ it('does NOT fire when the axes are absent — silence is not a weak grade', () => {
349
+ expect(isLowEvidenceAccepted(readAdrFrontmatter('---\nstatus: accepted\n---\n')!)).toBe(false);
350
+ });
351
+
352
+ // The exemption, and the reason provenance was the wrong gate: an owner
353
+ // purpose statement is legitimately E0 and its alternatives ARE foreclosed —
354
+ // by ownership, not by evidence. `authority_basis` is the declared field for
355
+ // that, so it is the field that decides.
356
+ it('does NOT fire on an owner_intent record, at E0', () => {
357
+ const doc = '---\nstatus: accepted\nauthority_basis: owner_intent\nevidence:\n strength: E0\n discovery: complete\n---\n';
358
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(doc)!)).toBe(false);
359
+ });
360
+
361
+ it('DOES fire on a thin human record that carries no owner claim', () => {
362
+ const doc = '---\nstatus: accepted\nprovenance:\n kind: human\nevidence:\n strength: E0\n discovery: incomplete\n---\n';
363
+ expect(isLowEvidenceAccepted(readAdrFrontmatter(doc)!)).toBe(true);
364
+ });
365
+ });