@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,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 };
@@ -6,22 +6,32 @@
6
6
  * WHY THIS GATE EXISTS (road-to-conformance-round5 Phase 1, measured 2026-08-07)
7
7
  * ---------------------------------------------------------------------------
8
8
  * `docs/contracts/rule-router.md` (§ "The router earns its place as a
9
- * **compile-time** artifact") states that under every shipped default nothing
10
- * loads `dist/router.json` at runtime, so **projection is the only reach
11
- * mechanism there is**:
9
+ * **compile-time** artifact") used to state that under every shipped default
10
+ * nothing loads `dist/router.json` at runtime, so **projection is the only
11
+ * reach mechanism there is**. Both halves of that have since been corrected,
12
+ * and the gate outlived both:
12
13
  *
13
14
  * CORRECTED 2026-08-26. This docblock used to cite that section's "measured:
14
15
  * zero consumers under `src/scripts/hooks/`", and that measurement is stale: the
15
16
  * `rule-inject` concern reads the router via `_lib/rule_injection.ts:76-79` and
16
- * is bound on three slots. The gate's rationale survives the correction because
17
- * the concern is DEFAULT-OFF and returns before reading the router unless
18
- * `lean_projection.mode: delivery` is set — so on a shipped default, projection
19
- * IS still the only reach mechanism. What no longer holds is the stronger claim
20
- * that no such mechanism exists. Stated here rather than quietly re-worded,
21
- * because a gate whose stated reason rests on a refuted premise is exactly the
22
- * decoration ADR-127 rejects: a non-kernel rule activates by the model's judgment over text
23
- * already in context, and the per-tool rule tree is what puts it there. That
24
- * makes the state of the projection load-bearing, and it was broken:
17
+ * is bound on three slots. What no longer holds is the stronger claim that no
18
+ * such mechanism exists.
19
+ *
20
+ * CORRECTED AGAIN 2026-09-12. The 2026-08-26 note rescued the rationale with
21
+ * "the concern is DEFAULT-OFF … so on a shipped default, projection IS still
22
+ * the only reach mechanism". That rescue is now false and the clause is gone:
23
+ * since ADR-267 the shipped template carries `lean_projection.mode: delivery`
24
+ * with `hosts: [claude-code]`, so on the shipped default that one host does
25
+ * reach rule bodies at runtime. The gate never depended on the rescue — its two
26
+ * assertions below are about the emit plan and the entry mtimes, and a thinned
27
+ * tree of stubs is exactly as load-bearing as a tree of bodies: a missing entry
28
+ * is a rule the model neither reads nor has a pointer to, in either mode.
29
+ *
30
+ * Both are stated rather than quietly re-worded, because a gate whose stated
31
+ * reason rests on a refuted premise is exactly the decoration ADR-127 rejects:
32
+ * a non-kernel rule activates by the model's judgment over text already in
33
+ * context, and the per-tool rule tree is what puts it there. That makes the
34
+ * state of the projection load-bearing, and it was broken:
25
35
  *
26
36
  * - `.claude/rules/` in the working checkout held **92** entries. A
27
37
  * regeneration in a clean worktree produces **108**. The tree was last