@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,450 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gate: a major that ships BREAKING CHANGES owes `docs/MIGRATION.md` a section.
4
+ *
5
+ * WHY THIS EXISTS. `lint_scheduled_deprecations` is the FORWARD half of the
6
+ * release pre-flight's MIGRATION.md obligation — commitments due at a future
7
+ * major. The backward half had nothing at all behind it, and it failed exactly
8
+ * the way an unchecked obligation does: 15.0.0 and 16.0.0 both shipped
9
+ * BREAKING CHANGES, neither had a migration section, and the gap survived
10
+ * seventeen separate reports before anything in the tree could see it.
11
+ *
12
+ * WHAT IT REQUIRES, AND WHAT IT DELIBERATELY DOES NOT. A major section in a
13
+ * changelog carrying at least one `### BREAKING CHANGES` entry must have a
14
+ * `## ` heading in `docs/MIGRATION.md` naming that version. That is the whole
15
+ * requirement: a STATED ANSWER, never a non-empty procedure. Not every
16
+ * breaking change asks something of a consumer, and a gate that could not
17
+ * express "this asks nothing of you" would turn into a ritual heading with no
18
+ * content — which is worse than no gate, because the heading would be
19
+ * believed. The 16.0.0 section this gate ships beside says exactly that, and
20
+ * passes.
21
+ *
22
+ * THE FLOOR, AND WHY IT IS A CONSTANT RATHER THAN `package.json`. Majors below
23
+ * {@link FLOOR_MAJOR} are out of scope. The obvious alternative — grandfather
24
+ * everything at or below the shipped version, the cutoff `lint_changelog_rollback`
25
+ * uses — would leave this gate comparing ZERO majors on the day it lands, which
26
+ * is the "a gate that scans nothing exits green" shape. A constant floor set at
27
+ * the oldest major this repository actually carries a section for means the gate
28
+ * does real arithmetic from its first run (two majors today, both passing) and
29
+ * a reader can see which historical majors were forgiven and why.
30
+ *
31
+ * EXIT CONTRACT
32
+ * 0 every in-scope major has a section, or none is in scope for a reason
33
+ * the output names
34
+ * 1 an in-scope major carries BREAKING CHANGES and has no section, or the
35
+ * scan root is dead
36
+ * 2 usage error
37
+ *
38
+ * `--root <dir>` drives the whole check against a fixture tree, which is the
39
+ * seam `--self-test` and the unit suite use. `--self-test` proves the rule
40
+ * against the real CLI on inline fixtures.
41
+ */
42
+ import * as fs from 'node:fs';
43
+ import * as os from 'node:os';
44
+ import * as path from 'node:path';
45
+ import { fileURLToPath, pathToFileURL } from 'node:url';
46
+
47
+ import { GateLedger } from './_lib/gate_ledger.js';
48
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
49
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
50
+
51
+ const _HERE = fileURLToPath(import.meta.url);
52
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
53
+
54
+ export const GATE = 'lint_major_migration_sections';
55
+ export const MIGRATION_PATH = path.join('docs', 'MIGRATION.md');
56
+ const CHANGELOG_PATH = 'CHANGELOG.md';
57
+ const ARCHIVE_DIR = path.join('docs', 'archive');
58
+
59
+ /**
60
+ * Oldest major this gate holds to the requirement.
61
+ *
62
+ * 15 because 15.0.0 is the oldest major `docs/MIGRATION.md` carries a section
63
+ * for. Everything below it shipped before the obligation existed and is
64
+ * forgiven on the record rather than silently: retro-failing eight majors
65
+ * nobody can now write an honest section for is the gate-lands-as-N-blockers
66
+ * failure this repository has refused before. Raise it only by deleting a
67
+ * section, which is not a move this gate should make cheap.
68
+ */
69
+ export const FLOOR_MAJOR = 15;
70
+
71
+ const MAJOR_HEADING_RE = /^## \[(\d+)\.(\d+)\.(\d+)\]/;
72
+ const ANY_SECTION_RE = /^#{1,2} /;
73
+ const BREAKING_HEADING_RE = /^### BREAKING CHANGES\s*$/;
74
+ const SUB_HEADING_RE = /^### /;
75
+ const BULLET_RE = /^\*\s+\S/;
76
+
77
+ /** One major section found in a changelog file. */
78
+ export interface MajorSection {
79
+ /** Bare `X.0.0`. */
80
+ readonly version: string;
81
+ readonly major: number;
82
+ /** Repo-relative file the heading was read from. */
83
+ readonly source: string;
84
+ /** 1-based heading line. */
85
+ readonly line: number;
86
+ /** BREAKING CHANGES bullets under the heading. */
87
+ readonly breaking: number;
88
+ }
89
+
90
+ /**
91
+ * Collect every `## [X.0.0](…)` section and how many BREAKING bullets it has.
92
+ *
93
+ * Minor and patch sections are not returned at all: the obligation is stated
94
+ * per MAJOR, because that is the boundary a consumer plans an upgrade around
95
+ * and the boundary the release runbook already stops at.
96
+ */
97
+ export function findMajorSections(text: string, source: string): MajorSection[] {
98
+ const lines = text.split('\n');
99
+ const out: MajorSection[] = [];
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const m = MAJOR_HEADING_RE.exec(lines[i] as string);
102
+ if (m === null) continue;
103
+ if (m[2] !== '0' || m[3] !== '0') continue;
104
+ let breaking = 0;
105
+ let inBreaking = false;
106
+ for (let j = i + 1; j < lines.length; j++) {
107
+ const line = lines[j] as string;
108
+ if (ANY_SECTION_RE.test(line)) break;
109
+ if (BREAKING_HEADING_RE.test(line)) {
110
+ inBreaking = true;
111
+ continue;
112
+ }
113
+ if (inBreaking && SUB_HEADING_RE.test(line)) {
114
+ inBreaking = false;
115
+ continue;
116
+ }
117
+ if (inBreaking && BULLET_RE.test(line)) breaking += 1;
118
+ }
119
+ out.push({
120
+ version: `${m[1] as string}.0.0`,
121
+ major: Number(m[1]),
122
+ source,
123
+ line: i + 1,
124
+ breaking,
125
+ });
126
+ }
127
+ return out;
128
+ }
129
+
130
+ /**
131
+ * Versions named by a `## ` heading in `docs/MIGRATION.md`.
132
+ *
133
+ * The heading text is matched on a whole version token, so both shapes the
134
+ * file already uses are accepted — `## 16.0.0 — …` and `## 8.x → 9.0.0 — …` —
135
+ * without this gate dictating a heading grammar the file did not choose.
136
+ */
137
+ export function migrationVersions(text: string): Set<string> {
138
+ const out = new Set<string>();
139
+ for (const line of text.split('\n')) {
140
+ if (!line.startsWith('## ')) continue;
141
+ for (const m of line.matchAll(/(?<![\d.])(\d+)\.0\.0(?![\d.])/g)) {
142
+ out.add(`${m[1] as string}.0.0`);
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ export interface Finding {
149
+ readonly section: MajorSection;
150
+ }
151
+
152
+ /** Read every changelog file under `root`, newest-era file first. */
153
+ export function changelogSources(root: string): string[] {
154
+ const out: string[] = [];
155
+ if (fs.existsSync(path.join(root, CHANGELOG_PATH))) out.push(CHANGELOG_PATH);
156
+ const archive = path.join(root, ARCHIVE_DIR);
157
+ if (fs.existsSync(archive)) {
158
+ for (const name of fs.readdirSync(archive).sort()) {
159
+ if (name.startsWith('CHANGELOG-') && name.endsWith('.md')) {
160
+ out.push(path.join(ARCHIVE_DIR, name));
161
+ }
162
+ }
163
+ }
164
+ return out;
165
+ }
166
+
167
+ export interface EvaluateResult {
168
+ readonly findings: Finding[];
169
+ readonly sections: MajorSection[];
170
+ readonly inScope: number;
171
+ }
172
+
173
+ /**
174
+ * Compare the majors found against the sections MIGRATION.md carries.
175
+ *
176
+ * Every section reaches exactly one ledger outcome: out of scope below the
177
+ * floor, out of scope with no BREAKING entries, completed when a heading
178
+ * exists, failed when it does not.
179
+ */
180
+ export function evaluate(
181
+ sections: readonly MajorSection[],
182
+ migration: ReadonlySet<string>,
183
+ ledger: GateLedger,
184
+ ): EvaluateResult {
185
+ const findings: Finding[] = [];
186
+ let inScope = 0;
187
+ for (const s of sections) {
188
+ const target = `${s.source}:${s.version}`;
189
+ ledger.plan(target);
190
+ if (s.major < FLOOR_MAJOR) {
191
+ ledger.outOfScope(target, 'declared_exemption');
192
+ continue;
193
+ }
194
+ if (s.breaking === 0) {
195
+ ledger.outOfScope(target, 'not_applicable_kind');
196
+ continue;
197
+ }
198
+ inScope += 1;
199
+ if (migration.has(s.version)) {
200
+ ledger.complete(target);
201
+ continue;
202
+ }
203
+ ledger.fail(target, `no docs/MIGRATION.md section for ${s.version}`);
204
+ findings.push({ section: s });
205
+ }
206
+ return { findings, sections: [...sections], inScope };
207
+ }
208
+
209
+ /**
210
+ * The cut-time question, answered against an entry that is not on disk yet.
211
+ *
212
+ * `release.ts` renders the changelog entry before prepending it, so at the
213
+ * moment the cut can still be refused the section exists only in memory. This
214
+ * takes that text directly rather than re-reading `CHANGELOG.md`, which at that
215
+ * point still describes the PREVIOUS release.
216
+ *
217
+ * @returns a refusal reason naming the version, or `null` when the cut is clear.
218
+ */
219
+ export function pendingMajorFinding(
220
+ target: string,
221
+ changelogEntry: string,
222
+ migrationText: string,
223
+ ): string | null {
224
+ const m = /^(\d+)\.0\.0$/.exec(target.trim());
225
+ if (m === null) return null;
226
+ const sections = findMajorSections(changelogEntry, '(pending entry)');
227
+ const breaking = sections.reduce((n, s) => n + s.breaking, 0);
228
+ if (breaking === 0) return null;
229
+ if (Number(m[1]) < FLOOR_MAJOR) return null;
230
+ if (migrationVersions(migrationText).has(target)) return null;
231
+ return (
232
+ `the ${target} entry carries ${String(breaking)} BREAKING CHANGES entr(y/ies) and ` +
233
+ `docs/MIGRATION.md has no "## " heading naming ${target}. Add one before the cut — ` +
234
+ 'a section stating that the change asks nothing of a consumer satisfies this, and is ' +
235
+ 'the right answer when it is true. What is not acceptable is silence.'
236
+ );
237
+ }
238
+
239
+ function repoRoot(): string {
240
+ return REPO_ROOT;
241
+ }
242
+
243
+ const SELF_TEST_MIN_CASES = 6;
244
+ const SELF_TEST_MIN_REJECT = 3;
245
+
246
+ /** Prove the rejections still fire against the real CLI, not against imports. */
247
+ function selfTest(): number {
248
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lmms-selftest-'));
249
+ const repo = repoRoot();
250
+ const script = path.join('src', 'scripts', 'lint_major_migration_sections.ts');
251
+
252
+ const changelog = (version: string, breakingBullets: readonly string[]): string =>
253
+ [
254
+ '# Changelog',
255
+ '',
256
+ `## [${version}](https://example.invalid/compare) (2026-01-01)`,
257
+ '',
258
+ ...(breakingBullets.length > 0
259
+ ? ['### BREAKING CHANGES', '', ...breakingBullets.map((b) => `* ${b}`), '']
260
+ : []),
261
+ '### Features',
262
+ '',
263
+ '* something additive',
264
+ '',
265
+ ].join('\n');
266
+
267
+ const fixture = (
268
+ name: string,
269
+ changelogText: string,
270
+ migrationHeadings: readonly string[],
271
+ ): number => {
272
+ const dir = path.join(root, name);
273
+ fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
274
+ fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), changelogText, 'utf8');
275
+ fs.writeFileSync(
276
+ path.join(dir, 'docs', 'MIGRATION.md'),
277
+ ['# Migration Guide', '', ...migrationHeadings.flatMap((h) => [h, '', 'body', ''])].join(
278
+ '\n',
279
+ ),
280
+ 'utf8',
281
+ );
282
+ return runGateCli(repo, script, ['--root', dir, '--quiet'], repo);
283
+ };
284
+
285
+ const cases: SelfTestCase[] = [
286
+ {
287
+ name: 'a major with BREAKING and no MIGRATION heading is refused',
288
+ expect: 'reject',
289
+ run: () => fixture('missing', changelog('17.0.0', ['**x:** drop y']), []),
290
+ },
291
+ {
292
+ // The same fixture with the heading added. This pair IS the gate:
293
+ // one half proves the refusal fires, the other that the heading is
294
+ // what clears it, so neither can be satisfied by a gate that always
295
+ // reds or always passes.
296
+ name: 'the same major passes once the heading exists',
297
+ expect: 'accept',
298
+ run: () =>
299
+ fixture('present', changelog('17.0.0', ['**x:** drop y']), [
300
+ '## 17.0.0 — what to do',
301
+ ]),
302
+ },
303
+ {
304
+ name: 'a heading naming a DIFFERENT major does not satisfy the requirement',
305
+ expect: 'reject',
306
+ run: () =>
307
+ fixture('wrong-version', changelog('17.0.0', ['**x:** drop y']), [
308
+ '## 16.0.0 — what to do',
309
+ ]),
310
+ },
311
+ {
312
+ name: 'an arrow-form heading naming the version satisfies it',
313
+ expect: 'accept',
314
+ run: () =>
315
+ fixture('arrow', changelog('17.0.0', ['**x:** drop y']), [
316
+ '## 16.x → 17.0.0 — what to do',
317
+ ]),
318
+ },
319
+ {
320
+ name: 'a major BELOW the floor is forgiven',
321
+ expect: 'accept',
322
+ run: () => fixture('below-floor', changelog('9.0.0', ['**x:** drop y']), []),
323
+ },
324
+ {
325
+ name: 'a major with no BREAKING entries owes nothing',
326
+ expect: 'accept',
327
+ run: () => fixture('no-breaking', changelog('17.0.0', []), []),
328
+ },
329
+ {
330
+ // The anti-vacuity half. A tree with no changelog at all must not
331
+ // read as "every major has its section"; a gate that greens over an
332
+ // empty corpus is the failure `_lib/scan_scope.ts` exists for.
333
+ name: 'a tree with no changelog is a dead scan root, not a pass',
334
+ expect: 'reject',
335
+ run: () => {
336
+ const dir = path.join(root, 'no-changelog');
337
+ fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
338
+ fs.writeFileSync(path.join(dir, 'docs', 'MIGRATION.md'), '# Migration Guide\n', 'utf8');
339
+ return runGateCli(repo, script, ['--root', dir, '--quiet'], repo);
340
+ },
341
+ },
342
+ ];
343
+
344
+ try {
345
+ return runSelfTest({
346
+ gate: GATE,
347
+ cases,
348
+ minCases: SELF_TEST_MIN_CASES,
349
+ minRejectCases: SELF_TEST_MIN_REJECT,
350
+ });
351
+ } finally {
352
+ fs.rmSync(root, { recursive: true, force: true });
353
+ }
354
+ }
355
+
356
+ export function main(argv: readonly string[]): number {
357
+ let root = REPO_ROOT;
358
+ let quiet = false;
359
+ for (let i = 0; i < argv.length; i++) {
360
+ const a = argv[i];
361
+ if (a === '--quiet') quiet = true;
362
+ else if (a === '--self-test') return selfTest();
363
+ else if (a === '--root') {
364
+ const next = argv[++i];
365
+ if (next === undefined || next.startsWith('--')) {
366
+ process.stderr.write('usage: --root expects a directory\n');
367
+ return 2;
368
+ }
369
+ root = path.resolve(next);
370
+ } else {
371
+ process.stderr.write(
372
+ `usage: ${GATE} [--root <dir>] [--quiet] [--self-test]\nunknown argument: ${String(a)}\n`,
373
+ );
374
+ return 2;
375
+ }
376
+ }
377
+
378
+ const migrationFile = path.join(root, MIGRATION_PATH);
379
+ if (!fs.existsSync(migrationFile)) {
380
+ process.stderr.write(`❌ ${GATE}: ${MIGRATION_PATH} not found under ${root}\n`);
381
+ return 1;
382
+ }
383
+ const migration = migrationVersions(fs.readFileSync(migrationFile, 'utf-8'));
384
+
385
+ const sources = changelogSources(root);
386
+ const sections: MajorSection[] = [];
387
+ for (const rel of sources) {
388
+ sections.push(...findMajorSections(fs.readFileSync(path.join(root, rel), 'utf-8'), rel));
389
+ }
390
+
391
+ const ledger = new GateLedger(GATE);
392
+ const result = evaluate(sections, migration, ledger);
393
+ const tally = ledger.finalize();
394
+
395
+ try {
396
+ // No `allowEmpty`. Zero major sections means the heading shape drifted
397
+ // or the changelog moved — blindness, not cleanliness, and this
398
+ // repository has shipped majors continuously since 1.x.
399
+ reportScanned({
400
+ gate: GATE,
401
+ scanned: tally.planned,
402
+ units: 'major changelog section(s)',
403
+ roots: [CHANGELOG_PATH, ARCHIVE_DIR],
404
+ });
405
+ } catch (exc) {
406
+ if (exc instanceof DeadScopeError) {
407
+ process.stderr.write(`❌ ${exc.message}\n`);
408
+ return 1;
409
+ }
410
+ throw exc;
411
+ }
412
+
413
+ if (result.findings.length > 0) {
414
+ for (const f of result.findings) {
415
+ process.stderr.write(
416
+ `❌ ${GATE}: ${f.section.version} ships ${String(f.section.breaking)} BREAKING ` +
417
+ `CHANGES entr(y/ies) (${f.section.source}:${String(f.section.line)}) and ` +
418
+ `${MIGRATION_PATH} carries no "## " heading naming it.\n` +
419
+ ` Add a section for ${f.section.version}. A section stating that the change\n` +
420
+ ' asks nothing of a consumer satisfies this and is the right answer when it\n' +
421
+ ' is true — the requirement is a stated answer, never a procedure.\n',
422
+ );
423
+ }
424
+ return 1;
425
+ }
426
+
427
+ if (!quiet) {
428
+ ledger.report();
429
+ process.stdout.write(
430
+ `✅ ${GATE}: ${String(tally.planned)} major section(s) read, ` +
431
+ `${String(result.inScope)} in scope (major >= ${String(FLOOR_MAJOR)} with BREAKING ` +
432
+ 'CHANGES), each with a migration section' +
433
+ (result.inScope === 0
434
+ ? ' — NONE in scope this run, so no comparison ran. Green here means\n' +
435
+ ' "nothing to compare", not "the comparison passed".\n'
436
+ : '.\n'),
437
+ );
438
+ }
439
+ return 0;
440
+ }
441
+
442
+ function _isCliEntry(): boolean {
443
+ const entry = process.argv[1];
444
+ if (entry === undefined) return false;
445
+ return import.meta.url === pathToFileURL(path.resolve(entry)).href;
446
+ }
447
+
448
+ if (_isCliEntry()) {
449
+ process.exit(main(process.argv.slice(2)));
450
+ }
@@ -588,6 +588,46 @@ function _blobAt(absPath: string, sha: string): string | null {
588
588
  return _git(root, ['show', `${sha}:${rel}`]);
589
589
  }
590
590
 
591
+ /** The `reviewed:` value carried by `text`, or null when it has no valid marker. */
592
+ function _reviewedIn(text: string): string | null {
593
+ for (const line of text.split('\n')) {
594
+ const m = parseMarker(line);
595
+ if (m !== null) return m.reviewed;
596
+ }
597
+ return null;
598
+ }
599
+
600
+ /**
601
+ * The commit that INTRODUCED the current `reviewed:` value — the oldest of the
602
+ * unbroken run of commits, counting back from the newest, that all carry it.
603
+ *
604
+ * Merge-date lag is why this exists. A review stamped on the day the work was
605
+ * authored but committed (or squash-merged) the next day puts the stamp and the
606
+ * content it reviewed in ONE commit whose date is later than the stamp itself.
607
+ * A baseline of "newest commit dated <= reviewed" then skips that very commit,
608
+ * falls back to the pre-work state, and reports stale against a plan that was
609
+ * reviewed exactly as the contract asks — every author-today/merge-tomorrow PR
610
+ * reproduces it.
611
+ *
612
+ * The oldest commit of the run is the honest baseline, and the age matters:
613
+ * comparing against the NEWEST commit carrying the stamp would compare the
614
+ * current content with itself, making every committed file trivially fresh and
615
+ * emptying the gate.
616
+ */
617
+ function _stampIntroducedAt(
618
+ absPath: string,
619
+ history: HistoryEntry[],
620
+ reviewed: string,
621
+ ): HistoryEntry | undefined {
622
+ let introduced: HistoryEntry | undefined;
623
+ for (const entry of history) {
624
+ const blob = _blobAt(absPath, entry.sha);
625
+ if (blob === null || _reviewedIn(blob) !== reviewed) break;
626
+ introduced = entry;
627
+ }
628
+ return introduced;
629
+ }
630
+
591
631
  export type FileStatus = 'ok' | 'draft-exempt' | 'grandfathered' | 'fail';
592
632
 
593
633
  export interface FileResult {
@@ -653,10 +693,14 @@ export function checkFile(absPath: string): FileResult {
653
693
  // current content, so staleness is not judgeable — see
654
694
  // hasUncommittedChanges. The committed-history case below is unchanged.
655
695
  if (content.reviewed !== null && !hasUncommittedChanges(absPath)) {
656
- // Staleness (contract §§ 1.1 + 3): compare features of the newest
657
- // commit on/before the reviewed date against the current content.
696
+ // Staleness (contract §§ 1.1 + 3): compare the features of the version
697
+ // the review was stamped on against the current content. That version
698
+ // is the commit which INTRODUCED the current `reviewed:` value; the
699
+ // date lookup is the fallback for a stamp no commit carries yet.
658
700
  const history = fileHistory(absPath);
659
- const base = history.find((h) => h.date <= (content.reviewed as string));
701
+ const base =
702
+ _stampIntroducedAt(absPath, history, content.reviewed) ??
703
+ history.find((h) => h.date <= (content.reviewed as string));
660
704
  if (base !== undefined) {
661
705
  const blob = _blobAt(absPath, base.sha);
662
706
  if (blob !== null && !featuresEqual(extractFeatures(blob), extractFeatures(text))) {
@@ -28,6 +28,19 @@
28
28
  * contract above makes. `later/` and `archive/` are outside this gate's
29
29
  * glob and are untouched by it.
30
30
  *
31
+ * 5. An entry that declares `- **Ownership:**` declares one of the three
32
+ * OWNER-OWNED classes (`product-owned`, `business-owned`,
33
+ * `destructive-owned`). A technical class there is a judgement call
34
+ * parked in a file instead of closed, which is the shape ADR-268 § 10
35
+ * retires: a technical decision does not become owner-owned because it is
36
+ * hard, so it routes back through the closure pass rather than into
37
+ * `## Blockers`.
38
+ *
39
+ * HARD rather than ratcheted, on the same "no backlog to grandfather"
40
+ * argument the `Class:` contract makes: `Ownership:` is a new opt-in
41
+ * field, so on the day it ships no entry in the tree declares one and the
42
+ * rule fires on nothing.
43
+ *
31
44
  * Fenced code blocks are stripped before scanning so a roadmap that shows
32
45
  * the `## Blockers` shape as a documentation example is not flagged.
33
46
  *
@@ -107,6 +120,29 @@ const KNOWN_CLASSES: ReadonlySet<string> = new Set(['0', '1', '2', '3']);
107
120
  /** The classes whose whole claim is that an agent can execute them. */
108
121
  const RUNNABLE_CLASSES: ReadonlySet<string> = new Set(['0', '1']);
109
122
 
123
+ /**
124
+ * The ownership axis, as a blocker may declare it.
125
+ *
126
+ * Only the three owner-owned classes are legal here. A blocker IS the record
127
+ * of a decision the agent correctly did not own; a technical class in that
128
+ * field says the opposite — that something the ownership ladder can close was
129
+ * filed instead. The remaining five ownership classes are deliberately absent
130
+ * rather than listed as "also accepted".
131
+ */
132
+ const OWNERSHIP_FIELD_RE = /^-[ \t]*\*\*Ownership:\*\*[ \t]*`?([a-z-]+)`?/im;
133
+ const OWNER_OWNED_CLASSES: ReadonlySet<string> = new Set([
134
+ 'product-owned',
135
+ 'business-owned',
136
+ 'destructive-owned',
137
+ ]);
138
+ const TECHNICAL_CLASSES: ReadonlySet<string> = new Set([
139
+ 'deterministic',
140
+ 'reversible-technical',
141
+ 'contested-technical',
142
+ 'critical-technical',
143
+ 'spend-exhaustion',
144
+ ]);
145
+
110
146
  /**
111
147
  * The authored class, or `''` when the entry declares none.
112
148
  *
@@ -248,6 +284,28 @@ function _scanBoth(rawText: string): ScanResult {
248
284
  });
249
285
  }
250
286
  }
287
+ const ownership = OWNERSHIP_FIELD_RE.exec(body);
288
+ if (ownership !== null) {
289
+ const declared = ownership[1] as string;
290
+ if (TECHNICAL_CLASSES.has(declared)) {
291
+ violations.push({
292
+ line: _lineAt(text, sectionStart + cur.start),
293
+ message:
294
+ `blocker '${cur.id}' declares ownership '${declared}', which is ` +
295
+ 'a technical class — a technical decision does not become ' +
296
+ 'owner-owned because it is hard (ADR-268 § 10). Route it back ' +
297
+ 'through the closure pass and record the answer as a ' +
298
+ '`## Decisions` row instead of parking it here',
299
+ });
300
+ } else if (!OWNER_OWNED_CLASSES.has(declared)) {
301
+ violations.push({
302
+ line: _lineAt(text, sectionStart + cur.start),
303
+ message:
304
+ `blocker '${cur.id}' declares unknown ownership '${declared}' ` +
305
+ '(expected product-owned, business-owned or destructive-owned)',
306
+ });
307
+ }
308
+ }
251
309
  const gaps = DECIDABILITY_FIELDS.filter(([, re]) => !re.test(body)).map(
252
310
  ([name]) => name,
253
311
  );
@@ -628,10 +686,13 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
628
686
  export {
629
687
  REPO_ROOT,
630
688
  ROADMAP_GLOB,
689
+ _stripFencedCode,
631
690
  REQUIRED_FIELDS,
632
691
  DECIDABILITY_FIELDS,
633
692
  KNOWN_CLASSES,
634
693
  RUNNABLE_CLASSES,
694
+ OWNER_OWNED_CLASSES,
695
+ TECHNICAL_CLASSES,
635
696
  _blockerClass,
636
697
  _hasExecutableSubstance,
637
698
  _scan,