@event4u/agent-config 14.5.0 → 14.7.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 (203) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +384 -3
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/decision.md +7 -0
  5. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  6. package/dist/agent-src/commands/analyze.md +1 -1
  7. package/dist/agent-src/commands/optimize/project.md +10 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +132 -4
  9. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  10. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
  11. package/dist/agent-src/rules/context-hygiene.md +2 -0
  12. package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
  13. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  14. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  15. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  16. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  17. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  18. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  19. package/dist/agent-src/rules/skill-quality.md +0 -2
  20. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  21. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  22. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  23. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  24. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  25. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  27. package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
  28. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  29. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  30. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  31. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  32. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  33. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  34. package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
  35. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  36. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  37. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  38. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  39. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  40. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  41. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  42. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  44. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  45. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  46. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  47. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  48. package/dist/agent-src/templates/roadmaps.md +45 -2
  49. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  50. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  51. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  52. package/dist/cli/registry.js +1 -1
  53. package/dist/cli/registry.js.map +1 -1
  54. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  55. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  56. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  57. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  58. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  59. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  60. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  61. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  62. package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
  63. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  64. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  65. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  66. package/dist/cli-delegate/cmd_conformance.js +459 -709
  67. package/dist/cli-delegate/cmd_doctor.js +6 -6
  68. package/dist/cli-delegate/cmd_explain.js +1 -1
  69. package/dist/cli-delegate/cmd_export.js +7 -6
  70. package/dist/cli-delegate/cmd_handoff.js +6 -6
  71. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  72. package/dist/cli-delegate/cmd_preflight.js +2 -2
  73. package/dist/cli-delegate/cmd_prune.js +7 -6
  74. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  75. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  76. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  77. package/dist/cli-delegate/cmd_sync.js +7 -6
  78. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  79. package/dist/cli-delegate/cmd_update.js +1 -1
  80. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  81. package/dist/cli-delegate/cmd_validate.js +7 -6
  82. package/dist/discovery/deprecation-report.md +1 -1
  83. package/dist/discovery/discovery-manifest.json +72 -29
  84. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  85. package/dist/discovery/discovery-manifest.summary.md +4 -4
  86. package/dist/discovery/dormancy-report.md +1 -1
  87. package/dist/discovery/orphan-report.md +1 -1
  88. package/dist/discovery/packs.json +10 -8
  89. package/dist/discovery/trust-report.md +3 -3
  90. package/dist/discovery/workspaces.json +8 -6
  91. package/dist/hooks/dispatch.js +3127 -2022
  92. package/dist/install/claudePathsPlan.js +187 -0
  93. package/dist/install/claudePathsPlan.js.map +1 -0
  94. package/dist/install/hostLayerFingerprint.js +136 -0
  95. package/dist/install/hostLayerFingerprint.js.map +1 -0
  96. package/dist/install/install.mjs +553 -425
  97. package/dist/install/partitionEligibility.js +276 -0
  98. package/dist/install/partitionEligibility.js.map +1 -0
  99. package/dist/mcp/registry-manifest.json +3 -3
  100. package/dist/mcp/server.json +2 -2
  101. package/dist/mcp/server.mjs +1744 -1191
  102. package/dist/router.json +1 -1
  103. package/dist/scripts/_lib/agent_src.js +41 -0
  104. package/dist/scripts/_lib/agent_src.js.map +1 -1
  105. package/dist/scripts/_lib/install_layout.js +83 -0
  106. package/dist/scripts/_lib/install_layout.js.map +1 -0
  107. package/dist/scripts/_lib/installed_lock.js +444 -0
  108. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  109. package/dist/scripts/condense.js +13 -164
  110. package/dist/scripts/condense.js.map +1 -1
  111. package/dist/server/schemas/settings.js +4 -0
  112. package/dist/server/schemas/settings.js.map +1 -1
  113. package/docs/contracts/settings-classes.md +12 -6
  114. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  115. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  116. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
  117. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  118. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  119. package/package.json +1 -1
  120. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  121. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  122. package/src/agent-src/scripts/gate_budget.ts +282 -0
  123. package/src/agent-src/scripts/gate_execute.ts +72 -31
  124. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  125. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  126. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  127. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  128. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  129. package/src/config/agent-settings.template.yml +27 -2
  130. package/src/config/ci-local-parity.yml +18 -0
  131. package/src/config/estate-count-budget.json +367 -3
  132. package/src/config/gate-violation-baselines.json +41 -41
  133. package/src/config/pack-size-budget.json +3 -2
  134. package/src/config/rule-activation-census.json +14 -6
  135. package/src/config/rule-stub-ceilings.json +19 -6
  136. package/src/install/claudePathsPlan.ts +196 -0
  137. package/src/install/hostLayerFingerprint.ts +145 -0
  138. package/src/install/partitionEligibility.ts +325 -0
  139. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  140. package/src/scripts/_dispatch.bash +6 -1
  141. package/src/scripts/_lib/ac_heading.ts +30 -0
  142. package/src/scripts/_lib/agent_src.ts +42 -0
  143. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  144. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  145. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  146. package/src/scripts/_lib/installed_lock.ts +50 -5
  147. package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
  148. package/src/scripts/_lib/md_section.ts +129 -0
  149. package/src/scripts/_lib/scope_guard.sh +86 -4
  150. package/src/scripts/_lib/self_repair.ts +19 -0
  151. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +81 -1
  153. package/src/scripts/adr/regenerate_index.ts +13 -2
  154. package/src/scripts/adr_cite_check.ts +424 -0
  155. package/src/scripts/ai_council/cli_help.ts +55 -0
  156. package/src/scripts/before_complete_hook.ts +153 -28
  157. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  158. package/src/scripts/check_adr_frontmatter.ts +187 -1
  159. package/src/scripts/check_references.ts +60 -0
  160. package/src/scripts/check_release_published.ts +32 -11
  161. package/src/scripts/check_single_delivery.ts +517 -0
  162. package/src/scripts/ci_settle.ts +170 -0
  163. package/src/scripts/condense.ts +20 -170
  164. package/src/scripts/council_cli.ts +3 -3
  165. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  166. package/src/scripts/hook_manifest.json +1 -1
  167. package/src/scripts/hook_manifest.yaml +77 -14
  168. package/src/scripts/hooks/block_no_verify.ts +61 -10
  169. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  170. package/src/scripts/hooks/concern_registry.ts +4 -0
  171. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  172. package/src/scripts/hooks/run_continuation_hook.ts +887 -68
  173. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  174. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  175. package/src/scripts/hooks/state_io.ts +782 -20
  176. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  178. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  179. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  181. package/src/scripts/install.sh +12 -2
  182. package/src/scripts/install.ts +16 -11
  183. package/src/scripts/language_mirror_hook.ts +561 -25
  184. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  185. package/src/scripts/lint_plan_risk_register.ts +4 -3
  186. package/src/scripts/lint_skill_router_head.ts +4 -4
  187. package/src/scripts/mcp_server/tools.ts +2 -2
  188. package/src/scripts/memory_consolidation.ts +189 -0
  189. package/src/scripts/memory_eviction.ts +384 -0
  190. package/src/scripts/new_roadmap.ts +136 -0
  191. package/src/scripts/pack_mcp_content.ts +20 -5
  192. package/src/scripts/release.ts +37 -38
  193. package/src/scripts/report_layer_overlap.ts +78 -0
  194. package/src/scripts/report_memory_pointers.ts +622 -0
  195. package/src/scripts/self_repair_hook.ts +37 -5
  196. package/src/scripts/session_register_hook.ts +115 -15
  197. package/src/scripts/skill_usage_collect.ts +11 -1
  198. package/src/scripts/skill_usage_report.ts +278 -8
  199. package/src/scripts/sync_pr_branch.ts +7 -0
  200. package/src/server/schemas/settings.ts +8 -0
  201. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  202. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  203. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * ADR cite-check — evaluate a decision BEFORE citing it as a reason not to act.
4
+ *
5
+ * The defect this closes is one of ordering, not of permission. Measured across
6
+ * 26 days of transcripts: in at least 13 passages a change was parked or refused
7
+ * citing a named ADR, and in zero cases did the agent refuse an explicit
8
+ * overturn instruction. The lock held, the owner had to notice and void it
9
+ * retroactively, and only then did the work resume. Nothing in the tree turned
10
+ * "this ADR blocks the change" into "is this ADR still true?".
11
+ *
12
+ * So this tool answers exactly that question, and only from what is mechanically
13
+ * decidable:
14
+ *
15
+ * - **status** — a `superseded` / `deprecated` ADR is not a live lock at all.
16
+ * This alone resolves a class of blockage with certainty. `rejected` is
17
+ * deliberately excluded: on an ADR it records a rejected PROPOSAL, so the
18
+ * rejection is the live decision.
19
+ * - **successors and predecessors** — `superseded_by` / `supersedes`, plus the
20
+ * `amended_by` / `amends` pair.
21
+ * - **amendment blocks in the body** — three unreconciled conventions exist in
22
+ * the corpus, so all three are matched.
23
+ * - **back-references** — other ADRs that name this one. A decision reopened
24
+ * by a later ADR that never linked back is invisible from the ADR's own file
25
+ * (measured: ADR-035 still asserts a rejection ADR-232 reopened).
26
+ * - **review_trigger** — verbatim, plus a state.
27
+ *
28
+ * `indeterminate` is a FIRST-CLASS trigger result, not a failure. Every
29
+ * `review_trigger` in the corpus is a semantic condition ("reopen when the
30
+ * capacity premise changes", "if a fifth band appears"), so forcing them to a
31
+ * boolean would convert uncertainty into either permission or blockage. Both
32
+ * council seats (2026-08-19) independently rejected building a machine-readable
33
+ * trigger grammar as a prerequisite for exactly this reason. An `indeterminate`
34
+ * result means: this may not be presented as an unqualified lock — route it.
35
+ *
36
+ * Usage:
37
+ * ./scripts-run src/scripts/adr_cite_check ADR-211
38
+ * ./scripts-run src/scripts/adr_cite_check ADR-001 ADR-035 --json
39
+ *
40
+ * Exit codes: 0 every reference resolved · 1 at least one did not · 2 usage.
41
+ */
42
+
43
+ import * as fs from 'node:fs';
44
+ import * as path from 'node:path';
45
+ import { fileURLToPath, pathToFileURL } from 'node:url';
46
+
47
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
48
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
49
+
50
+ /**
51
+ * The surfaces a decision can live on. Six exist in this tree; this tool reads
52
+ * the two that carry numbered ADRs and can therefore be cited as `ADR-NNN`.
53
+ *
54
+ * Deliberately NOT covered, stated rather than silently skipped:
55
+ * `docs/contracts/adr-*.md` (15 files) and `agents/settings/contexts/adr-*.md`
56
+ * (6) are named-slug contracts with their own status vocabulary (`Decided`,
57
+ * `locked`) and no ADR number, so a numeric reference can never resolve to one;
58
+ * `agents/decisions/` is a ledger of rows, not of files. A tool that silently
59
+ * saw one of six surfaces would report a real lock as unknown, which reads as
60
+ * "no constraint" and is worse than the stall it replaces — so the gap is
61
+ * printed in `--json` under `surfaces_not_scanned` and named here.
62
+ *
63
+ * The per-area surface took two passes to get honest. Its files are
64
+ * `<area>/NNNN-<slug>.md` with no `ADR-` prefix, so the flat filename pattern
65
+ * could never match one — the directory was listed as scanned and resolved
66
+ * nothing, which is the same false "not found" the paragraph above warns
67
+ * about, committed by this very file. It resolves by path and by the
68
+ * `ADR-<area>-NNNN` citation form now; a BARE number still cannot address one,
69
+ * because per-area numbering restarts per area and five files here are `0001`.
70
+ * See `PARTIAL_COVERAGE` for what a per-area hit does and does not carry.
71
+ */
72
+ export const ADR_DIRS = ['docs/decisions', 'docs/adrs'] as const;
73
+
74
+ export const SURFACES_NOT_SCANNED = [
75
+ 'docs/contracts/adr-*.md — slug-named contracts, no ADR number to resolve against',
76
+ 'agents/settings/contexts/adr-*.md — shadow notes, own status vocabulary',
77
+ 'agents/decisions/ — row ledger, not per-decision files',
78
+ ] as const;
79
+
80
+ /**
81
+ * Partial coverage, stated because a silent partial is the failure this tool
82
+ * exists to avoid: `docs/adrs/<area>/` resolves by PATH and by the
83
+ * `ADR-<area>-NNNN` citation form, but those files carry a quote-block header
84
+ * rather than YAML frontmatter, so `status` / `review_trigger` / the link
85
+ * fields read as absent. A per-area result is an accurate location and an
86
+ * honestly empty metadata set — never a claim that the fields are empty in the
87
+ * document.
88
+ */
89
+ export const PARTIAL_COVERAGE = [
90
+ 'docs/adrs/<area>/ — resolves, but the header is a quote block, not YAML: metadata reads empty',
91
+ ] as const;
92
+
93
+ /** A trigger state. `none` means the ADR never recorded a reopen condition. */
94
+ export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired';
95
+
96
+ export interface CiteResult {
97
+ ref: string;
98
+ resolved: boolean;
99
+ file?: string;
100
+ status?: string;
101
+ date?: string;
102
+ decision?: string;
103
+ review_trigger?: string;
104
+ trigger_state: TriggerState;
105
+ supersedes?: string;
106
+ superseded_by?: string;
107
+ amends?: string;
108
+ amended_by?: string;
109
+ /** Amendment headings found in the body, any of the three conventions. */
110
+ amendment_blocks: string[];
111
+ /** Other ADR files whose text names this one. */
112
+ referenced_by: string[];
113
+ /** Why this may or may not be cited as a live lock. */
114
+ verdict: string;
115
+ }
116
+
117
+ /**
118
+ * A parsed citation. `area` is null for the flat surface.
119
+ *
120
+ * Per-area numbering restarts at `0001` inside every area, so a bare number
121
+ * cannot address one: five files in this tree are `0001`. A per-area citation
122
+ * must therefore name its area — `ADR-cost-0001`, the form `adr-layout.md`
123
+ * already specifies for cross-surface `supersedes:` values.
124
+ */
125
+ export interface ParsedRef {
126
+ id: string;
127
+ area: string | null;
128
+ num: string;
129
+ }
130
+
131
+ /**
132
+ * `ADR-211` · `adr-211` · `211` · a path → the flat surface.
133
+ * `ADR-cost-0001` · a `docs/adrs/<area>/NNNN-…` path → that area.
134
+ */
135
+ export function normalise_ref(ref: string): ParsedRef | null {
136
+ const raw = ref.trim();
137
+
138
+ // A per-area path carries its area in the directory, and its file never
139
+ // has an `ADR-` prefix — `docs/adrs/cost/0001-hard-stop-hook.md`.
140
+ const byPath = /(?:^|\/)docs\/adrs\/([a-z0-9-]+)\/(\d{1,4})-/i.exec(raw);
141
+ if (byPath?.[1] !== undefined && byPath[2] !== undefined) {
142
+ const num = byPath[2].padStart(4, '0');
143
+ return { id: `ADR-${byPath[1]}-${num}`, area: byPath[1], num };
144
+ }
145
+
146
+ // `ADR-<area>-NNNN` — the citation form for a per-area decision.
147
+ const byArea = /^adr-([a-z][a-z0-9-]*?)-(\d{1,4})$/i.exec(raw.replace(/\.md$/i, ''));
148
+ if (byArea?.[1] !== undefined && byArea[2] !== undefined) {
149
+ const area = byArea[1].toLowerCase();
150
+ const num = byArea[2].padStart(4, '0');
151
+ return { id: `ADR-${area}-${num}`, area, num };
152
+ }
153
+
154
+ const m = /(?:^|[^0-9])(\d{1,4})(?:[^0-9]|$)/.exec(raw.replace(/^.*\//, ''));
155
+ if (m?.[1] === undefined) return null;
156
+ return { id: `ADR-${m[1].padStart(3, '0')}`, area: null, num: String(Number(m[1])) };
157
+ }
158
+
159
+ function walk_md(dir: string): string[] {
160
+ if (!fs.existsSync(dir)) return [];
161
+ const out: string[] = [];
162
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
163
+ const full = path.join(dir, entry.name);
164
+ if (entry.isDirectory()) out.push(...walk_md(full));
165
+ // INDEX.md is generated FROM these files and names every one of them, so
166
+ // leaving it in makes every ADR look referenced by one more decision
167
+ // than it is — noise that hides the single real back-reference.
168
+ else if (entry.name.endsWith('.md') && entry.name !== 'INDEX.md') out.push(full);
169
+ }
170
+ return out.sort();
171
+ }
172
+
173
+ /** Every candidate decision file across the scanned surfaces. */
174
+ export function adr_files(repo_root: string = REPO_ROOT): string[] {
175
+ return ADR_DIRS.flatMap((d) => walk_md(path.join(repo_root, d)));
176
+ }
177
+
178
+ /**
179
+ * Minimal frontmatter reader — folded (`>-`) values are joined, which the
180
+ * corpus needs: every `review_trigger` in it is a folded multi-line string.
181
+ */
182
+ export function parse_frontmatter(text: string): Record<string, string> | null {
183
+ if (!text.startsWith('---\n')) return null;
184
+ const end = text.indexOf('\n---\n', 4);
185
+ if (end === -1) return null;
186
+ const out: Record<string, string> = {};
187
+ let key: string | null = null;
188
+ for (const raw of text.slice(4, end).split('\n')) {
189
+ const line = raw.replace(/\s+$/, '');
190
+ if (!line || line.trimStart().startsWith('#')) continue;
191
+ if (/^\s/.test(line) && key !== null) {
192
+ out[key] = `${out[key]} ${line.trim()}`.trim();
193
+ continue;
194
+ }
195
+ const idx = line.indexOf(':');
196
+ if (idx === -1) continue;
197
+ key = line.slice(0, idx).trim();
198
+ out[key] = line
199
+ .slice(idx + 1)
200
+ .trim()
201
+ .replace(/^["'](.*)["']$/, '$1')
202
+ .replace(/^>-?$/, '');
203
+ }
204
+ return out;
205
+ }
206
+
207
+ /**
208
+ * The three amendment conventions the corpus actually uses. No convention is
209
+ * canonical yet, so all three are matched rather than one being assumed.
210
+ */
211
+ export function amendment_blocks(body: string): string[] {
212
+ const found: string[] = [];
213
+ for (const line of body.split('\n')) {
214
+ if (/^#{2,3}\s+Amendment\b/i.test(line) || /^\*\*Amended\b/i.test(line)) {
215
+ found.push(line.trim().replace(/^#+\s*/, '').replace(/\*\*/g, ''));
216
+ }
217
+ }
218
+ return found;
219
+ }
220
+
221
+ /**
222
+ * A trigger's state, from what is mechanically decidable and nothing else.
223
+ *
224
+ * There is no clause that returns `fired` or `not-fired` from prose, and that
225
+ * absence is the design: every trigger in the corpus is semantic. The states
226
+ * exist in the type because a future structured trigger can carry them; today
227
+ * the honest answers are `none` and `indeterminate`.
228
+ */
229
+ export function trigger_state(fm: Record<string, string>): TriggerState {
230
+ const t = (fm['review_trigger'] ?? '').trim();
231
+ if (t === '') return 'none';
232
+ return 'indeterminate';
233
+ }
234
+
235
+ /** The one-line answer to "may I cite this as a reason not to act?". */
236
+ export function verdict_for(r: Omit<CiteResult, 'verdict'>): string {
237
+ if (!r.resolved) return 'UNRESOLVED — no ADR file carries this number on a scanned surface.';
238
+ const status = (r.status ?? '').toLowerCase();
239
+ // `rejected` is deliberately NOT in this set. On an ADR it means "the
240
+ // proposal was rejected" — the record is a LIVE lock stating that
241
+ // rejection, not a dead decision. Folding it in with `superseded` would
242
+ // make the tool clear a lock that still binds, which is a worse failure
243
+ // than the stall it replaces. Found by running this tool over the eleven
244
+ // ADRs the transcripts show blocking work: ADR-054 came back "not a live
245
+ // lock" and it is one.
246
+ if (status === 'superseded' || status === 'deprecated') {
247
+ const by = r.superseded_by !== undefined && r.superseded_by !== '—' ? ` (by ${r.superseded_by})` : '';
248
+ return `NOT A LIVE LOCK — status is \`${status}\`${by}. Citing it as a blocker is a stale-state claim.`;
249
+ }
250
+ if (status === 'rejected') {
251
+ return (
252
+ 'LIVE — status `rejected` records a REJECTED PROPOSAL, so the rejection is the ' +
253
+ 'decision and it still binds. Reopening it means showing the premise it rejected on ' +
254
+ 'has changed; it does not mean the record is stale.'
255
+ );
256
+ }
257
+ if (r.amendment_blocks.length > 0 || (r.amended_by !== undefined && r.amended_by !== '—')) {
258
+ return 'AMENDED — read the amendment before citing the original decision; the text above it may no longer hold.';
259
+ }
260
+ if (r.trigger_state === 'none') {
261
+ return 'LIVE, NO REOPEN CONDITION — the ADR records no `review_trigger`, so nothing would ever reopen it on its own. Treat that as a defect in the ADR, not as strength of the lock.';
262
+ }
263
+ return 'LIVE, TRIGGER INDETERMINATE — the reopen condition is semantic and this tool cannot decide it. Not an unqualified lock: evaluate the condition against the current tree and route the result.';
264
+ }
265
+
266
+ export function cite_check(refs: string[], repo_root: string = REPO_ROOT): CiteResult[] {
267
+ const files = adr_files(repo_root);
268
+ const contents = new Map<string, string>();
269
+ for (const f of files) contents.set(f, fs.readFileSync(f, 'utf-8'));
270
+
271
+ return refs.map((ref) => {
272
+ const parsed = normalise_ref(ref);
273
+ if (parsed === null) {
274
+ const empty: Omit<CiteResult, 'verdict'> = {
275
+ ref,
276
+ resolved: false,
277
+ trigger_state: 'none',
278
+ amendment_blocks: [],
279
+ referenced_by: [],
280
+ };
281
+ return { ...empty, verdict: verdict_for(empty) };
282
+ }
283
+ const { id, area, num } = parsed;
284
+
285
+ // Two filename conventions, and the flat one's `ADR-` prefix is exactly
286
+ // what a per-area file does NOT have (`docs/adrs/cost/0001-…`). Matching
287
+ // both against one pattern was the defect: the tool advertised
288
+ // `docs/adrs` as covered and could never resolve a citation to it.
289
+ const match =
290
+ area === null
291
+ ? files.find((f) => new RegExp(`(^|/)ADR-0*${num}[-.]`, 'i').test(f))
292
+ : files.find((f) =>
293
+ new RegExp(`/docs/adrs/${area}/0*${Number(num)}-`, 'i').test(f),
294
+ );
295
+ if (match === undefined) {
296
+ const empty: Omit<CiteResult, 'verdict'> = {
297
+ ref: id,
298
+ resolved: false,
299
+ trigger_state: 'none',
300
+ amendment_blocks: [],
301
+ referenced_by: [],
302
+ };
303
+ return { ...empty, verdict: verdict_for(empty) };
304
+ }
305
+
306
+ const text = contents.get(match) ?? '';
307
+ const fm = parse_frontmatter(text) ?? {};
308
+ const bodyStart = text.indexOf('\n---\n', 4);
309
+ const body = bodyStart === -1 ? text : text.slice(bodyStart + 5);
310
+
311
+ // Back-references: any OTHER decision file naming this number. This is
312
+ // the only way to see a one-sided reopen — the amending ADR links back,
313
+ // the amended one does not.
314
+ // A per-area decision is cited as `ADR-<area>-NNNN`; a flat one by number.
315
+ const cited =
316
+ area === null
317
+ ? new RegExp(`ADR-0*${num}\\b`, 'i')
318
+ : new RegExp(`ADR-${area}-0*${Number(num)}\\b`, 'i');
319
+ const referenced_by = files
320
+ .filter((f) => f !== match && cited.test(contents.get(f) ?? ''))
321
+ .map((f) => path.relative(repo_root, f));
322
+
323
+ const partial: Omit<CiteResult, 'verdict'> = {
324
+ ref: id,
325
+ resolved: true,
326
+ file: path.relative(repo_root, match),
327
+ ...(fm['status'] !== undefined ? { status: fm['status'] } : {}),
328
+ ...(fm['date'] !== undefined ? { date: fm['date'] } : {}),
329
+ ...(fm['decision'] !== undefined ? { decision: fm['decision'] } : {}),
330
+ ...(fm['review_trigger'] !== undefined && fm['review_trigger'] !== ''
331
+ ? { review_trigger: fm['review_trigger'] }
332
+ : {}),
333
+ trigger_state: trigger_state(fm),
334
+ ...(fm['supersedes'] !== undefined ? { supersedes: fm['supersedes'] } : {}),
335
+ ...(fm['superseded_by'] !== undefined ? { superseded_by: fm['superseded_by'] } : {}),
336
+ ...(fm['amends'] !== undefined ? { amends: fm['amends'] } : {}),
337
+ ...(fm['amended_by'] !== undefined ? { amended_by: fm['amended_by'] } : {}),
338
+ amendment_blocks: amendment_blocks(body),
339
+ referenced_by,
340
+ };
341
+ return { ...partial, verdict: verdict_for(partial) };
342
+ });
343
+ }
344
+
345
+ function render(results: CiteResult[]): string {
346
+ const lines: string[] = [];
347
+ for (const r of results) {
348
+ lines.push(`\n${r.ref}${r.file !== undefined ? ` · ${r.file}` : ''}`);
349
+ if (!r.resolved) {
350
+ lines.push(` ❌ ${r.verdict}`);
351
+ continue;
352
+ }
353
+ lines.push(` status ${r.status ?? '—'} date ${r.date ?? '—'}`);
354
+ if (r.decision !== undefined) lines.push(` decision ${r.decision}`);
355
+ lines.push(` review_trigger ${r.review_trigger ?? '— (none recorded)'}`);
356
+ lines.push(` trigger state ${r.trigger_state}`);
357
+ if (r.supersedes !== undefined && r.supersedes !== '—') lines.push(` supersedes ${r.supersedes}`);
358
+ if (r.superseded_by !== undefined && r.superseded_by !== '—')
359
+ lines.push(` superseded_by ${r.superseded_by}`);
360
+ if (r.amends !== undefined && r.amends !== '—') lines.push(` amends ${r.amends}`);
361
+ if (r.amended_by !== undefined && r.amended_by !== '—') lines.push(` amended_by ${r.amended_by}`);
362
+ if (r.amendment_blocks.length > 0)
363
+ lines.push(` amendments ${String(r.amendment_blocks.length)}: ${r.amendment_blocks.join(' · ')}`);
364
+ if (r.referenced_by.length > 0)
365
+ lines.push(` referenced by ${String(r.referenced_by.length)} other ADR(s): ${r.referenced_by.join(', ')}`);
366
+ lines.push(` → ${r.verdict}`);
367
+ }
368
+ return lines.join('\n');
369
+ }
370
+
371
+ function main(argv: string[]): number {
372
+ const as_json = argv.includes('--json');
373
+ const refs = argv.filter((a) => !a.startsWith('--'));
374
+ if (refs.length === 0) {
375
+ process.stderr.write(
376
+ 'usage: adr_cite_check <ADR-NNN> [ADR-NNN …] [--json]\n' +
377
+ ' evaluate a decision before citing it as a reason not to act\n',
378
+ );
379
+ return 2;
380
+ }
381
+
382
+ const results = cite_check(refs);
383
+ if (as_json) {
384
+ process.stdout.write(
385
+ JSON.stringify(
386
+ {
387
+ results,
388
+ surfaces_not_scanned: SURFACES_NOT_SCANNED,
389
+ partial_coverage: PARTIAL_COVERAGE,
390
+ },
391
+ null,
392
+ 2,
393
+ ) + '\n',
394
+ );
395
+ } else {
396
+ process.stdout.write(render(results) + '\n');
397
+ }
398
+
399
+ const unresolved = results.filter((r) => !r.resolved);
400
+ if (unresolved.length > 0) {
401
+ process.stderr.write(
402
+ `\n❌ ${String(unresolved.length)} reference(s) did not resolve on a scanned surface. ` +
403
+ `Not scanned: ${SURFACES_NOT_SCANNED.join(' · ')}\n`,
404
+ );
405
+ return 1;
406
+ }
407
+ return 0;
408
+ }
409
+
410
+ // Main-guard (realpath-compared, mirrors the repo convention).
411
+ if (process.argv[1] !== undefined) {
412
+ try {
413
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
414
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
415
+ if (here === argv1) {
416
+ process.exit(main(process.argv.slice(2)));
417
+ }
418
+ } catch {
419
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
420
+ if (import.meta.url === argvUrl) {
421
+ process.exit(main(process.argv.slice(2)));
422
+ }
423
+ }
424
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Sub-help rendering for the council CLI.
3
+ *
4
+ * Lives here rather than in `council_cli.ts` for a measured reason: that file
5
+ * sits ~2,500 lines above the 1,500-line source ceiling, and
6
+ * `check_source_size_budget` is a shrink-only ratchet — every line added there
7
+ * is a new violation, so a 45-line help renderer reds the build. Extraction is
8
+ * the documented response; raising the baseline is not.
9
+ *
10
+ * WHAT IT FIXES. `council run --help` printed `usage: agent-config council run
11
+ * [-h] ...` and no flag list at all. Measured 2026-08-20: a first council run
12
+ * cost six calls, two of them spent discovering `--confirm` and `--output` by
13
+ * grepping the CLI source. The information was available at runtime all along —
14
+ * the parser's own spec table carries it — it simply was not printed.
15
+ *
16
+ * Rendered from those specs rather than hand-listed on purpose: a hand-written
17
+ * flag list is a second source of truth that drifts the first time a flag is
18
+ * added.
19
+ */
20
+
21
+ /** The shape `council_cli`'s spec table exposes, narrowed to what help needs. */
22
+ export interface HelpSpec {
23
+ readonly positionals: readonly string[];
24
+ readonly opts: readonly { readonly flag: string; readonly takesValue: boolean; readonly choices?: readonly string[] | null }[];
25
+ readonly requiredOpts: readonly string[];
26
+ }
27
+
28
+ /**
29
+ * `usageLine` first (its exact text is asserted elsewhere and must not move),
30
+ * then positionals, then every flag with its value shape and a `[required]`
31
+ * marker where the parser demands one.
32
+ */
33
+ export function renderSubHelp(_cmd: string, usageLine: string, spec: HelpSpec): string {
34
+ const lines: string[] = [usageLine.trimEnd()];
35
+ if (spec.positionals.length > 0) {
36
+ lines.push('', 'positional arguments:');
37
+ for (const p of spec.positionals) {
38
+ lines.push(` ${p}`);
39
+ }
40
+ }
41
+ if (spec.opts.length > 0) {
42
+ lines.push('', 'options:');
43
+ const required = new Set(spec.requiredOpts);
44
+ for (const o of spec.opts) {
45
+ const value = o.takesValue
46
+ ? o.choices && o.choices.length > 0
47
+ ? ` {${o.choices.join(',')}}`
48
+ : ' VALUE'
49
+ : '';
50
+ lines.push(` ${o.flag}${value}${required.has(o.flag) ? ' [required]' : ''}`);
51
+ }
52
+ }
53
+ lines.push('');
54
+ return lines.join('\n');
55
+ }