@event4u/agent-config 16.0.0 → 16.1.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 (246) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +297 -137
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +74 -27
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
  25. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
  27. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  28. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  29. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  30. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  31. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  32. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  33. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  34. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  35. package/dist/agent-src/rules/delegation-policy.md +23 -45
  36. package/dist/agent-src/rules/design-fidelity.md +32 -37
  37. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  38. package/dist/agent-src/rules/test-first.md +41 -0
  39. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  40. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  41. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  42. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  43. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  44. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  45. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  46. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  47. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  48. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  49. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  50. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  52. package/dist/agent-src/templates/agent-settings.md +1 -1
  53. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  54. package/dist/agent-src/templates/roadmaps.md +188 -0
  55. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  56. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  58. package/dist/cli-delegate/check_update_banner.js +40 -2
  59. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  60. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  61. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  62. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  63. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  64. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  65. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  66. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  67. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  68. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  69. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  70. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  71. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  72. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  73. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  74. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  75. package/dist/cli-delegate/cmd_conformance.js +90 -56
  76. package/dist/cli-delegate/cmd_doctor.js +7 -5
  77. package/dist/cli-delegate/cmd_explain.js +2 -2
  78. package/dist/cli-delegate/cmd_export.js +9 -7
  79. package/dist/cli-delegate/cmd_fleet.js +4 -2
  80. package/dist/cli-delegate/cmd_handoff.js +4 -4
  81. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  82. package/dist/cli-delegate/cmd_migrate.js +2 -2
  83. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  84. package/dist/cli-delegate/cmd_preflight.js +4 -2
  85. package/dist/cli-delegate/cmd_prune.js +10 -8
  86. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  87. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  88. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  89. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  90. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  91. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  92. package/dist/cli-delegate/cmd_sync.js +10 -8
  93. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  94. package/dist/cli-delegate/cmd_update.js +1 -1
  95. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  96. package/dist/cli-delegate/cmd_validate.js +12 -10
  97. package/dist/cli-delegate/cmd_versions.js +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +85 -38
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +4 -4
  102. package/dist/discovery/dormancy-report.md +1 -1
  103. package/dist/discovery/orphan-report.md +1 -1
  104. package/dist/discovery/packs.json +10 -8
  105. package/dist/discovery/trust-report.md +3 -3
  106. package/dist/discovery/workspaces.json +8 -6
  107. package/dist/hooks/dispatch.js +1832 -1007
  108. package/dist/install/claudeRuleRewrite.js +14 -3
  109. package/dist/install/conflict.js +60 -10
  110. package/dist/install/conflictTracker.js +218 -0
  111. package/dist/install/fsPrimitives.js +54 -0
  112. package/dist/install/install.mjs +2007 -1481
  113. package/dist/install/manifestFiles.js +259 -0
  114. package/dist/install/preflight.js +14 -2
  115. package/dist/install/preserve.js +157 -0
  116. package/dist/install/recordedOwnership.js +132 -0
  117. package/dist/install/txlog.js +11 -2
  118. package/dist/mcp/registry-manifest.json +3 -3
  119. package/dist/mcp/server.json +2 -2
  120. package/dist/mcp/server.mjs +398 -39
  121. package/dist/router.json +1 -1
  122. package/dist/scripts/_lib/agent_settings.js +3 -2
  123. package/dist/scripts/_lib/fs_atomic.js +167 -0
  124. package/dist/scripts/_lib/installed_tools.js +460 -0
  125. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  126. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  127. package/dist/server/routes/install.js +8 -0
  128. package/dist/server/routes/wizard.js +9 -3
  129. package/dist/server/schemas/settings.js +17 -5
  130. package/dist/shared/missionExecution.js +97 -0
  131. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  132. package/dist/ui/index.html +1 -1
  133. package/docs/MIGRATION.md +103 -0
  134. package/docs/contracts/settings-classes.md +26 -7
  135. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  136. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  137. package/docs/guidelines/design-fidelity-routing.md +29 -0
  138. package/package.json +1 -1
  139. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  140. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  141. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  142. package/src/config/agent-settings.template.yml +139 -27
  143. package/src/config/auto-rule-budget.json +16 -0
  144. package/src/config/ci-local-parity.yml +18 -5
  145. package/src/config/continuity-surface.json +21 -0
  146. package/src/config/gate-coverage.yml +146 -13
  147. package/src/config/gate-violation-baselines.json +6 -6
  148. package/src/config/hook-token-budget.json +2 -0
  149. package/src/config/loop-surfaces.yaml +215 -0
  150. package/src/config/platform-anchor.json +3 -2
  151. package/src/config/release-gate-locality.yml +11 -0
  152. package/src/config/routing-coverage-seed.json +18 -4
  153. package/src/config/rule-enforcement-baseline.json +0 -1
  154. package/src/install/claudeRuleRewrite.ts +16 -2
  155. package/src/install/conflict.ts +65 -9
  156. package/src/install/conflictTracker.ts +299 -0
  157. package/src/install/fsPrimitives.ts +54 -0
  158. package/src/install/manifestFiles.ts +312 -0
  159. package/src/install/preflight.ts +16 -2
  160. package/src/install/preserve.ts +205 -0
  161. package/src/install/recordedOwnership.ts +149 -0
  162. package/src/install/txlog.ts +11 -2
  163. package/src/install/types.ts +19 -4
  164. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  165. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  166. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  167. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  168. package/src/scripts/_cli/doctor_execution.ts +90 -0
  169. package/src/scripts/_lib/activation_payload.ts +247 -0
  170. package/src/scripts/_lib/agent_settings.ts +4 -2
  171. package/src/scripts/_lib/authority_path.ts +102 -0
  172. package/src/scripts/_lib/cascade_base.ts +94 -0
  173. package/src/scripts/_lib/changelog_eras.ts +7 -2
  174. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  175. package/src/scripts/_lib/council_transport.ts +129 -0
  176. package/src/scripts/_lib/delivery_ready.ts +112 -0
  177. package/src/scripts/_lib/forge_protection.ts +267 -0
  178. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  179. package/src/scripts/_lib/json_python_parity.ts +127 -0
  180. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  181. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  182. package/src/scripts/_lib/mission_record.ts +190 -0
  183. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  184. package/src/scripts/_lib/obligations.ts +402 -0
  185. package/src/scripts/_lib/orchestration_record.ts +11 -20
  186. package/src/scripts/_lib/release_highlights.ts +199 -10
  187. package/src/scripts/_lib/release_holds.ts +493 -0
  188. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  189. package/src/scripts/_lib/rides_along.ts +102 -0
  190. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  191. package/src/scripts/_lib/test_delta.ts +166 -0
  192. package/src/scripts/_lib/test_provenance.ts +171 -0
  193. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  194. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  195. package/src/scripts/ai_council/config.ts +8 -11
  196. package/src/scripts/ai_council/fallback_config.ts +5 -3
  197. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  198. package/src/scripts/ask_block_census.ts +285 -15
  199. package/src/scripts/check_enforcement_matrix.ts +624 -0
  200. package/src/scripts/check_gate_reachability.ts +173 -10
  201. package/src/scripts/check_generator_sync.ts +954 -0
  202. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  203. package/src/scripts/check_release_highlights.ts +33 -1
  204. package/src/scripts/check_release_holds.ts +567 -0
  205. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  206. package/src/scripts/check_test_delta.ts +294 -0
  207. package/src/scripts/check_test_weakening.ts +299 -0
  208. package/src/scripts/closure_scan.ts +333 -0
  209. package/src/scripts/compile_hook_manifest.ts +43 -3
  210. package/src/scripts/council_record_shape.ts +209 -0
  211. package/src/scripts/hook_manifest.json +1 -1
  212. package/src/scripts/hook_manifest.yaml +88 -9
  213. package/src/scripts/hooks/concern_registry.ts +4 -0
  214. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  215. package/src/scripts/hooks/evidence_independence.ts +96 -0
  216. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  217. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  218. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  219. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  220. package/src/scripts/hooks_doctor.ts +85 -0
  221. package/src/scripts/hooks_status.ts +19 -0
  222. package/src/scripts/install.ts +106 -206
  223. package/src/scripts/janitor.ts +7 -0
  224. package/src/scripts/lint_decision_classes.ts +464 -0
  225. package/src/scripts/lint_hook_manifest.ts +11 -0
  226. package/src/scripts/lint_major_migration_sections.ts +450 -0
  227. package/src/scripts/lint_plan_risk_register.ts +47 -3
  228. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  229. package/src/scripts/lint_roadmap_producers.ts +202 -0
  230. package/src/scripts/measure_rule_budget.ts +101 -5
  231. package/src/scripts/new_roadmap.ts +37 -0
  232. package/src/scripts/orchestration_record.ts +0 -5
  233. package/src/scripts/release.ts +14 -98
  234. package/src/scripts/release_drill.ts +267 -1
  235. package/src/scripts/release_publication.ts +51 -0
  236. package/src/scripts/report_enforcement_drift.ts +566 -0
  237. package/src/scripts/report_host_injection_effect.ts +3 -11
  238. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  239. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  240. package/src/scripts/schemas/command.schema.json +4 -0
  241. package/src/scripts/scope_growth.ts +219 -0
  242. package/src/scripts/trigger_eval_grandfather.json +0 -1
  243. package/src/scripts/ui_conformance_probe.ts +622 -0
  244. package/src/server/schemas/settings.ts +31 -7
  245. package/src/shared/missionExecution.ts +126 -0
  246. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * The closure detector — finds the open decisions in a plan, and classifies
4
+ * each one by OWNERSHIP. It never resolves one.
5
+ *
6
+ * That boundary is the whole design. A decision dispatcher was killed on
7
+ * purpose: no TypeScript path routes a decision, and this script adds none.
8
+ * What it adds is the half that can be mechanical — *which questions are open,
9
+ * and whose they are* — so the claim "this plan reached execution with twelve
10
+ * open technical decisions and asked the owner none of them" is falsifiable
11
+ * rather than a feeling.
12
+ *
13
+ * Detector families, each a phrase an author writes when they KNOW something
14
+ * is open. Deliberately narrow: a detector that fires on ordinary plan prose
15
+ * turns every producer red at once, and the cheapest repair is then to weaken
16
+ * it until it finds nothing.
17
+ *
18
+ * `tbd` TBD / to be decided / decide later
19
+ * `unpicked-alternative` "either X or Y", "option A … option B"
20
+ * `unchecked-assumption` "assuming", "presumably", "probably fine"
21
+ * `missing-verify` an open step with no `verify:` line
22
+ * `ambiguous-acceptance` a vague acceptance criterion (improve / clean up
23
+ * / best practices / handle errors properly)
24
+ * `contradictory` "contradicts", "conflicts with", "but also"
25
+ * `product-semantics` two valid user-visible outcomes
26
+ * `typed-op` an operation from the typed vocabulary
27
+ * `spend` a ceiling or quota question
28
+ *
29
+ * Ownership classification is the output that matters. Six families classify
30
+ * to a technical class the agent, an independent session, a council or a team
31
+ * resolves; exactly two reach the owner (`product-semantics` → `product-owned`,
32
+ * `typed-op` → `destructive-owned`); `spend` reaches nobody — it pauses and
33
+ * reports (ADR-268 § 8).
34
+ *
35
+ * Exit codes: 0 no open decisions · 1 open decisions found · 2 bad argv.
36
+ * Finding open decisions is the POINT, so `--strict` is what makes exit 1 a
37
+ * failure for a caller that wants one; the default is a report.
38
+ */
39
+
40
+ import * as fs from 'node:fs';
41
+ import * as path from 'node:path';
42
+ import process from 'node:process';
43
+ import { fileURLToPath } from 'node:url';
44
+
45
+ const _HERE = fileURLToPath(import.meta.url);
46
+ const _PROG = 'closure_scan';
47
+
48
+ export type Kind =
49
+ | 'tbd'
50
+ | 'unpicked-alternative'
51
+ | 'unchecked-assumption'
52
+ | 'missing-verify'
53
+ | 'ambiguous-acceptance'
54
+ | 'contradictory'
55
+ | 'product-semantics'
56
+ | 'typed-op'
57
+ | 'spend';
58
+
59
+ export interface Finding {
60
+ readonly id: string;
61
+ readonly line: number;
62
+ readonly kind: Kind;
63
+ readonly ownership: string;
64
+ readonly excerpt: string;
65
+ }
66
+
67
+ /**
68
+ * Kind → ownership class. The two owner-routed rows are the whole cost of
69
+ * closure: everything else is resolved without reaching a person, which is
70
+ * what "zero owner questions for twelve technical ambiguities" means.
71
+ */
72
+ export const OWNERSHIP_BY_KIND: Readonly<Record<Kind, string>> = {
73
+ tbd: 'contested-technical',
74
+ 'unpicked-alternative': 'contested-technical',
75
+ 'unchecked-assumption': 'reversible-technical',
76
+ 'missing-verify': 'deterministic',
77
+ 'ambiguous-acceptance': 'reversible-technical',
78
+ contradictory: 'contested-technical',
79
+ 'product-semantics': 'product-owned',
80
+ 'typed-op': 'destructive-owned',
81
+ spend: 'spend-exhaustion',
82
+ };
83
+
84
+ /** Classes that reach a person. Everything else closes without an ask. */
85
+ export const OWNER_ROUTED: ReadonlySet<string> = new Set([
86
+ 'product-owned',
87
+ 'business-owned',
88
+ 'destructive-owned',
89
+ ]);
90
+
91
+ interface Pattern {
92
+ readonly kind: Kind;
93
+ readonly re: RegExp;
94
+ }
95
+
96
+ const LINE_PATTERNS: readonly Pattern[] = [
97
+ { kind: 'tbd', re: /\bTBD\b/ },
98
+ { kind: 'tbd', re: /\bto be decided\b/i },
99
+ { kind: 'tbd', re: /\bdecide later\b/i },
100
+ { kind: 'unpicked-alternative', re: /\beither\b[^.]{3,80}\bor\b/i },
101
+ { kind: 'unpicked-alternative', re: /\boption A\b[^.]{0,120}\boption B\b/i },
102
+ { kind: 'unpicked-alternative', re: /\bwe could (?:either )?\w+[^.]{0,80}\bor\b/i },
103
+ // "two equal technical strategies" is the canonical unpicked alternative,
104
+ // and the phrasing an author reaches for when the choice is genuinely open
105
+ // — it carries no `either`, no `or` and no `option A`.
106
+ { kind: 'unpicked-alternative', re: /\btwo (?:equal|competing)\b[^.]{0,60}\b(?:strateg|approach|design|option)/i },
107
+ { kind: 'unchecked-assumption', re: /\bassuming\b/i },
108
+ { kind: 'unchecked-assumption', re: /\bpresumably\b/i },
109
+ { kind: 'unchecked-assumption', re: /\bprobably (?:fine|works|ok)\b/i },
110
+ { kind: 'ambiguous-acceptance', re: /\b(?:improve|optimi[sz]e)\b(?![^.]*\bby\b)/i },
111
+ { kind: 'ambiguous-acceptance', re: /\bmake it (?:better|cleaner|nicer)\b/i },
112
+ { kind: 'ambiguous-acceptance', re: /\buse best practices\b/i },
113
+ { kind: 'ambiguous-acceptance', re: /\bhandle errors properly\b/i },
114
+ { kind: 'contradictory', re: /\bcontradicts\b/i },
115
+ { kind: 'contradictory', re: /\bconflicts with\b/i },
116
+ { kind: 'product-semantics', re: /\buser-visible\b[^.]{0,80}\b(?:or|versus|vs\.?)\b/i },
117
+ { kind: 'product-semantics', re: /\bwhat the user sees\b/i },
118
+ { kind: 'product-semantics', re: /\btwo valid (?:user|product) semantics\b/i },
119
+ { kind: 'typed-op', re: /\b(?:merge|force-push|deploy|publish|purchase|rotate the secret)\b/i },
120
+ { kind: 'spend', re: /\b(?:over|above|crosses?) the (?:configured )?(?:spend )?ceiling\b/i },
121
+ { kind: 'spend', re: /\bquota (?:is )?(?:exhausted|gone)\b/i },
122
+ ];
123
+
124
+ const STEP_RE = /^\s*-\s\[[ x~-]\]\s/;
125
+ const OPEN_STEP_RE = /^\s*-\s\[ \]\s/;
126
+ const H2_RE = /^##\s+/;
127
+ const DISCHARGE_H2_RE = /^##\s+(Decisions|Blockers|Kill register|Risk Register)\s*$/;
128
+ const ACCEPTANCE_H2_RE = /^##\s+Acceptance Criteria\s*$/;
129
+
130
+ function _isFence(line: string): boolean {
131
+ return /^\s*(```|~~~)/.test(line);
132
+ }
133
+
134
+ /**
135
+ * A scan unit: one logical statement, which is NOT one physical line.
136
+ *
137
+ * A roadmap step wraps over several lines, and the ambiguity it carries
138
+ * routinely straddles the wrap ("either newline-delimited JSON / or a single
139
+ * array"). Matching per line missed exactly those, which are the ones an
140
+ * author is least likely to notice. A step block is therefore ONE unit, joined
141
+ * and matched once — which also means one finding per step at most, so a step
142
+ * carrying two hedges is one open decision rather than two.
143
+ */
144
+ export interface Unit {
145
+ /** 1-based line of the unit's first line. */
146
+ readonly line: number;
147
+ readonly text: string;
148
+ readonly inAcceptance: boolean;
149
+ readonly isOpenStep: boolean;
150
+ readonly hasVerify: boolean;
151
+ }
152
+
153
+ export function units(lines: readonly string[]): Unit[] {
154
+ const out: Unit[] = [];
155
+ let inFence = false;
156
+ let inDischarge = false;
157
+ let inAcceptance = false;
158
+ let i = 0;
159
+ while (i < lines.length) {
160
+ const line = lines[i] as string;
161
+ if (_isFence(line)) {
162
+ inFence = !inFence;
163
+ i += 1;
164
+ continue;
165
+ }
166
+ if (inFence) {
167
+ i += 1;
168
+ continue;
169
+ }
170
+ if (H2_RE.test(line)) {
171
+ inDischarge = DISCHARGE_H2_RE.test(line);
172
+ inAcceptance = ACCEPTANCE_H2_RE.test(line);
173
+ }
174
+ if (inDischarge) {
175
+ i += 1;
176
+ continue;
177
+ }
178
+ if (STEP_RE.test(line)) {
179
+ let j = i + 1;
180
+ while (j < lines.length && !STEP_RE.test(lines[j] as string) && !H2_RE.test(lines[j] as string)) {
181
+ j += 1;
182
+ }
183
+ const block = lines.slice(i, j);
184
+ out.push({
185
+ line: i + 1,
186
+ // Whitespace-normalised: a wrapped step joined with a single
187
+ // space still carries the next line's indent, so a pattern
188
+ // written with single spaces stops matching across the wrap —
189
+ // which is the case this unit exists to catch.
190
+ text: block.join(' ').replace(/\s+/g, ' '),
191
+ inAcceptance,
192
+ isOpenStep: OPEN_STEP_RE.test(line),
193
+ hasVerify: /(?:^|\s)verify:/m.test(block.join('\n')),
194
+ });
195
+ i = j;
196
+ continue;
197
+ }
198
+ out.push({
199
+ line: i + 1,
200
+ text: line,
201
+ inAcceptance,
202
+ isOpenStep: false,
203
+ hasVerify: false,
204
+ });
205
+ i += 1;
206
+ }
207
+ return out;
208
+ }
209
+
210
+ export function scan(text: string): Finding[] {
211
+ const lines = text.split('\n');
212
+ const findings: Finding[] = [];
213
+
214
+ for (const u of units(lines)) {
215
+ if (/<!--\s*closure:\s*ignore\s*-->/.test(u.text)) continue;
216
+ let matched: Kind | null = null;
217
+ for (const { kind, re } of LINE_PATTERNS) {
218
+ if (re.test(u.text)) {
219
+ matched = kind;
220
+ break;
221
+ }
222
+ }
223
+ // An open step carries a `verify:` line — a step whose success cannot
224
+ // be checked is a trigger for a question, not for work. Acceptance
225
+ // criteria are criteria, not steps: they have their own contract and
226
+ // carry no verify line by design.
227
+ if (matched === null && u.isOpenStep && !u.hasVerify && !u.inAcceptance) {
228
+ matched = 'missing-verify';
229
+ }
230
+ if (matched === null) continue;
231
+ findings.push({
232
+ id: '',
233
+ line: u.line,
234
+ kind: matched,
235
+ ownership: OWNERSHIP_BY_KIND[matched],
236
+ excerpt: u.text.trim().replace(/\s+/g, ' ').slice(0, 120),
237
+ });
238
+ }
239
+
240
+ findings.sort((a, b) => a.line - b.line);
241
+ return findings.map((f, idx) => ({ ...f, id: `C${String(idx + 1)}` }));
242
+ }
243
+
244
+ /** The `## Decisions` skeleton — one row per finding, resolution left open. */
245
+ export function renderRows(findings: readonly Finding[]): string {
246
+ const header = '| ID | ownership | resolved by | decision | evidence | revisit if |';
247
+ const sep = '|---|---|---|---|---|---|';
248
+ const rows = findings.map(
249
+ (f) => `| ${f.id} | ${f.ownership} | <resolver> | <decision> | ${f.kind}:${String(f.line)} | <condition> |`,
250
+ );
251
+ return [header, sep, ...rows].join('\n');
252
+ }
253
+
254
+ export function ownerQuestionCount(findings: readonly Finding[]): number {
255
+ return findings.filter((f) => OWNER_ROUTED.has(f.ownership)).length;
256
+ }
257
+
258
+ // --------------------------------------------------------------------------
259
+ // CLI.
260
+ // --------------------------------------------------------------------------
261
+
262
+ function _usage(): string {
263
+ return (
264
+ `usage: ${_PROG} [-h] [--json] [--rows] [--strict] <roadmap.md>\n\n` +
265
+ `Find and classify the open decisions in a plan. Resolves nothing.\n`
266
+ );
267
+ }
268
+
269
+ export function main(argv?: readonly string[]): number {
270
+ const args = argv ?? process.argv.slice(2);
271
+ let json = false;
272
+ let rows = false;
273
+ let strict = false;
274
+ const paths: string[] = [];
275
+ for (const a of args) {
276
+ if (a === '-h' || a === '--help') {
277
+ process.stdout.write(_usage());
278
+ return 0;
279
+ } else if (a === '--json') json = true;
280
+ else if (a === '--rows') rows = true;
281
+ else if (a === '--strict') strict = true;
282
+ else if (a.startsWith('-')) {
283
+ process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
284
+ return 2;
285
+ } else paths.push(a);
286
+ }
287
+ if (paths.length !== 1) {
288
+ process.stderr.write(`${_usage()}${_PROG}: error: exactly one roadmap path is required\n`);
289
+ return 2;
290
+ }
291
+ const target = path.resolve(paths[0] as string);
292
+ let text: string;
293
+ try {
294
+ text = fs.readFileSync(target, 'utf-8');
295
+ } catch {
296
+ process.stderr.write(`${_PROG}: error: cannot read ${paths[0] as string}\n`);
297
+ return 2;
298
+ }
299
+
300
+ const findings = scan(text);
301
+ const owner = ownerQuestionCount(findings);
302
+
303
+ if (json) {
304
+ process.stdout.write(
305
+ JSON.stringify({ findings, open: findings.length, owner_questions: owner }, null, 2) + '\n',
306
+ );
307
+ } else if (rows) {
308
+ process.stdout.write(renderRows(findings) + '\n');
309
+ } else {
310
+ process.stdout.write(`open decisions: ${String(findings.length)}\n`);
311
+ process.stdout.write(`owner questions: ${String(owner)}\n`);
312
+ for (const f of findings) {
313
+ process.stdout.write(
314
+ ` ${f.id} ${String(f.line).padStart(4)} ${f.kind.padEnd(20)} ${f.ownership.padEnd(22)} ${f.excerpt}\n`,
315
+ );
316
+ }
317
+ }
318
+ if (!findings.length) return 0;
319
+ return strict ? 1 : 0;
320
+ }
321
+
322
+ function _isCliEntry(): boolean {
323
+ if (!process.argv[1]) return false;
324
+ try {
325
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ if (_isCliEntry() || process.argv[1] === _HERE) {
332
+ process.exitCode = main();
333
+ }
@@ -23,10 +23,21 @@
23
23
  * Run after editing the manifest:
24
24
  * ./scripts-run src/scripts/compile_hook_manifest
25
25
  *
26
+ * `--out <path>` writes the compiled bytes THERE and leaves the tree alone. It
27
+ * exists for `check_generator_sync`, whose contract is that a triple regenerates
28
+ * into a temp dir the gate owns and never into the working tree — a gate that
29
+ * repaired the artefact it is measuring would turn every staleness into a silent
30
+ * green plus an unexplained dirty file. Anything that wants to compare against
31
+ * the committed JSON uses this flag; anything that wants to FIX it omits it.
32
+ *
26
33
  * `tests/hooks/hook_manifest_compiled.test.ts` fails if the committed JSON
27
- * stops matching the YAML, so a forgotten run is caught rather than shipped.
34
+ * stops matching the YAML, so a forgotten run is caught rather than shipped —
35
+ * but only against the COMMITTED blob in CI. Locally that same test reads the
36
+ * working tree, so a recompile that was run and never committed makes it pass
37
+ * over a defect that is still on the branch. See the discriminator in
38
+ * `check_generator_sync`'s header.
28
39
  *
29
- * Exit codes: 0 written (or already current) · 2 internal error.
40
+ * Exit codes: 0 written (or already current) · 2 internal error or usage.
30
41
  */
31
42
  import * as fs from 'node:fs';
32
43
  import * as path from 'node:path';
@@ -46,9 +57,38 @@ export function compile(yamlText: string): string {
46
57
  });
47
58
  }
48
59
 
49
- export function main(): number {
60
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
61
+ let outPath: string | null = null;
62
+ for (let i = 0; i < argv.length; i += 1) {
63
+ const a = argv[i] as string;
64
+ if (a === '--out') {
65
+ const next = argv[i + 1];
66
+ if (next === undefined || next.startsWith('--')) {
67
+ process.stderr.write('compile_hook_manifest: --out needs a path\n');
68
+ return 2;
69
+ }
70
+ outPath = next;
71
+ i += 1;
72
+ continue;
73
+ }
74
+ process.stderr.write(
75
+ `compile_hook_manifest: unknown argument ${JSON.stringify(a)}\n` +
76
+ 'usage: compile_hook_manifest [--out <path>]\n',
77
+ );
78
+ return 2;
79
+ }
80
+
50
81
  const text = fs.readFileSync(YAML_PATH, 'utf-8');
51
82
  const next = compile(text);
83
+
84
+ if (outPath !== null) {
85
+ // Comparison copy. No `already current` short-circuit: the caller asked
86
+ // for the bytes, and an unwritten file would read as a generator that
87
+ // silently did nothing.
88
+ fs.writeFileSync(outPath, next);
89
+ return 0;
90
+ }
91
+
52
92
  const current = fs.existsSync(JSON_PATH) ? fs.readFileSync(JSON_PATH, 'utf-8') : null;
53
93
  if (current === next) {
54
94
  process.stdout.write('compile_hook_manifest: already current\n');
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * When a council record owes the owner an options block — and when it must not.
4
+ *
5
+ * The output contract used to end every council run with a numbered-options
6
+ * block carrying the host verdict, unconditionally. Under an ownership axis
7
+ * that is wrong in one specific case and right in every other: a **conclusive
8
+ * technical** verdict is a resolution, and handing it to the owner as options
9
+ * routes a technical decision to a person because it was hard — the move
10
+ * ADR-268 § 10 forbids by name.
11
+ *
12
+ * So the block becomes conditional, on exactly two readable fields:
13
+ *
14
+ * owed the verdict is owner-owned (`product-owned`, `business-owned`,
15
+ * `destructive-owned`), OR the council did not converge. Both
16
+ * are cases where nobody but the owner can close it, and the
17
+ * block carries the council's PROPOSAL for them to confirm.
18
+ * forbidden the verdict is technical AND conclusive. The record states
19
+ * the resolution; the run continues.
20
+ *
21
+ * Convergence is read, not inferred: a record that does not say whether its
22
+ * members converged is itself the finding, because "conclusive" would then be
23
+ * the author's word for it rather than a property of the run.
24
+ *
25
+ * This is an evaluator over a record, not a gate over the tree — the tree's
26
+ * council records are gitignored and pruned (they are dev-time scratch; the
27
+ * durable record is the convergence inlined into the artifact). It exists so
28
+ * the contract is checkable against a fixture rather than asserted in prose.
29
+ *
30
+ * Exit codes: 0 conforming · 1 findings · 2 bad argv.
31
+ */
32
+
33
+ import * as fs from 'node:fs';
34
+ import * as path from 'node:path';
35
+ import process from 'node:process';
36
+ import { fileURLToPath } from 'node:url';
37
+
38
+ const _HERE = fileURLToPath(import.meta.url);
39
+ const _PROG = 'council_record_shape';
40
+
41
+ /** Owner-owned classes — the only ones a council may not close for the owner. */
42
+ export const OWNER_OWNED: ReadonlySet<string> = new Set([
43
+ 'product-owned',
44
+ 'business-owned',
45
+ 'destructive-owned',
46
+ ]);
47
+
48
+ // Emphasis is stripped before these run (see `_plain`): the label is written
49
+ // `Ownership:`, `**Ownership:**` and `- **Ownership**:` across the corpus, and
50
+ // a pattern that tries to spell all three is how one spelling ends up silently
51
+ // unreadable.
52
+ const OWNERSHIP_RE = /^\s*[-*]?\s*Ownership\s*:\s*`?([a-z-]+)`?/im;
53
+ const CONVERGENCE_RE = /^\s*[-*]?\s*Convergence\s*:\s*(.+)$/im;
54
+ const OPTIONS_HEADING_RE = /^#{2,4}\s+(?:Options|Owner options|User options)\b/im;
55
+ const NUMBERED_OPTION_RE = /^\s*1\.\s+\S/m;
56
+
57
+ export interface RecordShape {
58
+ readonly ownership: string | null;
59
+ readonly convergence: string | null;
60
+ readonly convergent: boolean;
61
+ readonly hasOptionsBlock: boolean;
62
+ }
63
+
64
+ /**
65
+ * A convergence line is conclusive when it says so in the vocabulary the
66
+ * council already uses: `2/2`, `unanimous`, `convergent`. `split`, `1/2` and
67
+ * `no quorum` are the other side, and they win on conflict — a line reading
68
+ * "2/2 but split" is not a resolution.
69
+ */
70
+ export function isConvergent(line: string): boolean {
71
+ if (/\b(?:split|divergent|no quorum|abstain|inconclusive)\b/i.test(line)) return false;
72
+ if (/\b(?:unanimous|convergent|conclusive)\b/i.test(line)) return true;
73
+ const m = /(\d+)\s*\/\s*(\d+)/.exec(line);
74
+ if (m) return m[1] === m[2];
75
+ return false;
76
+ }
77
+
78
+ /** Markdown emphasis removed, line structure kept. */
79
+ export function _plain(text: string): string {
80
+ return text.replace(/\*+/g, '');
81
+ }
82
+
83
+ export function parseRecord(text: string): RecordShape {
84
+ const plain = _plain(text);
85
+ const o = OWNERSHIP_RE.exec(plain);
86
+ const c = CONVERGENCE_RE.exec(plain);
87
+ const convergence = c ? (c[1] as string).trim() : null;
88
+ const headingIdx = OPTIONS_HEADING_RE.exec(plain);
89
+ return {
90
+ ownership: o ? (o[1] as string) : null,
91
+ convergence,
92
+ convergent: convergence === null ? false : isConvergent(convergence),
93
+ hasOptionsBlock: headingIdx !== null && NUMBERED_OPTION_RE.test(plain.slice(headingIdx.index)),
94
+ };
95
+ }
96
+
97
+ /** True when the record must carry an owner-facing options block. */
98
+ export function owesOwnerOptions(shape: RecordShape): boolean {
99
+ if (shape.ownership !== null && OWNER_OWNED.has(shape.ownership)) return true;
100
+ return !shape.convergent;
101
+ }
102
+
103
+ export function checkRecord(rel: string, text: string): string[] {
104
+ const out: string[] = [];
105
+ const shape = parseRecord(text);
106
+ if (shape.ownership === null) {
107
+ out.push(`${rel}: no \`Ownership:\` line — the routing cannot be read`);
108
+ }
109
+ if (shape.convergence === null) {
110
+ out.push(
111
+ `${rel}: no \`Convergence:\` line — "conclusive" would be the author's word for it ` +
112
+ `rather than a property of the run`,
113
+ );
114
+ return out;
115
+ }
116
+ const owed = owesOwnerOptions(shape);
117
+ if (owed && !shape.hasOptionsBlock) {
118
+ out.push(
119
+ `${rel}: owner-owned or non-convergent, and carries no owner-facing options block — ` +
120
+ `the owner still has to confirm the council's proposal`,
121
+ );
122
+ }
123
+ if (!owed && shape.hasOptionsBlock) {
124
+ out.push(
125
+ `${rel}: a conclusive technical verdict carries an owner-facing options block — ` +
126
+ `that routes a technical decision to the owner because it was hard (ADR-268 § 10)`,
127
+ );
128
+ }
129
+ return out;
130
+ }
131
+
132
+ function _usage(): string {
133
+ return `usage: ${_PROG} [-h] [--self-test] <record.md>...\n`;
134
+ }
135
+
136
+ export function main(argv?: readonly string[]): number {
137
+ const args = argv ?? process.argv.slice(2);
138
+ const paths: string[] = [];
139
+ for (const a of args) {
140
+ if (a === '-h' || a === '--help') {
141
+ process.stdout.write(_usage());
142
+ return 0;
143
+ }
144
+ if (a === '--self-test') return _selfTest();
145
+ if (a.startsWith('-')) {
146
+ process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
147
+ return 2;
148
+ }
149
+ paths.push(a);
150
+ }
151
+ if (!paths.length) {
152
+ process.stderr.write(`${_usage()}${_PROG}: error: at least one record path is required\n`);
153
+ return 2;
154
+ }
155
+ const findings: string[] = [];
156
+ for (const p of paths) {
157
+ let text: string;
158
+ try {
159
+ text = fs.readFileSync(path.resolve(p), 'utf-8');
160
+ } catch {
161
+ process.stderr.write(`${_PROG}: error: cannot read ${p}\n`);
162
+ return 2;
163
+ }
164
+ findings.push(...checkRecord(p, text));
165
+ }
166
+ if (findings.length) {
167
+ for (const f of findings) process.stderr.write(`❌ ${f}\n`);
168
+ return 1;
169
+ }
170
+ process.stdout.write(`✅ ${_PROG}: ${String(paths.length)} record(s) conform\n`);
171
+ return 0;
172
+ }
173
+
174
+ function _selfTest(): number {
175
+ const technical = '- Ownership: contested-technical\n- Convergence: 2/2 convergent\n';
176
+ const product = '- Ownership: product-owned\n- Convergence: 2/2 convergent\n';
177
+ const options = '\n## Options\n\n1. Take the council proposal\n2. Something else\n';
178
+ const failures: string[] = [];
179
+ if (checkRecord('a', technical).length !== 0) failures.push('a conclusive technical verdict red');
180
+ if (checkRecord('b', technical + options).length === 0) {
181
+ failures.push('an options block on a conclusive technical verdict did not red');
182
+ }
183
+ if (checkRecord('c', product + options).length !== 0) failures.push('an owner-owned proposal red');
184
+ if (checkRecord('d', product).length === 0) {
185
+ failures.push('an owner-owned verdict with no proposal did not red');
186
+ }
187
+ if (checkRecord('e', '- Ownership: contested-technical\n- Convergence: 1/2 split\n').length === 0) {
188
+ failures.push('a non-convergent technical verdict with no proposal did not red');
189
+ }
190
+ if (failures.length) {
191
+ for (const f of failures) process.stderr.write(`❌ ${_PROG} --self-test: ${f}\n`);
192
+ return 1;
193
+ }
194
+ process.stdout.write(`✅ ${_PROG} --self-test: all five directions hold\n`);
195
+ return 0;
196
+ }
197
+
198
+ function _isCliEntry(): boolean {
199
+ if (!process.argv[1]) return false;
200
+ try {
201
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ if (_isCliEntry() || process.argv[1] === _HERE) {
208
+ process.exitCode = main();
209
+ }