@event4u/agent-config 15.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 (283) 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 +301 -347
  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 +77 -29
  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 +97 -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/rules/user-interaction.md +31 -30
  40. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  41. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  42. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  44. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  45. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  46. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  47. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  48. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  51. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  52. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  53. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  54. package/dist/agent-src/templates/agent-settings.md +1 -1
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +188 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  58. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  59. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  60. package/dist/cli-delegate/check_update_banner.js +40 -2
  61. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  62. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  63. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  64. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  65. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  66. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  67. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  68. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  69. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  70. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
  71. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  72. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  73. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  74. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  75. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  76. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  77. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  78. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  79. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  80. package/dist/cli-delegate/cmd_conformance.js +918 -325
  81. package/dist/cli-delegate/cmd_doctor.js +7 -5
  82. package/dist/cli-delegate/cmd_explain.js +5 -3
  83. package/dist/cli-delegate/cmd_export.js +9 -7
  84. package/dist/cli-delegate/cmd_fleet.js +4 -2
  85. package/dist/cli-delegate/cmd_handoff.js +6 -6
  86. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +3 -2
  89. package/dist/cli-delegate/cmd_preflight.js +4 -2
  90. package/dist/cli-delegate/cmd_prune.js +10 -8
  91. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  92. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  93. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  94. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  95. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  96. package/dist/cli-delegate/cmd_settings_set.js +14 -1
  97. package/dist/cli-delegate/cmd_sync.js +10 -8
  98. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  99. package/dist/cli-delegate/cmd_update.js +1 -1
  100. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  101. package/dist/cli-delegate/cmd_validate.js +12 -10
  102. package/dist/cli-delegate/cmd_versions.js +1 -1
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +87 -40
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +4 -4
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +10 -8
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +8 -6
  112. package/dist/hooks/dispatch.js +2601 -1073
  113. package/dist/install/claudeRuleRewrite.js +14 -3
  114. package/dist/install/conflict.js +60 -10
  115. package/dist/install/conflictTracker.js +218 -0
  116. package/dist/install/fsPrimitives.js +54 -0
  117. package/dist/install/install.mjs +2008 -1481
  118. package/dist/install/manifestFiles.js +259 -0
  119. package/dist/install/preflight.js +14 -2
  120. package/dist/install/preserve.js +157 -0
  121. package/dist/install/recordedOwnership.js +132 -0
  122. package/dist/install/txlog.js +11 -2
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/mcp/server.json +2 -2
  125. package/dist/mcp/server.mjs +1612 -667
  126. package/dist/router.json +1 -1
  127. package/dist/scripts/_lib/agent_settings.js +3 -2
  128. package/dist/scripts/_lib/fs_atomic.js +167 -0
  129. package/dist/scripts/_lib/installed_lock.js +1 -1
  130. package/dist/scripts/_lib/installed_tools.js +460 -0
  131. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  132. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  133. package/dist/server/io/yamlIO.js +50 -0
  134. package/dist/server/routes/install.js +8 -0
  135. package/dist/server/routes/wizard.js +9 -3
  136. package/dist/server/schemas/settings.js +17 -5
  137. package/dist/shared/missionExecution.js +97 -0
  138. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +103 -0
  141. package/docs/contracts/settings-classes.md +26 -7
  142. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  143. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  144. package/docs/guidelines/design-fidelity-routing.md +29 -0
  145. package/package.json +1 -1
  146. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  147. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  148. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  149. package/src/config/agent-settings.template.yml +139 -27
  150. package/src/config/auto-rule-budget.json +16 -0
  151. package/src/config/ci-local-parity.yml +18 -5
  152. package/src/config/continuity-surface.json +28 -0
  153. package/src/config/gate-coverage.yml +146 -13
  154. package/src/config/gate-violation-baselines.json +6 -6
  155. package/src/config/hook-token-budget.json +2 -0
  156. package/src/config/host-injection-effect.json +1 -1
  157. package/src/config/loop-surfaces.yaml +215 -0
  158. package/src/config/platform-anchor.json +3 -2
  159. package/src/config/preamble-payload-budget.json +12 -7
  160. package/src/config/preamble-payload-exceptions.json +28 -0
  161. package/src/config/release-gate-locality.yml +11 -0
  162. package/src/config/routing-coverage-seed.json +18 -4
  163. package/src/config/rule-enforcement-baseline.json +0 -1
  164. package/src/install/claudeRuleRewrite.ts +16 -2
  165. package/src/install/conflict.ts +65 -9
  166. package/src/install/conflictTracker.ts +299 -0
  167. package/src/install/fsPrimitives.ts +54 -0
  168. package/src/install/manifestFiles.ts +312 -0
  169. package/src/install/preflight.ts +16 -2
  170. package/src/install/preserve.ts +205 -0
  171. package/src/install/recordedOwnership.ts +149 -0
  172. package/src/install/txlog.ts +11 -2
  173. package/src/install/types.ts +19 -4
  174. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  175. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  176. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  177. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  178. package/src/scripts/_cli/doctor_execution.ts +90 -0
  179. package/src/scripts/_dispatch.bash +3 -1
  180. package/src/scripts/_lib/activation_payload.ts +247 -0
  181. package/src/scripts/_lib/agent_settings.ts +4 -2
  182. package/src/scripts/_lib/authority_path.ts +102 -0
  183. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  184. package/src/scripts/_lib/cascade_base.ts +94 -0
  185. package/src/scripts/_lib/changelog_eras.ts +7 -2
  186. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  187. package/src/scripts/_lib/council_transport.ts +129 -0
  188. package/src/scripts/_lib/delivery_ready.ts +112 -0
  189. package/src/scripts/_lib/dropped_decision.ts +142 -0
  190. package/src/scripts/_lib/forge_protection.ts +267 -0
  191. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  192. package/src/scripts/_lib/installed_lock.ts +1 -1
  193. package/src/scripts/_lib/json_python_parity.ts +127 -0
  194. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  195. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  196. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  197. package/src/scripts/_lib/mission_record.ts +190 -0
  198. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  199. package/src/scripts/_lib/obligations.ts +402 -0
  200. package/src/scripts/_lib/orchestration_record.ts +11 -20
  201. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  202. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  203. package/src/scripts/_lib/release_highlights.ts +199 -10
  204. package/src/scripts/_lib/release_holds.ts +493 -0
  205. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  206. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  207. package/src/scripts/_lib/review_baseline.ts +254 -0
  208. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  209. package/src/scripts/_lib/rides_along.ts +102 -0
  210. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  211. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  212. package/src/scripts/_lib/test_delta.ts +166 -0
  213. package/src/scripts/_lib/test_provenance.ts +171 -0
  214. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  215. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  216. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  217. package/src/scripts/ai_council/config.ts +8 -11
  218. package/src/scripts/ai_council/fallback_config.ts +5 -3
  219. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  220. package/src/scripts/ask_block_census.ts +285 -15
  221. package/src/scripts/check_beta_review_markers.ts +35 -4
  222. package/src/scripts/check_enforcement_matrix.ts +624 -0
  223. package/src/scripts/check_finding_dispositions.ts +167 -16
  224. package/src/scripts/check_gate_reachability.ts +173 -10
  225. package/src/scripts/check_generator_sync.ts +954 -0
  226. package/src/scripts/check_held_object_arrivals.ts +381 -0
  227. package/src/scripts/check_preamble_payload_budget.ts +474 -124
  228. package/src/scripts/check_release_highlights.ts +33 -1
  229. package/src/scripts/check_release_holds.ts +567 -0
  230. package/src/scripts/check_release_published.ts +1 -1
  231. package/src/scripts/check_reply_consistency.ts +86 -17
  232. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  233. package/src/scripts/check_test_delta.ts +294 -0
  234. package/src/scripts/check_test_weakening.ts +299 -0
  235. package/src/scripts/closure_scan.ts +333 -0
  236. package/src/scripts/compile_hook_manifest.ts +43 -3
  237. package/src/scripts/council_record_shape.ts +209 -0
  238. package/src/scripts/hook_manifest.json +1 -1
  239. package/src/scripts/hook_manifest.yaml +127 -12
  240. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  241. package/src/scripts/hooks/concern_registry.ts +6 -0
  242. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  243. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  244. package/src/scripts/hooks/evidence_independence.ts +96 -0
  245. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  246. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  247. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  248. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  249. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  250. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  251. package/src/scripts/hooks_doctor.ts +89 -1
  252. package/src/scripts/hooks_status.ts +19 -0
  253. package/src/scripts/install.ts +107 -207
  254. package/src/scripts/interruption_report.ts +8 -2
  255. package/src/scripts/janitor.ts +15 -1
  256. package/src/scripts/lint_decision_classes.ts +464 -0
  257. package/src/scripts/lint_hook_manifest.ts +11 -0
  258. package/src/scripts/lint_major_migration_sections.ts +450 -0
  259. package/src/scripts/lint_plan_risk_register.ts +47 -3
  260. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  261. package/src/scripts/lint_roadmap_producers.ts +202 -0
  262. package/src/scripts/measure_rule_budget.ts +101 -5
  263. package/src/scripts/measure_turn_end_gate.ts +302 -5
  264. package/src/scripts/new_roadmap.ts +37 -0
  265. package/src/scripts/orchestration_record.ts +0 -5
  266. package/src/scripts/release.ts +66 -162
  267. package/src/scripts/release_drill.ts +460 -15
  268. package/src/scripts/release_env.ts +1 -1
  269. package/src/scripts/release_publication.ts +52 -1
  270. package/src/scripts/report_enforcement_drift.ts +566 -0
  271. package/src/scripts/report_held_object_arrivals.ts +456 -0
  272. package/src/scripts/report_host_injection_effect.ts +3 -11
  273. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  274. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  275. package/src/scripts/schemas/command.schema.json +4 -0
  276. package/src/scripts/scope_growth.ts +219 -0
  277. package/src/scripts/sync_agent_settings.ts +140 -18
  278. package/src/scripts/trigger_eval_grandfather.json +0 -1
  279. package/src/scripts/ui_conformance_probe.ts +622 -0
  280. package/src/server/io/yamlIO.ts +48 -0
  281. package/src/server/schemas/settings.ts +31 -7
  282. package/src/shared/missionExecution.ts +126 -0
  283. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -70,6 +70,7 @@ import {
70
70
  type SpanCommit,
71
71
  collect_span_commits,
72
72
  derive_categories,
73
+ make_span_file_reader,
73
74
  parse_git_log,
74
75
  previous_release_tag,
75
76
  stale_draft_labels,
@@ -126,6 +127,24 @@ export interface Contradiction {
126
127
  evidence: string[];
127
128
  }
128
129
 
130
+ /** The fifth label, named once so the note below and the gate cannot drift. */
131
+ export const KNOWN_LIMITATIONS = 'Known limitations';
132
+
133
+ /**
134
+ * What a passing `Known limitations: _none_` actually claims.
135
+ *
136
+ * Exported so it is a fixture rather than a sentence somebody can soften: the
137
+ * derivation reads self-declared residuals, so it is blind to a limitation
138
+ * nobody documented, and printing `_none_` without saying so would turn an
139
+ * undefended field into a defended-looking one. That inversion is the whole
140
+ * risk the roadmap registered against making this label derivable.
141
+ */
142
+ export const UNDERIVED_LIMITATIONS_NOTE =
143
+ 'ℹ️ `Known limitations: _none_` means NO CANDIDATE WAS DERIVED from the span, ' +
144
+ 'not that no limitations exist —\n' +
145
+ ' the derivation reads residuals a commit or a touched executable file ' +
146
+ 'DECLARES, so an undocumented one is invisible to it.';
147
+
129
148
  /**
130
149
  * A populated generated category meeting a `_none_` curated field is a
131
150
  * contradiction. Filled fields and underived labels never fail.
@@ -281,7 +300,12 @@ export function main(argv: readonly string[]): number {
281
300
  // reaches another section, another era file, or `docs/archive/`. That is
282
301
  // what keeps eighteen historical marker lines from turning this into a
283
302
  // permanent red on every future release.
284
- const derived = derive_categories(span);
303
+ // `--to` and not the worktree: the gate must answer about the ref it was
304
+ // pointed at, and on a release branch the checkout and `--to` are the same
305
+ // commit only by accident of how the gate happens to be invoked.
306
+ const derived = derive_categories(span, {
307
+ readTouchedFile: make_span_file_reader(to, REPO_ROOT),
308
+ });
285
309
  const drafts = stale_draft_labels(curated);
286
310
  if (drafts.length > 0) {
287
311
  process.stderr.write(
@@ -315,6 +339,14 @@ export function main(argv: readonly string[]): number {
315
339
  const contradictions = highlight_contradictions(curated, derived);
316
340
  if (contradictions.length === 0) {
317
341
  process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
342
+ // Risk 2 of the roadmap that made `Known limitations` derivable, paid
343
+ // where it lands rather than in a comment: a self-declared residual is
344
+ // only detectable when somebody WROTE IT DOWN, so a derived `_none_`
345
+ // here reads stronger than the undefended one it replaced unless the
346
+ // gate says which of the two it means. It means the first.
347
+ if ((curated[KNOWN_LIMITATIONS] ?? '').trim() === HEAD_NONE) {
348
+ process.stdout.write(`${UNDERIVED_LIMITATIONS_NOTE}\n`);
349
+ }
318
350
  return 0;
319
351
  }
320
352
  process.stderr.write(
@@ -0,0 +1,567 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * check_release_holds — the one evaluator every release boundary reads.
4
+ *
5
+ * Template rule 28. Four modes, and the split matters because two of them run
6
+ * on completely different paths:
7
+ *
8
+ * --lint a malformed declaration reddens NORMAL CI, so a broken hold
9
+ * cannot fail open by being unparseable.
10
+ * --status list every declared hold and its state. Read-only.
11
+ * --require-safe the RELEASE-path question: may a cut be published now?
12
+ * --selftest every row of rule 28's state table, including the
13
+ * not-evaluable row, asserted against fixtures.
14
+ *
15
+ * Exit codes: 0 = clean / safe · 1 = a violation or a refusal · 2 = usage.
16
+ *
17
+ * The load-bearing behaviour, and the one worth re-reading before changing
18
+ * anything here: `not-evaluable` REFUSES, on every channel. An evaluator that
19
+ * cannot read a declaration has learned nothing about whether the tree is safe,
20
+ * and "learned nothing" is not "safe". There is deliberately no flag, label or
21
+ * trailer anywhere in this file that turns a refusal into a pass.
22
+ */
23
+
24
+ import * as fs from 'node:fs';
25
+ import * as os from 'node:os';
26
+ import * as path from 'node:path';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+
29
+ import {
30
+ evaluateFile,
31
+ evaluateHolds,
32
+ lifecycleViolations,
33
+ refuses,
34
+ refusalReport,
35
+ REFUSING_DIRS,
36
+ type CutChannel,
37
+ type Hold,
38
+ } from './_lib/release_holds.js';
39
+ import { REPO_ROOT } from './lint_roadmap_blockers.js';
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
41
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
42
+
43
+ const _HERE = fileURLToPath(import.meta.url);
44
+
45
+ /**
46
+ * The folders a release question must read.
47
+ *
48
+ * Wider than `lint_roadmap_blockers`' active-only glob on purpose: rule 28's
49
+ * per-folder lifecycle says a window does not disappear by moving the file, and
50
+ * a `later/` roadmap still refuses. `archive/` and `skipped/` are included
51
+ * because the move INTO them is refused while a window is open — a file that
52
+ * reached them with an open window is a defect this must still see, not one it
53
+ * should be blind to.
54
+ */
55
+ const HOLD_DIRS = ['', 'later', 'archive', 'skipped', 'stubs'] as const;
56
+
57
+ function globRoadmaps(root = REPO_ROOT): string[] {
58
+ const out: string[] = [];
59
+ for (const sub of HOLD_DIRS) {
60
+ const dir = path.join(root, 'agents', 'roadmaps', sub);
61
+ let entries: fs.Dirent[];
62
+ try {
63
+ entries = fs.readdirSync(dir, { withFileTypes: true });
64
+ } catch {
65
+ continue;
66
+ }
67
+ for (const e of entries) {
68
+ if (e.isFile() && e.name.endsWith('.md')) {
69
+ out.push(path.join(dir, e.name));
70
+ }
71
+ }
72
+ }
73
+ return out.sort();
74
+ }
75
+
76
+ function collect(root = REPO_ROOT): Hold[] {
77
+ return globRoadmaps(root).flatMap((f) => evaluateFile(f));
78
+ }
79
+
80
+ function rel(p: string): string {
81
+ return path.relative(REPO_ROOT, p).split(path.sep).join('/');
82
+ }
83
+
84
+ function cmdLint(root: string): number {
85
+ // A broken glob is this gate's one silent failure mode: with zero files it
86
+ // would print "0 declared hold(s), all well-formed" and exit 0, certifying
87
+ // a coverage that does not exist. "Zero holds" is a real and normal state;
88
+ // "zero FILES under agents/roadmaps" never is.
89
+ try {
90
+ assertScanned({
91
+ gate: 'check_release_holds',
92
+ scanned: globRoadmaps(root).length,
93
+ units: 'roadmap file(s)',
94
+ roots: ['agents/roadmaps'],
95
+ });
96
+ } catch (e) {
97
+ if (e instanceof DeadScopeError) {
98
+ process.stderr.write(`❌ ${e.message}\n`);
99
+ return 1;
100
+ }
101
+ throw e;
102
+ }
103
+
104
+ const files = globRoadmaps(root);
105
+ const holds = collect(root);
106
+ const bad = holds.filter((h) => h.state === 'not-evaluable');
107
+
108
+ // The lifecycle half. A move already made is what a gate can see: if a file
109
+ // with a live window is sitting in `archive/` or `skipped/`, the refused
110
+ // move happened, and reddening CI is how the move is refused in practice.
111
+ const lifecycle = files.flatMap((f) => {
112
+ const hs = evaluateFile(f);
113
+ if (hs.length === 0) {
114
+ return [];
115
+ }
116
+ let text: string;
117
+ try {
118
+ text = fs.readFileSync(f, 'utf8');
119
+ } catch {
120
+ return [];
121
+ }
122
+ return lifecycleViolations(f, hs, text);
123
+ });
124
+
125
+ process.stdout.write(`scanned: ${files.length}\n`);
126
+ if (bad.length === 0 && lifecycle.length === 0) {
127
+ process.stdout.write(
128
+ `✅ check-release-holds: ${holds.length} declared hold(s), all well-formed, ` +
129
+ `lifecycle clean\n`,
130
+ );
131
+ return 0;
132
+ }
133
+ if (bad.length > 0) {
134
+ process.stderr.write('❌ check-release-holds: malformed declaration(s):\n');
135
+ for (const h of bad) {
136
+ for (const why of h.malformed) {
137
+ process.stderr.write(` - ${rel(h.file)} · hold \`${h.id}\`: ${why}\n`);
138
+ }
139
+ }
140
+ }
141
+ if (lifecycle.length > 0) {
142
+ process.stderr.write('❌ check-release-holds: lifecycle violation(s):\n');
143
+ for (const v of lifecycle) {
144
+ process.stderr.write(` - ${rel(v.file)} · hold \`${v.holdId}\`: ${v.reason}\n`);
145
+ }
146
+ }
147
+ return 1;
148
+ }
149
+
150
+ /**
151
+ * Answer the move BEFORE it happens, so the loop asks rather than discovers.
152
+ *
153
+ * `--can-move <file> <archive|skipped|later>`; exit 0 permits, 1 refuses. The
154
+ * `--lint` gate above is the backstop that catches a move made without asking.
155
+ */
156
+ function cmdCanMove(file: string, dest: string): number {
157
+ if (!(REFUSING_DIRS as readonly string[]).includes(dest) && dest !== 'later') {
158
+ process.stderr.write(`❌ --can-move destination must be archive, skipped or later\n`);
159
+ return 2;
160
+ }
161
+ const holds = evaluateFile(file);
162
+ const live = holds.filter((h) => h.state === 'open' || h.state === 'not-evaluable');
163
+ if (live.length === 0) {
164
+ process.stdout.write(`✅ no live window — the move to \`${dest}/\` is permitted\n`);
165
+ return 0;
166
+ }
167
+ if (dest === 'later') {
168
+ let text = '';
169
+ try {
170
+ text = fs.readFileSync(file, 'utf8');
171
+ } catch {
172
+ /* an unreadable file is already not-evaluable above */
173
+ }
174
+ const problems = lifecycleViolations(
175
+ path.join(REPO_ROOT, 'agents', 'roadmaps', 'later', path.basename(file)),
176
+ holds,
177
+ text,
178
+ );
179
+ if (problems.length === 0) {
180
+ process.stdout.write(
181
+ `✅ the move to \`later/\` is permitted — the window stays listed and still refuses\n`,
182
+ );
183
+ return 0;
184
+ }
185
+ for (const v of problems) {
186
+ process.stderr.write(`❌ hold \`${v.holdId}\`: ${v.reason}\n`);
187
+ }
188
+ return 1;
189
+ }
190
+ for (const h of live) {
191
+ process.stderr.write(
192
+ `❌ REFUSED: hold \`${h.id}\` is ${h.state} — a roadmap with a live window may not ` +
193
+ `move to \`${dest}/\`. Finish \`${h.clearedBy || '?'}\`, or move it to \`later/\`.\n`,
194
+ );
195
+ }
196
+ return 1;
197
+ }
198
+
199
+ function cmdStatus(root: string): number {
200
+ const holds = collect(root);
201
+ if (holds.length === 0) {
202
+ process.stdout.write('No release holds declared. Every cut is permitted.\n');
203
+ return 0;
204
+ }
205
+ for (const h of holds) {
206
+ process.stdout.write(
207
+ `${h.state.padEnd(14)} ${h.id.padEnd(28)} channel=${h.channel.padEnd(6)} ` +
208
+ `opens=${h.openedBy || '-'} clears=${h.clearedBy || '-'} ${rel(h.file)}\n`,
209
+ );
210
+ }
211
+ return 0;
212
+ }
213
+
214
+ function cmdRequireSafe(root: string, cut: CutChannel): number {
215
+ const report = refusalReport(collect(root), cut);
216
+ if (report === null) {
217
+ process.stdout.write(`✅ release-holds: safe to cut (channel ${cut})\n`);
218
+ return 0;
219
+ }
220
+ process.stderr.write(`❌ ${report}\n`);
221
+ return 1;
222
+ }
223
+
224
+ /** Rule 28's state table, one fixture per row, plus the two channel cases. */
225
+ function cmdSelftest(): number {
226
+ // Built line-by-line rather than as one template literal on purpose: a
227
+ // heredoc whose lines begin with `##` reads to `lint_code_comments` as a
228
+ // markdown heading left in source, and the fixture would redden a gate it
229
+ // has nothing to do with.
230
+ const base = (open: string, clear: string, channel = 'all', extra = '') =>
231
+ [
232
+ '',
233
+ '## Phase 1',
234
+ `- [${open}] **1.1 opener** <!-- opens-hold: probe -->`,
235
+ " verify: the opener's own check",
236
+ `- [${clear}] **1.2 clearer** <!-- clears-hold: probe -->`,
237
+ ' verify: the check that proves the state is repaired',
238
+ '',
239
+ '## Release holds',
240
+ '',
241
+ '### hold: probe',
242
+ `- **Channel:** ${channel}`,
243
+ '- **Opened by:** 1.1',
244
+ '- **Cleared by:** 1.2',
245
+ '- **State:** the surface is half wired while this is open.',
246
+ '- **Why not a guard:** the entry point is reachable and cannot be made inert.',
247
+ extra,
248
+ ].join('\n');
249
+
250
+ type Case = { name: string; text: string; state: string; all: boolean; latest: boolean };
251
+ const cases: Case[] = [
252
+ { name: 'unopened permits', text: base(' ', ' '), state: 'unopened', all: false, latest: false },
253
+ { name: 'open/all refuses every cut', text: base('x', ' '), state: 'open', all: true, latest: true },
254
+ { name: 'cleared permits', text: base('x', 'x'), state: 'cleared', all: false, latest: false },
255
+ {
256
+ name: 'open/latest refuses stable, permits prerelease',
257
+ text: base('x', ' ', 'latest'),
258
+ state: 'open',
259
+ all: true,
260
+ latest: false,
261
+ },
262
+ // The load-bearing row. Each of these is a DIFFERENT way to be
263
+ // unreadable, and every one of them must refuse on BOTH channels.
264
+ {
265
+ name: 'not-evaluable: missing `Why not a guard:`',
266
+ text: base('x', ' ').replace(/- \*\*Why not a guard:\*\*.*\n/, ''),
267
+ state: 'not-evaluable',
268
+ all: true,
269
+ latest: true,
270
+ },
271
+ {
272
+ name: 'not-evaluable: unknown channel',
273
+ text: base('x', ' ', 'someday'),
274
+ state: 'not-evaluable',
275
+ all: true,
276
+ latest: true,
277
+ },
278
+ {
279
+ name: 'not-evaluable: clearer carries no `verify:`',
280
+ text: base('x', ' ').replace(
281
+ ' verify: the check that proves the state is repaired\n',
282
+ '',
283
+ ),
284
+ state: 'not-evaluable',
285
+ all: true,
286
+ latest: true,
287
+ },
288
+ {
289
+ name: 'not-evaluable: marker is not on a checkbox',
290
+ text: base('x', ' ').replace(
291
+ '- [ ] **1.2 clearer** <!-- clears-hold: probe -->',
292
+ 'The clearer is step 1.2 <!-- clears-hold: probe -->',
293
+ ),
294
+ state: 'not-evaluable',
295
+ all: true,
296
+ latest: true,
297
+ },
298
+ {
299
+ name: 'not-evaluable: hold entry names a version (rule 13)',
300
+ text: base('x', ' ', 'all', '- **Note:** ships in 15.1.0\n'),
301
+ state: 'not-evaluable',
302
+ all: true,
303
+ latest: true,
304
+ },
305
+ {
306
+ name: 'not-evaluable: duplicate hold id',
307
+ text: `${base('x', ' ')}\n### hold: probe\n- **Channel:** all\n`,
308
+ state: 'not-evaluable',
309
+ all: true,
310
+ latest: true,
311
+ },
312
+ {
313
+ name: 'not-evaluable: marker with no entry',
314
+ text: '## Phase 1\n- [x] **1.1 opener** <!-- opens-hold: orphan -->\n',
315
+ state: 'not-evaluable',
316
+ all: true,
317
+ latest: true,
318
+ },
319
+ ];
320
+
321
+ // The two NEGATIVE cases. Without them a check that refused everything
322
+ // would pass every row above, which is the tautology this guards against.
323
+ const negatives: { name: string; text: string }[] = [
324
+ { name: 'a roadmap with no holds section declares nothing', text: '## Phase 1\n- [ ] **1.1 x**\n' },
325
+ {
326
+ name: 'a fenced documentation example is not a declaration',
327
+ text: '```markdown\n## Release holds\n\n### hold: doc-example\n- **Channel:** all\n```\n',
328
+ },
329
+ ];
330
+
331
+ // Lifecycle, rule 28's per-folder half. Asserted on the SAME fixture text
332
+ // under three different paths, so the only variable is the folder.
333
+ const openText = base('x', ' ');
334
+ const lifecycle: { name: string; file: string; text: string; want: number }[] = [
335
+ {
336
+ name: 'archive/ refuses a live window',
337
+ file: 'agents/roadmaps/archive/probe.md',
338
+ text: openText,
339
+ want: 1,
340
+ },
341
+ {
342
+ name: 'skipped/ refuses a live window',
343
+ file: 'agents/roadmaps/skipped/probe.md',
344
+ text: openText,
345
+ want: 1,
346
+ },
347
+ {
348
+ name: 'later/ refuses when entry_condition.what does not name the hold',
349
+ file: 'agents/roadmaps/later/probe.md',
350
+ text: `---\nstatus: later\nentry_condition:\n what: something else entirely\n when: later\n who: maintainer\n---\n${openText}`,
351
+ want: 1,
352
+ },
353
+ {
354
+ name: 'later/ permits when entry_condition.what names the hold',
355
+ file: 'agents/roadmaps/later/probe.md',
356
+ text: `---\nstatus: later\nentry_condition:\n what: the probe hold stays open until 1.2 lands\n when: later\n who: maintainer\n---\n${openText}`,
357
+ want: 0,
358
+ },
359
+ {
360
+ name: 'the active root permits a live window',
361
+ file: 'agents/roadmaps/probe.md',
362
+ text: openText,
363
+ want: 0,
364
+ },
365
+ {
366
+ name: 'archive/ permits a CLEARED window',
367
+ file: 'agents/roadmaps/archive/probe.md',
368
+ text: base('x', 'x'),
369
+ want: 0,
370
+ },
371
+ ];
372
+
373
+ let failed = 0;
374
+ for (const c of cases) {
375
+ const holds = evaluateHolds(c.text, 'selftest.md');
376
+ const h = holds[0];
377
+ const ok =
378
+ h !== undefined &&
379
+ h.state === c.state &&
380
+ refuses(h, 'all') === c.all &&
381
+ refuses(h, 'latest') === c.latest;
382
+ if (!ok) {
383
+ failed += 1;
384
+ process.stderr.write(
385
+ ` ❌ ${c.name}: got state=${h?.state ?? '<none>'} ` +
386
+ `all=${h ? refuses(h, 'all') : '-'} latest=${h ? refuses(h, 'latest') : '-'}; ` +
387
+ `want state=${c.state} all=${c.all} latest=${c.latest}\n`,
388
+ );
389
+ }
390
+ }
391
+ for (const n of negatives) {
392
+ const holds = evaluateHolds(n.text, 'selftest.md');
393
+ if (holds.length !== 0) {
394
+ failed += 1;
395
+ process.stderr.write(` ❌ ${n.name}: got ${holds.length} hold(s), want 0\n`);
396
+ }
397
+ }
398
+
399
+ for (const l of lifecycle) {
400
+ const got = lifecycleViolations(l.file, evaluateHolds(l.text, l.file), l.text).length;
401
+ if (got !== l.want) {
402
+ failed += 1;
403
+ process.stderr.write(` ❌ ${l.name}: got ${got} violation(s), want ${l.want}\n`);
404
+ }
405
+ }
406
+
407
+ const total = cases.length + negatives.length + lifecycle.length;
408
+ if (failed > 0) {
409
+ process.stderr.write(`❌ check-release-holds --selftest: ${failed}/${total} failed\n`);
410
+ return 1;
411
+ }
412
+ process.stdout.write(
413
+ `✅ check-release-holds --selftest: ${total}/${total} — every state-table row ` +
414
+ `(including not-evaluable, 7 ways), both negatives, and all six lifecycle cases\n`,
415
+ );
416
+ return 0;
417
+ }
418
+
419
+ /**
420
+ * The house self-test: fixture trees on disk, this gate's real CLI, exit codes.
421
+ *
422
+ * Two accept cases are not padding. A gate that refused everything would pass
423
+ * every reject case, and the one that matters most is the VALID OPEN WINDOW:
424
+ * normal CI must stay green on exactly the tree state the release path refuses,
425
+ * and nothing else in this suite would notice if `--lint` started reddening it.
426
+ */
427
+ function cmdConventionSelfTest(): number {
428
+ const script = 'src/scripts/check_release_holds.ts';
429
+ const mk = (name: string, files: Record<string, string>): string => {
430
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), `crh-${name}-`));
431
+ for (const [rel, body] of Object.entries(files)) {
432
+ const abs = path.join(root, rel);
433
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
434
+ fs.writeFileSync(abs, body, 'utf8');
435
+ }
436
+ return root;
437
+ };
438
+ const hold = (open: string, clear: string, extra = ''): string =>
439
+ [
440
+ '## Phase 1',
441
+ `- [${open}] **1.1 opener** <!-- opens-hold: probe -->`,
442
+ ' verify: the opener check',
443
+ `- [${clear}] **1.2 clearer** <!-- clears-hold: probe -->`,
444
+ ' verify: ./scripts-run src/scripts/check_surface_whole',
445
+ '',
446
+ '## Release holds',
447
+ '',
448
+ '### hold: probe',
449
+ '- **Channel:** all',
450
+ '- **Opened by:** 1.1',
451
+ '- **Cleared by:** 1.2',
452
+ '- **State:** the surface is half wired while this is open.',
453
+ `- **Why not a guard:** the entry point cannot be made inert.${extra}`,
454
+ '',
455
+ ].join('\n');
456
+ const at = (root: string, args: string[]): number =>
457
+ runGateCli(REPO_ROOT, script, [...args, '--root', root], REPO_ROOT);
458
+
459
+ const cases: SelfTestCase[] = [
460
+ {
461
+ name: 'a malformed declaration reddens --lint',
462
+ expect: 'reject',
463
+ run: () =>
464
+ at(
465
+ mk('bad', {
466
+ 'agents/roadmaps/r.md': hold('x', ' ').replace(
467
+ /- \*\*Why not a guard:\*\*.*\n/,
468
+ '',
469
+ ),
470
+ }),
471
+ ['--lint'],
472
+ ),
473
+ },
474
+ {
475
+ name: 'an open window refuses --require-safe',
476
+ expect: 'reject',
477
+ run: () => at(mk('open', { 'agents/roadmaps/r.md': hold('x', ' ') }), ['--require-safe']),
478
+ },
479
+ {
480
+ name: 'a live window sitting in archive/ reddens --lint',
481
+ expect: 'reject',
482
+ run: () => at(mk('arch', { 'agents/roadmaps/archive/r.md': hold('x', ' ') }), ['--lint']),
483
+ },
484
+ {
485
+ name: 'a dead scan scope reddens rather than certifying an empty corpus',
486
+ expect: 'reject',
487
+ run: () => at(mk('dead', { 'README.md': 'no roadmaps here\n' }), ['--lint']),
488
+ },
489
+ {
490
+ name: 'a clean corpus passes --lint',
491
+ expect: 'accept',
492
+ run: () =>
493
+ at(mk('clean', { 'agents/roadmaps/r.md': '## Phase 1\n- [ ] **1.1 x**\n' }), ['--lint']),
494
+ },
495
+ {
496
+ name: 'a VALID OPEN window passes --lint — normal CI stays green on it',
497
+ expect: 'accept',
498
+ run: () => at(mk('valid', { 'agents/roadmaps/r.md': hold('x', ' ') }), ['--lint']),
499
+ },
500
+ ];
501
+ return runSelfTest({ gate: 'check_release_holds', cases, minCases: 6, minRejectCases: 4 });
502
+ }
503
+
504
+ function usage(): number {
505
+ process.stderr.write(
506
+ 'usage: check_release_holds (--lint | --status | --require-safe [--channel latest|all]\n' +
507
+ ' | --can-move <file> <archive|skipped|later>\n' +
508
+ ' | --selftest | --self-test) [--root <dir>]\n' +
509
+ ' --channel all (default) a stable X.Y.Z cut — every open hold refuses\n' +
510
+ ' --channel latest a -next.N prerelease — only `Channel: all` holds refuse\n',
511
+ );
512
+ return 2;
513
+ }
514
+
515
+ function main(argv: string[] = process.argv.slice(2)): number {
516
+ const ri = argv.indexOf('--root');
517
+ const root = ri === -1 ? REPO_ROOT : path.resolve(argv[ri + 1] ?? '.');
518
+ // `--self-test` is the house convention (`_lib/gate_self_test`): it drives
519
+ // this CLI against fixture trees on disk and asserts the exit code, so what
520
+ // it proves is that the BINARY still rejects. `--selftest` is the unit-level
521
+ // state-table suite. They are complementary, not duplicates -- one can pass
522
+ // while the other fails, which is the whole reason both exist.
523
+ if (argv.includes('--self-test') && process.env['GATE_SELF_TEST_CHILD'] !== '1') {
524
+ return cmdConventionSelfTest();
525
+ }
526
+ if (argv.includes('--selftest')) {
527
+ return cmdSelftest();
528
+ }
529
+ if (argv.includes('--lint')) {
530
+ return cmdLint(root);
531
+ }
532
+ if (argv.includes('--status')) {
533
+ return cmdStatus(root);
534
+ }
535
+ if (argv.includes('--require-safe')) {
536
+ const i = argv.indexOf('--channel');
537
+ const raw = i === -1 ? 'all' : argv[i + 1];
538
+ if (raw !== 'all' && raw !== 'latest') {
539
+ process.stderr.write(`❌ --channel must be \`all\` or \`latest\`, got \`${raw ?? ''}\`\n`);
540
+ return usage();
541
+ }
542
+ return cmdRequireSafe(root, raw);
543
+ }
544
+ const mv = argv.indexOf('--can-move');
545
+ if (mv !== -1) {
546
+ const file = argv[mv + 1];
547
+ const dest = argv[mv + 2];
548
+ if (file === undefined || dest === undefined) {
549
+ return usage();
550
+ }
551
+ return cmdCanMove(path.resolve(file), dest);
552
+ }
553
+ return usage();
554
+ }
555
+
556
+ function _isCliEntry(): boolean {
557
+ if (process.argv[1] === undefined) {
558
+ return false;
559
+ }
560
+ return import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
561
+ }
562
+
563
+ if (_isCliEntry() || process.argv[1] === _HERE) {
564
+ process.exit(main());
565
+ }
566
+
567
+ export { collect, globRoadmaps, main, HOLD_DIRS };
@@ -57,7 +57,7 @@ function _package_name(): string {
57
57
  *
58
58
  * This probe used to answer local-OR-remote, and that made the gate reply
59
59
  * "tagged, therefore published" to the exact state it exists to detect. A tag
60
- * created by `release.ts` step 8 whose push then failed sits in the local
60
+ * created by `release.ts` step 9 whose push then failed sits in the local
61
61
  * repository while nothing has shipped: `publish-npm.yml` triggers on
62
62
  * `push: tags:`, so a tag that never reached the remote published nothing.
63
63
  * Measured 2026-08-20 on 14.6.0 — main carried `package.json` 14.6.0, the tag