@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,464 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * The `## Decisions` contract — ownership vocabulary and section shape.
4
+ *
5
+ * Two check families:
6
+ *
7
+ * A. **Ownership vocabulary.** `decision_resolution`'s axis is
8
+ * ownership, not impact (ADR-268 § 10). A `## Decisions` table's
9
+ * `ownership` column accepts exactly the eight class names and nothing
10
+ * else — `high impact`, `P1`, `critical` and friends are the old axis
11
+ * leaking back in, and the whole point of the ruling is that a technical
12
+ * decision does not become owner-owned because it is hard.
13
+ *
14
+ * B. **Section shape.** A `status: ready` roadmap that still
15
+ * carries an unresolved decision marker (`TBD`, *decide later*, *to be
16
+ * decided*, …) outside a `## Decisions` row is a plan that enters
17
+ * execution with an open question in it. The same marker resolved into a
18
+ * `## Decisions` row is green: the section IS the discharge.
19
+ *
20
+ * Corpus: top-level `agents/roadmaps/*.md` code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
21
+ * (`archive/`, `skipped/`, `later/`,
22
+ * `stubs/` excluded, as are `template.md` and `dashboard*`) — the same corpus
23
+ * `lint_plan_risk_register` reads, and for the same reason: a parked or
24
+ * archived plan is not entering execution. Archived roadmaps carry freeform
25
+ * `## Decisions (locked …)` prose from before this contract existed; they are
26
+ * out of scope rather than grandfathered by a special case.
27
+ *
28
+ * Exit codes: 0 clean · 1 violations · 2 internal error / bad argv.
29
+ */
30
+
31
+ import * as fs from 'node:fs';
32
+ import * as path from 'node:path';
33
+ import process from 'node:process';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
37
+
38
+ const _HERE = fileURLToPath(import.meta.url);
39
+ const _PROG = 'lint_decision_classes';
40
+
41
+ /**
42
+ * The eight ownership classes (ADR-268 § 10). Kept as a literal here rather
43
+ * than imported from `ai_council/config.ts`: that module is the COUNCIL
44
+ * config loader and pulling it in would make a roadmap lint depend on the
45
+ * council's whole schema. The duplication is pinned by a test that reads both.
46
+ */
47
+ export const OWNERSHIP_CLASSES: readonly string[] = [
48
+ 'deterministic',
49
+ 'reversible-technical',
50
+ 'contested-technical',
51
+ 'critical-technical',
52
+ 'product-owned',
53
+ 'business-owned',
54
+ 'destructive-owned',
55
+ 'spend-exhaustion',
56
+ ];
57
+
58
+ /** `resolved by` vocabulary — the third column's contract. */
59
+ export const RESOLVER_FORMS: readonly string[] = [
60
+ 'evidence',
61
+ 'agent',
62
+ 'independent:',
63
+ 'council:',
64
+ 'team:',
65
+ 'owner',
66
+ ];
67
+
68
+ /**
69
+ * Unresolved-decision markers. Deliberately narrow: every pattern is a phrase
70
+ * an author writes when they KNOW a decision is open, never a word that shows
71
+ * up in ordinary plan prose. A detector that fires on normal writing gets
72
+ * weakened until it finds nothing, which is how a
73
+ * closure detector gets neutered.
74
+ */
75
+ const UNRESOLVED_PATTERNS: readonly { readonly re: RegExp; readonly label: string }[] = [
76
+ { re: /\bTBD\b/, label: 'TBD' },
77
+ { re: /\bto be decided\b/i, label: 'to be decided' },
78
+ { re: /\bdecide later\b/i, label: 'decide later' },
79
+ { re: /\bdecided later\b/i, label: 'decided later' },
80
+ { re: /\bopen question\b/i, label: 'open question' },
81
+ { re: /\bwe(?:'| a)re not sure (?:yet|which|whether)\b/i, label: 'not sure yet' },
82
+ ];
83
+
84
+ const DECISIONS_HEADING_RE = /^##\s+Decisions\s*$/;
85
+ const DISCHARGE_HEADING_RE = /^##\s+(Decisions|Blockers)\s*$/;
86
+ const ANY_H2_RE = /^##\s+/;
87
+ const FRONTMATTER_STATUS_RE = /^status:\s*(\S+)/m;
88
+ const EXCLUDED_NAMES_RE = /^(template\.md|dashboard.*)$/;
89
+
90
+ export interface Violation {
91
+ readonly file: string;
92
+ readonly line: number;
93
+ readonly reason: string;
94
+ }
95
+
96
+ /** Split a markdown table row into trimmed cells (leading/trailing pipe dropped). */
97
+ export function tableCells(line: string): string[] {
98
+ const trimmed = line.trim();
99
+ if (!trimmed.startsWith('|')) return [];
100
+ const inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
101
+ return inner.split('|').map((c) => c.trim());
102
+ }
103
+
104
+ function isSeparatorRow(cells: readonly string[]): boolean {
105
+ return cells.length > 0 && cells.every((c) => /^:?-{2,}:?$/.test(c));
106
+ }
107
+
108
+ /** Frontmatter `status:` (lower-cased), or `ready` when absent — status is binary. */
109
+ export function readStatus(text: string): string {
110
+ if (!text.startsWith('---')) return 'ready';
111
+ const end = text.indexOf('\n---', 3);
112
+ if (end < 0) return 'ready';
113
+ const fm = text.slice(0, end);
114
+ const m = FRONTMATTER_STATUS_RE.exec(fm);
115
+ return m ? (m[1] as string).toLowerCase() : 'ready';
116
+ }
117
+
118
+ interface DecisionsSection {
119
+ /** 0-based index of the heading line, or -1 when there is no section. */
120
+ readonly headingIdx: number;
121
+ /** 0-based [start, end) line range of the section body. */
122
+ readonly start: number;
123
+ readonly end: number;
124
+ }
125
+
126
+ export function findDecisionsSection(lines: readonly string[]): DecisionsSection {
127
+ for (let i = 0; i < lines.length; i += 1) {
128
+ if (DECISIONS_HEADING_RE.test(lines[i] as string)) {
129
+ let j = i + 1;
130
+ while (j < lines.length && !ANY_H2_RE.test(lines[j] as string)) j += 1;
131
+ return { headingIdx: i, start: i + 1, end: j };
132
+ }
133
+ }
134
+ return { headingIdx: -1, start: 0, end: 0 };
135
+ }
136
+
137
+ /**
138
+ * Check family A — the ownership column of a `## Decisions` table.
139
+ *
140
+ * The header row names the columns; `ownership` is located by name rather than
141
+ * by position so a plan may add a column without silently moving the one the
142
+ * gate reads.
143
+ */
144
+ export function checkOwnershipColumn(rel: string, lines: readonly string[]): Violation[] {
145
+ const out: Violation[] = [];
146
+ const sec = findDecisionsSection(lines);
147
+ if (sec.headingIdx < 0) return out;
148
+
149
+ let ownershipCol = -1;
150
+ let resolvedCol = -1;
151
+ let headerSeen = false;
152
+ for (let i = sec.start; i < sec.end; i += 1) {
153
+ const cells = tableCells(lines[i] as string);
154
+ if (cells.length === 0) continue;
155
+ if (isSeparatorRow(cells)) continue;
156
+ if (!headerSeen) {
157
+ headerSeen = true;
158
+ ownershipCol = cells.findIndex((c) => /^ownership$/i.test(c));
159
+ resolvedCol = cells.findIndex((c) => /^resolved[ _]by$/i.test(c));
160
+ if (ownershipCol < 0) {
161
+ out.push({
162
+ file: rel,
163
+ line: i + 1,
164
+ reason:
165
+ 'the `## Decisions` table has no `ownership` column — the contract is ' +
166
+ '`| ID | ownership | resolved by | decision | evidence | revisit if |`',
167
+ });
168
+ return out;
169
+ }
170
+ if (resolvedCol < 0) {
171
+ out.push({
172
+ file: rel,
173
+ line: i + 1,
174
+ reason:
175
+ 'the `## Decisions` table has no `resolved by` column — a decision with ' +
176
+ 'no recorded resolver is not a closed decision',
177
+ });
178
+ }
179
+ continue;
180
+ }
181
+ const owner = (cells[ownershipCol] ?? '').replace(/`/g, '').trim();
182
+ if (owner === '') {
183
+ out.push({ file: rel, line: i + 1, reason: 'empty `ownership` cell' });
184
+ } else if (!OWNERSHIP_CLASSES.includes(owner)) {
185
+ out.push({
186
+ file: rel,
187
+ line: i + 1,
188
+ reason:
189
+ `ownership=\`${owner}\` is not one of the eight ownership classes ` +
190
+ `(${OWNERSHIP_CLASSES.join(', ')}). The axis is ownership, not impact ` +
191
+ `(ADR-268 § 10).`,
192
+ });
193
+ }
194
+ if (resolvedCol >= 0) {
195
+ const resolved = (cells[resolvedCol] ?? '').replace(/`/g, '').trim();
196
+ const ok = RESOLVER_FORMS.some((f) =>
197
+ f.endsWith(':') ? resolved.startsWith(f) && resolved.length > f.length : resolved === f,
198
+ );
199
+ if (!ok) {
200
+ out.push({
201
+ file: rel,
202
+ line: i + 1,
203
+ reason:
204
+ `resolved by=\`${resolved}\` is not one of ` +
205
+ `evidence, agent, independent:<session or model>, council:<record>, ` +
206
+ `team:<record>, owner`,
207
+ });
208
+ }
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+
214
+ /**
215
+ * The two sections where an open item is already DISCHARGED by a contract of
216
+ * its own, so the marker there is the record rather than the defect:
217
+ *
218
+ * - `## Decisions` — the closure record this gate exists to require.
219
+ * - `## Blockers` — the structured five-field blocker contract, which owns
220
+ * open items a human must decide. Firing here would demand a roadmap close
221
+ * a decision it has correctly recorded as not-the-agent's to close.
222
+ */
223
+ export function dischargeRanges(lines: readonly string[]): [number, number][] {
224
+ const out: [number, number][] = [];
225
+ for (let i = 0; i < lines.length; i += 1) {
226
+ if (!DISCHARGE_HEADING_RE.test(lines[i] as string)) continue;
227
+ let j = i + 1;
228
+ while (j < lines.length && !ANY_H2_RE.test(lines[j] as string)) j += 1;
229
+ out.push([i, j]);
230
+ }
231
+ return out;
232
+ }
233
+
234
+ /**
235
+ * The IDs recorded in the `## Decisions` table.
236
+ *
237
+ * A marker that NAMES its row is discharged by the record, not by deletion.
238
+ * Without this, the cheapest way to clear a finding is to delete the word
239
+ * "TBD" from the step — which loses the fact that a decision was ever open and
240
+ * teaches exactly the wrong repair.
241
+ */
242
+ export function decisionRowIds(lines: readonly string[]): Set<string> {
243
+ const out = new Set<string>();
244
+ const sec = findDecisionsSection(lines);
245
+ if (sec.headingIdx < 0) return out;
246
+ let headerSeen = false;
247
+ for (let i = sec.start; i < sec.end; i += 1) {
248
+ const cells = tableCells(lines[i] as string);
249
+ if (cells.length === 0 || isSeparatorRow(cells)) continue;
250
+ if (!headerSeen) {
251
+ headerSeen = true;
252
+ continue;
253
+ }
254
+ const id = (cells[0] ?? '').replace(/`/g, '').trim();
255
+ if (id) out.add(id);
256
+ }
257
+ return out;
258
+ }
259
+
260
+ /** A marker line pointing at its own closure record: `closed as D1`, `see D1`. */
261
+ const DISCHARGE_REF_RE = /(?:closed as|resolved as|see|per)\s+`?([A-Za-z]+\d+)`?/i;
262
+
263
+ /**
264
+ * Check family B — an unresolved marker outside the `## Decisions` section.
265
+ *
266
+ * Fenced code, HTML comments and the roadmap's own prose ABOUT the gate would
267
+ * all produce false positives, so: code fences are skipped, and a marker on a
268
+ * line inside the `## Decisions` section is by definition the discharge, not
269
+ * the defect.
270
+ */
271
+ export function checkUnresolvedMarkers(rel: string, lines: readonly string[]): Violation[] {
272
+ const out: Violation[] = [];
273
+ const discharge = dischargeRanges(lines);
274
+ const rowIds = decisionRowIds(lines);
275
+ let inFence = false;
276
+ for (let i = 0; i < lines.length; i += 1) {
277
+ const line = lines[i] as string;
278
+ if (/^\s*(```|~~~)/.test(line)) {
279
+ inFence = !inFence;
280
+ continue;
281
+ }
282
+ if (inFence) continue;
283
+ if (discharge.some(([a, b]) => i >= a && i < b)) continue;
284
+ if (/<!--\s*decision-marker:\s*ignore\s*-->/.test(line)) continue;
285
+ const ref = DISCHARGE_REF_RE.exec(line);
286
+ if (ref && rowIds.has(ref[1] as string)) continue;
287
+ for (const { re, label } of UNRESOLVED_PATTERNS) {
288
+ if (re.test(line)) {
289
+ out.push({
290
+ file: rel,
291
+ line: i + 1,
292
+ reason:
293
+ `unresolved decision marker \`${label}\` in a \`ready\` roadmap — ` +
294
+ `close it and record the answer as a \`## Decisions\` row`,
295
+ });
296
+ break;
297
+ }
298
+ }
299
+ }
300
+ return out;
301
+ }
302
+
303
+ export function checkFile(rel: string, text: string): Violation[] {
304
+ const lines = text.split('\n');
305
+ const status = readStatus(text);
306
+ const out: Violation[] = [...checkOwnershipColumn(rel, lines)];
307
+ if (status === 'ready') {
308
+ out.push(...checkUnresolvedMarkers(rel, lines));
309
+ }
310
+ return out;
311
+ }
312
+
313
+ // --------------------------------------------------------------------------
314
+ // CLI.
315
+ // --------------------------------------------------------------------------
316
+
317
+ function _repoRoot(): string {
318
+ return path.resolve(path.dirname(_HERE), '..', '..');
319
+ }
320
+
321
+ export function listCorpus(root: string): string[] {
322
+ let entries: fs.Dirent[];
323
+ try {
324
+ entries = fs.readdirSync(root, { withFileTypes: true });
325
+ } catch {
326
+ return [];
327
+ }
328
+ return entries
329
+ .filter((e) => e.isFile() && e.name.endsWith('.md') && !EXCLUDED_NAMES_RE.test(e.name))
330
+ .map((e) => path.join(root, e.name))
331
+ .sort();
332
+ }
333
+
334
+ function _usage(): string {
335
+ return (
336
+ `usage: ${_PROG} [-h] [--quiet] [--json] [--self-test]\n\n` +
337
+ `Validate the \`## Decisions\` contract on top-level agents/roadmaps/*.md.\n`
338
+ );
339
+ }
340
+
341
+ export function main(argv?: readonly string[]): number {
342
+ const args = argv ?? process.argv.slice(2);
343
+ let quiet = false;
344
+ let json = false;
345
+ let selfTest = false;
346
+ const unknown: string[] = [];
347
+ for (const a of args) {
348
+ if (a === '-h' || a === '--help') {
349
+ process.stdout.write(_usage());
350
+ return 0;
351
+ } else if (a === '--quiet') quiet = true;
352
+ else if (a === '--json') json = true;
353
+ else if (a === '--self-test') selfTest = true;
354
+ else unknown.push(a);
355
+ }
356
+ if (unknown.length) {
357
+ process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${unknown.join(' ')}\n`);
358
+ return 2;
359
+ }
360
+
361
+ if (selfTest) return _selfTest();
362
+
363
+ const repo = _repoRoot();
364
+ const root = path.join(repo, 'agents', 'roadmaps');
365
+ const files = listCorpus(root);
366
+ try {
367
+ assertScanned({
368
+ gate: _PROG,
369
+ scanned: files.length,
370
+ units: 'roadmap file(s)',
371
+ roots: ['agents/roadmaps'],
372
+ });
373
+ } catch (e) {
374
+ if (e instanceof DeadScopeError) {
375
+ process.stderr.write(`❌ ${e.message}\n`);
376
+ return 2;
377
+ }
378
+ throw e;
379
+ }
380
+
381
+ const violations: Violation[] = [];
382
+ for (const f of files) {
383
+ const rel = path.relative(repo, f);
384
+ violations.push(...checkFile(rel, fs.readFileSync(f, 'utf-8')));
385
+ }
386
+
387
+ if (json) {
388
+ process.stdout.write(JSON.stringify(violations, null, 2) + '\n');
389
+ return violations.length ? 1 : 0;
390
+ }
391
+ if (violations.length) {
392
+ process.stderr.write(`❌ ${_PROG}: ${violations.length} violation(s)\n`);
393
+ for (const v of violations) {
394
+ process.stderr.write(` ${v.file}:${v.line} — ${v.reason}\n`);
395
+ }
396
+ return 1;
397
+ }
398
+ if (!quiet) {
399
+ process.stdout.write(`✅ ${_PROG}: ${files.length} roadmap file(s) scanned, 0 violations\n`);
400
+ }
401
+ return 0;
402
+ }
403
+
404
+ /** Both directions, in-process: a bad table must red, a good one must not. */
405
+ function _selfTest(): number {
406
+ const bad = [
407
+ '## Decisions',
408
+ '',
409
+ '| ID | ownership | resolved by | decision | evidence | revisit if |',
410
+ '|---|---|---|---|---|---|',
411
+ '| D1 | high impact | owner | x | y | z |',
412
+ '',
413
+ ].join('\n');
414
+ const good = [
415
+ '## Decisions',
416
+ '',
417
+ '| ID | ownership | resolved by | decision | evidence | revisit if |',
418
+ '|---|---|---|---|---|---|',
419
+ '| D1 | critical-technical | council:rec-1 | x | y | z |',
420
+ '',
421
+ ].join('\n');
422
+ const failures: string[] = [];
423
+ if (checkFile('bad.md', bad).length === 0) failures.push('an impact-axis class did not red');
424
+ if (checkFile('good.md', good).length !== 0) failures.push('a valid ownership row red');
425
+ if (checkFile('m.md', 'status: ready\n\nPick the codec: TBD\n').length === 0) {
426
+ failures.push('an unresolved marker did not red');
427
+ }
428
+ const resolved = [
429
+ 'Pick the codec: TBD — closed as D1 below.',
430
+ '',
431
+ '## Decisions',
432
+ '',
433
+ '| ID | ownership | resolved by | decision | evidence | revisit if |',
434
+ '|---|---|---|---|---|---|',
435
+ '| D1 | contested-technical | agent | opus | bench | the bench changes |',
436
+ '',
437
+ ].join('\n');
438
+ if (checkFile('m2.md', resolved).length !== 0) {
439
+ failures.push('a marker naming its own closure row red');
440
+ }
441
+ const dangling = 'Pick the codec: TBD — closed as D9.\n';
442
+ if (checkFile('m3.md', dangling).length === 0) {
443
+ failures.push('a marker naming a row that does not exist did not red');
444
+ }
445
+ if (failures.length) {
446
+ for (const f of failures) process.stderr.write(`❌ ${_PROG} --self-test: ${f}\n`);
447
+ return 1;
448
+ }
449
+ process.stdout.write(`✅ ${_PROG} --self-test: both directions hold\n`);
450
+ return 0;
451
+ }
452
+
453
+ function _isCliEntry(): boolean {
454
+ if (!process.argv[1]) return false;
455
+ try {
456
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+
462
+ if (_isCliEntry() || process.argv[1] === _HERE) {
463
+ process.exitCode = main();
464
+ }
@@ -27,6 +27,7 @@
27
27
  * `--strict` upgrades warnings to errors.
28
28
  */
29
29
  import fs from "node:fs";
30
+ import { PLATFORM_METADATA_KEYS } from "./_lib/hook_platform_keys.js";
30
31
  import path from "node:path";
31
32
  import { fileURLToPath, pathToFileURL } from "node:url";
32
33
  import { parse as parseYaml } from "yaml";
@@ -452,10 +453,20 @@ function _check_platforms(
452
453
  errors.push(`platforms.${plat}: must be mapping or null`);
453
454
  continue;
454
455
  }
456
+ const askShape = block["ask"];
457
+ if (askShape !== undefined && askShape !== "native" && askShape !== "text") {
458
+ errors.push(
459
+ `platforms.${plat}.ask: must be 'native' or 'text' ` +
460
+ `(got ${JSON.stringify(askShape)})`,
461
+ );
462
+ }
455
463
  if (block["fallback_only"]) {
456
464
  continue; // Copilot — intentional, no event surface
457
465
  }
458
466
  for (const [event, names] of Object.entries(block)) {
467
+ if (PLATFORM_METADATA_KEYS.has(event)) {
468
+ continue;
469
+ }
459
470
  if (!EVENT_VOCABULARY.has(event)) {
460
471
  errors.push(
461
472
  `platforms.${plat}.${event}: unknown event ` +