@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,247 @@
1
+ /**
2
+ * The runtime activation payload — what a trigger match charges per fire.
3
+ *
4
+ * The third of the three numbers `road-to-the-delivery-flip-that-tells-the-truth`
5
+ * Phase 3 separates. The other two already have names and a record: ADR-270
6
+ * calls them `source_corpus` (what the source tree could deliver) and
7
+ * `host_payload` (what one host actually receives at rest), and requires them to
8
+ * be structurally separate fields because "until the two are named apart, every
9
+ * ceiling discussion risks comparing unlike quantities". After a per-host
10
+ * projection flip a third quantity diverges from both: the bytes a delivery
11
+ * concern injects when a rule's trigger fires. A standing reduction published
12
+ * without it is a saving published without its cost.
13
+ *
14
+ * MEASURED THROUGH THE CONCERN'S OWN SELECTION, never a second model of it.
15
+ * `matchTierRules` + `selectForInjection` are the functions the runtime concern
16
+ * calls; an offline figure computed by a different matcher would price a set the
17
+ * concern does not deliver. That is the same single-definition rule
18
+ * `_lib/rule_injection.ts` was written for, applied one layer out.
19
+ *
20
+ * THE CAP IS A PARAMETER, not an import. The value lives on the concern
21
+ * (`hooks/rule_inject_hook.ts::CAP_BYTES`) and every caller passes it, so this
22
+ * module stays free of the hook's import closure and can be read from a gate, a
23
+ * report, or a bundled path without dragging a hook into it.
24
+ *
25
+ * Moved here from `report_standing_payload_by_host.ts`, which had the only
26
+ * implementation and is still its largest caller — same move, and for the same
27
+ * reason, as `censusRuleDir` into `_lib/carrier_divergence.ts`: a second reader
28
+ * needed it and a second copy is how two numbers start disagreeing.
29
+ */
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+
33
+ import { loadRouter, matchTierRules, selectForInjection } from './rule_injection.js';
34
+
35
+ /** The frozen corpus every fire figure in this module is sampled from. */
36
+ export const ROUTING_MATRIX_REL = path.join('tests', 'eval', 'routing-matrix');
37
+
38
+ /** One positive prompt of the frozen corpus, with the open files it declares. */
39
+ export interface CorpusPrompt {
40
+ prompt: string;
41
+ openFiles: string[] | null;
42
+ }
43
+
44
+ /**
45
+ * Every YAML shape inside `positives:` this hand parser cannot read.
46
+ *
47
+ * `readCorpusPositives` accepts only `- prompt: "…"` with a double-quoted
48
+ * same-line scalar and an inline `open_files: [...]`. Any other legal shape —
49
+ * single quotes, an unquoted scalar, a folded block, a block sequence for
50
+ * `open_files` — is SILENTLY SKIPPED, which drops fires out of the distribution
51
+ * a budget row is derived from. A measurement that governs a budget must fail
52
+ * loudly on a shape it cannot read rather than under-count, so callers refuse on
53
+ * a non-empty result here.
54
+ *
55
+ * Reported as a shape problem rather than fixed by widening the parser: this
56
+ * module must not grow a second YAML implementation, and the corpus is frozen,
57
+ * so the honest answer to an unreadable shape is to say which file carries it.
58
+ */
59
+ export function corpusShapeProblems(root: string): string[] {
60
+ const dir = path.join(root, ROUTING_MATRIX_REL);
61
+ const problems: string[] = [];
62
+ if (!fs.existsSync(dir)) return problems;
63
+ for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
64
+ let inPositives = false;
65
+ let lineNo = 0;
66
+ for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
67
+ lineNo += 1;
68
+ if (/^positives:/u.test(raw)) { inPositives = true; continue; }
69
+ if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
70
+ if (!inPositives) continue;
71
+ if (/^\s*-\s*prompt:/u.test(raw) && !/^\s*-\s*prompt:\s*"(.*)"\s*$/u.test(raw)) {
72
+ problems.push(
73
+ `${f}:${String(lineNo)}: a \`- prompt:\` this parser cannot read — only a ` +
74
+ `double-quoted same-line scalar is accepted: ${JSON.stringify(raw.trim())}`,
75
+ );
76
+ continue;
77
+ }
78
+ if (/^\s*-\s*/u.test(raw) && !/^\s*-\s*(prompt|command|open_files):/u.test(raw)
79
+ && raw.trim() !== '-' && raw.trim() !== '') {
80
+ // A bare sequence item under `positives:` is a shape with no
81
+ // `prompt:` key on its first line — folded or nested.
82
+ if (!/^\s{4,}/u.test(raw)) {
83
+ problems.push(
84
+ `${f}:${String(lineNo)}: a positives entry whose first line carries no ` +
85
+ `\`prompt:\` key: ${JSON.stringify(raw.trim())}`,
86
+ );
87
+ }
88
+ }
89
+ if (/^\s*open_files:\s*$/u.test(raw)) {
90
+ problems.push(
91
+ `${f}:${String(lineNo)}: a BLOCK-sequence \`open_files:\` — only the inline ` +
92
+ '`[...]` form is read, so this entry\'s files would be dropped',
93
+ );
94
+ }
95
+ }
96
+ }
97
+ return problems;
98
+ }
99
+
100
+ /** Every positive prompt in the frozen routing-matrix corpus. Near-misses are not fires. */
101
+ export function readCorpusPositives(root: string): CorpusPrompt[] {
102
+ const dir = path.join(root, ROUTING_MATRIX_REL);
103
+ const out: CorpusPrompt[] = [];
104
+ if (!fs.existsSync(dir)) return out;
105
+ for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
106
+ let inPositives = false;
107
+ let cur: CorpusPrompt | null = null;
108
+ for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
109
+ if (/^positives:/u.test(raw)) { inPositives = true; continue; }
110
+ if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
111
+ if (!inPositives) continue;
112
+ const p = /^\s*-\s*prompt:\s*"(.*)"\s*$/u.exec(raw);
113
+ if (p !== null) {
114
+ cur = { prompt: p[1] as string, openFiles: null };
115
+ out.push(cur);
116
+ continue;
117
+ }
118
+ const of = /^\s*open_files:\s*\[(.*)\]\s*$/u.exec(raw);
119
+ if (of !== null && cur !== null) {
120
+ cur.openFiles = (of[1] as string)
121
+ .split(',')
122
+ .map((x) => x.trim().replace(/^["']|["']$/gu, ''))
123
+ .filter((x) => x !== '');
124
+ }
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+
130
+ /** Nearest-rank percentile over an unsorted sample. Empty sample → 0. */
131
+ export function percentile(xs: readonly number[], q: number): number {
132
+ if (xs.length === 0) return 0;
133
+ const s = [...xs].sort((a, b) => a - b);
134
+ return s[Math.min(s.length - 1, Math.floor(q * (s.length - 1)))] as number;
135
+ }
136
+
137
+ export interface SlotFireSizes {
138
+ readonly slot: string;
139
+ readonly fires: number;
140
+ readonly p50: number;
141
+ readonly p90: number;
142
+ readonly max: number;
143
+ }
144
+
145
+ /**
146
+ * Gate-open per-fire emission sizes for the delivery concern, per slot.
147
+ *
148
+ * `pre_compact` is 0 by construction rather than by measurement: that branch of
149
+ * `rule_inject_hook` clears the seen-set and returns allow without writing to
150
+ * stdout, so there is no emission to sample.
151
+ */
152
+ export function slotFireSizes(root: string, capBytes: number): SlotFireSizes[] {
153
+ const router = loadRouter(root);
154
+ const prompts = readCorpusPositives(root);
155
+ const sample = (mode: 'prompt' | 'file'): number[] => {
156
+ const out: number[] = [];
157
+ for (const r of prompts) {
158
+ if (mode === 'file' && (r.openFiles === null || r.openFiles.length === 0)) continue;
159
+ const matches =
160
+ mode === 'prompt'
161
+ ? matchTierRules(router, r.prompt, null, null)
162
+ : matchTierRules(router, '', r.openFiles, null);
163
+ if (matches.length === 0) continue;
164
+ const sel = selectForInjection(root, matches, capBytes);
165
+ if (sel.selected.length > 0) out.push(sel.bytes);
166
+ }
167
+ return out;
168
+ };
169
+ const row = (slot: string, xs: number[]): SlotFireSizes => ({
170
+ slot,
171
+ fires: xs.length,
172
+ p50: percentile(xs, 0.5),
173
+ p90: percentile(xs, 0.9),
174
+ max: xs.length === 0 ? 0 : Math.max(...xs),
175
+ });
176
+ return [
177
+ row('user_prompt_submit', sample('prompt')),
178
+ row('pre_tool_use', sample('file')),
179
+ row('pre_compact', []),
180
+ ];
181
+ }
182
+
183
+ /** The third labelled number, or the reason it could not be taken on this tree. */
184
+ export interface ActivationPayload {
185
+ /** Always the literal `activation-payload` — the label a reader and a caller key on. */
186
+ readonly scope: 'activation-payload';
187
+ /** `false` when the corpus or the router is absent; every figure is then 0. */
188
+ readonly measured: boolean;
189
+ /** Why it is unmeasured, or `null`. Never an estimate in place of a number. */
190
+ readonly unmeasured_reason: string | null;
191
+ /** The cap the sampled selection was taken at — the concern's `CAP_BYTES`. */
192
+ readonly cap_bytes: number;
193
+ readonly corpus: string;
194
+ readonly slots: readonly SlotFireSizes[];
195
+ readonly basis: string;
196
+ }
197
+
198
+ /**
199
+ * The runtime activation payload, or an honestly unmeasured reading.
200
+ *
201
+ * Never throws and never estimates: a missing corpus or router yields
202
+ * `measured: false` with the reason named, because a fabricated distribution
203
+ * published beside a standing saving is the exact defect this number exists to
204
+ * prevent.
205
+ */
206
+ export function measureActivationPayload(root: string, capBytes: number): ActivationPayload {
207
+ const basis =
208
+ 'Gate-open per-fire emission of the `rule-inject` concern over the frozen ' +
209
+ '`tests/eval/routing-matrix` positives, measured through the same ' +
210
+ '`matchTierRules` + `selectForInjection` the runtime concern calls, at the ' +
211
+ 'concern\'s own CAP_BYTES. Bytes per fire, never tokens — the unit ' +
212
+ '`hook-token-budget.json` enforces.';
213
+ const empty = (reason: string): ActivationPayload => ({
214
+ scope: 'activation-payload',
215
+ measured: false,
216
+ unmeasured_reason: reason,
217
+ cap_bytes: capBytes,
218
+ corpus: ROUTING_MATRIX_REL,
219
+ slots: [],
220
+ basis,
221
+ });
222
+ if (!fs.existsSync(path.join(root, ROUTING_MATRIX_REL))) {
223
+ return empty(`the frozen corpus ${ROUTING_MATRIX_REL} is absent under ${root}`);
224
+ }
225
+ const shape = corpusShapeProblems(root);
226
+ if (shape.length > 0) {
227
+ return empty(
228
+ `${String(shape.length)} corpus entry/entries are in a shape the reader cannot parse, ` +
229
+ `so the distribution would be under-counted — first: ${shape[0] as string}`,
230
+ );
231
+ }
232
+ let slots: SlotFireSizes[];
233
+ try {
234
+ slots = slotFireSizes(root, capBytes);
235
+ } catch (err) {
236
+ return empty(`the fire sample could not be taken: ${(err as Error).message}`);
237
+ }
238
+ return {
239
+ scope: 'activation-payload',
240
+ measured: true,
241
+ unmeasured_reason: null,
242
+ cap_bytes: capBytes,
243
+ corpus: ROUTING_MATRIX_REL,
244
+ slots,
245
+ basis,
246
+ };
247
+ }
@@ -53,6 +53,7 @@ import { fileURLToPath } from 'node:url';
53
53
 
54
54
  import * as user_global_paths from './user_global_paths.js';
55
55
  import { carveOutKeys } from '../../shared/settingsCarveOut.js';
56
+ import { applyRenamedKeys } from './settings_renamed_keys.js';
56
57
  import type * as YamlModule from 'yaml';
57
58
 
58
59
  // ESM has no `require`; `createRequire(import.meta.url)` restores it so the
@@ -956,9 +957,9 @@ export function load_agent_settings(
956
957
  // precedence here is the inverse of the server family's, so the order of
957
958
  // these three statements is the whole contract.
958
959
  const merged: SettingsDict = template_defaults(template_path ?? undefined);
959
- _deep_merge(merged, user_global_filtered);
960
+ _deep_merge(merged, applyRenamedKeys(user_global_filtered));
960
961
  for (const p of cascade) {
961
- const layer = _read_yaml(p) ?? {};
962
+ const layer = applyRenamedKeys(_read_yaml(p) ?? {});
962
963
  if (Object.keys(layer).length > 0) {
963
964
  _deep_merge(merged, layer);
964
965
  }
@@ -967,6 +968,7 @@ export function load_agent_settings(
967
968
  return merged;
968
969
  }
969
970
 
971
+
970
972
  /**
971
973
  * Settings keys this package has DELETED. A leftover value from an older
972
974
  * install is ignored — never applied by any reader — and surfaced with ONE
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The strictest path, reserved for authority —
3
+ * `road-to-adversarial-verification-and-long-runs` 11.1.
4
+ *
5
+ * An authority-expanding change takes four independent passes — an independent
6
+ * TEST author, an independent GOVERNANCE reviewer, a COUNCIL pass, and a
7
+ * PROVIDER-DIVERSE reviewer — plus the ratification artefact. Until the artefact
8
+ * carries `verdict: ratified`, the new authority is **inert**.
9
+ *
10
+ * ```
11
+ * NEW AUTHORITY IS INERT UNTIL RATIFIED. AN AGENT MAY AUTHOR THE CHANGE
12
+ * AND MAY NEVER RATIFY ITS OWN INCREASE IN POWER (ADR-268 § 4).
13
+ * FOUR PASSES, ALL FOUR, AND THEY MAY NOT BE THE SAME REVIEWER TWICE.
14
+ * ```
15
+ *
16
+ * **Inert-until-ratified is the property that makes the rest safe.** Without it
17
+ * the four passes are a process an author could complete and then merge; with it
18
+ * the change can land, be read, and still do nothing — so the ratification is a
19
+ * separate act on a separate turn by a party that is not the author.
20
+ *
21
+ * Pure.
22
+ */
23
+
24
+ /** The four passes. All four, and the list is closed. */
25
+ export const REQUIRED_PASSES = [
26
+ 'independent-test-author',
27
+ 'independent-governance-reviewer',
28
+ 'council',
29
+ 'provider-diverse-reviewer',
30
+ ] as const;
31
+ export type Pass = (typeof REQUIRED_PASSES)[number];
32
+
33
+ export interface PassRecord {
34
+ readonly pass: Pass;
35
+ /** Who performed it. Two passes by the same identity is not two passes. */
36
+ readonly by: string;
37
+ /** The provider, so provider-diversity is checkable rather than asserted. */
38
+ readonly provider: string;
39
+ }
40
+
41
+ export interface RatificationArtefact {
42
+ /** Anything other than `ratified` leaves the authority inert. */
43
+ readonly verdict: string;
44
+ readonly passes: readonly PassRecord[];
45
+ /** The author of the change under ratification. */
46
+ readonly author: string;
47
+ }
48
+
49
+ export type AuthorityState = 'inert' | 'active';
50
+
51
+ export interface AuthorityVerdict {
52
+ readonly state: AuthorityState;
53
+ readonly blockers: readonly string[];
54
+ }
55
+
56
+ /**
57
+ * Is the new authority active yet?
58
+ *
59
+ * Returns EVERY blocker rather than the first: the artefact is a checklist a
60
+ * human completes, and handing them one item at a time turns four passes into
61
+ * four round trips.
62
+ */
63
+ export function authorityState(a: RatificationArtefact | null): AuthorityVerdict {
64
+ if (a === null) {
65
+ return { state: 'inert', blockers: ['no ratification artefact exists'] };
66
+ }
67
+ const blockers: string[] = [];
68
+
69
+ for (const required of REQUIRED_PASSES) {
70
+ if (!a.passes.some((p) => p.pass === required)) {
71
+ blockers.push(`missing pass: ${required}`);
72
+ }
73
+ }
74
+
75
+ // The author may perform NONE of the four. This is the ADR-268 § 4 clause
76
+ // and the one a process can satisfy on paper while violating in substance.
77
+ for (const p of a.passes) {
78
+ if (p.by === a.author) {
79
+ blockers.push(`${p.pass} was performed by the author (${p.by}) — an agent may not ratify its own increase in power`);
80
+ }
81
+ }
82
+
83
+ // Provider diversity is a property of the SET, so it is checked over the set
84
+ // rather than trusted from the pass that carries the name.
85
+ const providers = new Set(a.passes.map((p) => p.provider));
86
+ if (a.passes.length > 1 && providers.size < 2) {
87
+ blockers.push(
88
+ `every pass ran on one provider (${[...providers].join(', ')}) — provider-diverse means at least two`,
89
+ );
90
+ }
91
+
92
+ if (a.verdict !== 'ratified') {
93
+ blockers.push(`verdict is "${a.verdict}", not "ratified"`);
94
+ }
95
+
96
+ return { state: blockers.length === 0 ? 'active' : 'inert', blockers };
97
+ }
98
+
99
+ /** `true` only for an artefact that clears everything. */
100
+ export function isRatified(a: RatificationArtefact | null): boolean {
101
+ return authorityState(a).state === 'active';
102
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The cascade base — `road-to-adversarial-verification-and-long-runs` 5.1.
3
+ *
4
+ * `check_branch_freshness` asks whether a branch is behind THE BASE ITS PR
5
+ * TARGETS. That is one hop, and it is the right question for the common case
6
+ * where the base is the trunk. On a stacked PR it is only half of it: a branch
7
+ * targeting `feat/parent` can be perfectly current with `feat/parent` while
8
+ * `feat/parent` itself is fifty commits behind `main`, so the branch is stale
9
+ * against the tree it will eventually land in and every gate reports green.
10
+ *
11
+ * The cascade is the ordered list of refs a branch must be current with, nearest
12
+ * first. Merging in that order is not arbitrary: taking the trunk first would
13
+ * pull trunk commits past the parent and make the stack's own diff unreadable,
14
+ * which is the thing a stack exists to avoid.
15
+ *
16
+ * **Depth is bounded and the bound is deliberate.** A cascade is a chain of
17
+ * bases, and a chain can cycle — a branch whose PR targets a branch that targets
18
+ * it. `MAX_HOPS` stops the walk and the result says it was truncated, because
19
+ * a hop list silently cut short is indistinguishable from a short one.
20
+ *
21
+ * Pure: the caller supplies the base-of lookup, so the whole thing is testable
22
+ * without a forge.
23
+ */
24
+
25
+ /** How many bases a cascade may chain through before the walk stops. */
26
+ export const MAX_HOPS = 8;
27
+
28
+ export interface Cascade {
29
+ /** Refs to merge in, nearest base first, trunk last. Never includes `branch`. */
30
+ readonly hops: readonly string[];
31
+ /** True when the walk stopped on a cycle rather than on the trunk. */
32
+ readonly cycle: boolean;
33
+ /** True when the walk stopped on {@link MAX_HOPS}. */
34
+ readonly truncated: boolean;
35
+ }
36
+
37
+ /** `null` when the ref has no PR base — i.e. it is a trunk or is unknown. */
38
+ export type BaseOf = (ref: string) => string | null;
39
+
40
+ /**
41
+ * Walk from `branch` to `trunk`, collecting every base on the way.
42
+ *
43
+ * A branch whose base IS the trunk yields one hop — identical to the single-hop
44
+ * behaviour that existed before, which is the compatibility property that
45
+ * matters: this widens the check, it does not change the common case.
46
+ */
47
+ export function cascadeBase(branch: string, trunk: string, baseOf: BaseOf): Cascade {
48
+ const hops: string[] = [];
49
+ const seen = new Set<string>([branch]);
50
+ let current = branch;
51
+
52
+ for (let i = 0; i < MAX_HOPS; i += 1) {
53
+ const base = baseOf(current);
54
+ if (base === null || base === '') {
55
+ // No PR base. The trunk is still owed unless we are already on it.
56
+ if (current !== trunk && !hops.includes(trunk)) hops.push(trunk);
57
+ return { hops, cycle: false, truncated: false };
58
+ }
59
+ if (seen.has(base)) {
60
+ return { hops, cycle: true, truncated: false };
61
+ }
62
+ seen.add(base);
63
+ hops.push(base);
64
+ if (base === trunk) {
65
+ return { hops, cycle: false, truncated: false };
66
+ }
67
+ current = base;
68
+ }
69
+ return { hops, cycle: false, truncated: true };
70
+ }
71
+
72
+ /** One ref's freshness, as the caller measured it. */
73
+ export interface HopReading {
74
+ readonly ref: string;
75
+ /** `true` behind · `false` current · `null` not measured. */
76
+ readonly behind: boolean | null;
77
+ }
78
+
79
+ /**
80
+ * Which hops still need merging, in order.
81
+ *
82
+ * An UNMEASURED hop is returned alongside the behind ones rather than skipped:
83
+ * "we could not tell" and "it is current" are different states, and treating the
84
+ * first as the second is how a single-hop check reported green on a stale stack
85
+ * in the first place.
86
+ */
87
+ export function hopsNeedingMerge(readings: readonly HopReading[]): HopReading[] {
88
+ return readings.filter((r) => r.behind !== false);
89
+ }
90
+
91
+ /** True only when every hop was measured and every hop is current. */
92
+ export function cascadeCurrent(readings: readonly HopReading[]): boolean {
93
+ return readings.length > 0 && readings.every((r) => r.behind === false);
94
+ }
@@ -231,8 +231,13 @@ export function archive_file_header(boundary: string): string {
231
231
  '> `tests/test_changelog_eras.py`.\n' +
232
232
  '>\n' +
233
233
  '> **Read-only.** New entries land in `CHANGELOG.md`. Entries\n' +
234
- '> here are not amended — git tags remain the canonical source\n' +
235
- '> for what shipped.\n' +
234
+ '> here are not rewritten — git tags remain the canonical source\n' +
235
+ '> for what shipped. The one amendment an archive permits is a\n' +
236
+ '> **correction that adds**: where a published entry says something\n' +
237
+ '> the shipped tree contradicts, the correction is appended beside\n' +
238
+ '> it and names the commit that decided the matter. Nothing is\n' +
239
+ '> deleted, so both the original claim and its correction stay\n' +
240
+ '> readable.\n' +
236
241
  '>\n' +
237
242
  '> Entry shape follows\n' +
238
243
  '> [`../contracts/CHANGELOG-conventions.md`](../contracts/CHANGELOG-conventions.md).\n' +
@@ -60,6 +60,122 @@ export const HALT_ACTIONS: readonly LadderAction[] = [
60
60
  'halt-premise-invalidated',
61
61
  ];
62
62
 
63
+ /**
64
+ * The delivery state machine — `road-to-adversarial-verification-and-long-runs`
65
+ * Phase 8.1, in the order a run walks it.
66
+ *
67
+ * A run's checkboxes and its DELIVERY are two different completions, and until
68
+ * now only the first one could end a run. That is the defect: a run whose
69
+ * roadmap reads zero-open while its PR sits on red CI, or on a target that moved
70
+ * underneath it, reported `complete` — a completion claim about work whose only
71
+ * evidence (the CI verdict on the final head) says otherwise.
72
+ *
73
+ * Two of these are ENDINGS and the rest are not:
74
+ * · `merged` — a grant existed and was spent.
75
+ * · `open-green` — no grant; the PR is open, its CI is green on the head CI
76
+ * actually observed, and the run says so. This is a success, not a
77
+ * shortfall: a run without a merge grant is not supposed to merge.
78
+ * Every earlier position means work remains, whatever the checkboxes read.
79
+ */
80
+ export const DELIVERY_STATES = [
81
+ 'working',
82
+ 'local-green',
83
+ 'pushed',
84
+ 'pr-open',
85
+ 'ci-pending',
86
+ 'ci-red',
87
+ 'target-sync-check',
88
+ 'target-moved',
89
+ 'delivery-ready',
90
+ 'merged',
91
+ 'open-green',
92
+ ] as const;
93
+ export type DeliveryState = (typeof DELIVERY_STATES)[number];
94
+
95
+ /** The two positions at which a run has actually delivered. */
96
+ export const DELIVERY_ENDINGS: readonly DeliveryState[] = ['merged', 'open-green'];
97
+
98
+ /**
99
+ * True when this delivery position means work remains.
100
+ *
101
+ * `null` — nothing recorded a position — is NOT incomplete. The ladder runs on
102
+ * the Stop path, where a `gh` probe is not affordable, so the position is READ
103
+ * from what the run wrote rather than measured here. An unrecorded position
104
+ * therefore means *this run does not report delivery*, and inventing
105
+ * incompleteness from that absence would hang every run that never adopted the
106
+ * field. Fail-open, in the one direction that cannot manufacture a stall.
107
+ */
108
+ export function deliveryBlocksCompletion(state: DeliveryState | null | undefined): boolean {
109
+ if (state === null || state === undefined) return false;
110
+ return !DELIVERY_ENDINGS.includes(state);
111
+ }
112
+
113
+ /** The facts one terminal ledger line records about a run. */
114
+ export interface LedgerEventInput {
115
+ readonly action: LadderAction;
116
+ readonly runId: string;
117
+ readonly slug: string;
118
+ readonly turn: number;
119
+ readonly iterations: number;
120
+ readonly open: number;
121
+ readonly blocked: number;
122
+ readonly delivery: DeliveryState | null;
123
+ }
124
+
125
+ /**
126
+ * Build the terminal ledger line for a run.
127
+ *
128
+ * Extracted from `hooks/run_continuation_hook.ts` by
129
+ * `road-to-adversarial-verification-and-long-runs` 8.1, for the reason this
130
+ * module's own header gives: the hook sits past `check_source_size_budget`'s
131
+ * 1,500-line cap, where every added line is an added violation, so a phase pays
132
+ * for its additions by moving the code it is editing to a file under the cap.
133
+ * The `delivery` field is that addition.
134
+ *
135
+ * A ledger line is a persisted shape carrying a `RunTerminalState`, so it names
136
+ * the vocabulary version it was written against: a reader meeting an unknown
137
+ * value can then tell a newer writer from a corrupt row. `delivery` is OMITTED
138
+ * when the run reported no position rather than written as a null — `complete`
139
+ * alone cannot tell a merged run from an open-green one, and a null would read
140
+ * as "neither" rather than "unreported".
141
+ */
142
+ export function buildLedgerEvent(
143
+ input: LedgerEventInput,
144
+ vocabularyVersion: number,
145
+ now: Date,
146
+ ): Record<string, unknown> {
147
+ return {
148
+ event: input.action,
149
+ run_id: input.runId,
150
+ roadmap: input.slug,
151
+ turn: input.turn,
152
+ iterations: input.iterations,
153
+ open: input.open,
154
+ blocked: input.blocked,
155
+ terminal_state: terminalStateFor(input.action),
156
+ terminal_vocabulary_version: vocabularyVersion,
157
+ ...(input.delivery !== null ? { delivery: input.delivery } : {}),
158
+ at: now.toISOString(),
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Read a persisted delivery position tolerantly — `undefined` for anything this
164
+ * build does not recognise.
165
+ *
166
+ * Same forward-compatibility shape as {@link parseHaltStamp}, and the opposite
167
+ * fail direction on purpose: an unknown HALT is preserved, because dropping it
168
+ * re-engages a deliberately ended run; an unknown DELIVERY position is dropped,
169
+ * because preserving it would hold a run open on a state this build cannot
170
+ * evaluate. Both choose the branch that cannot manufacture a stall.
171
+ */
172
+ export function parseDeliveryState(raw: unknown): DeliveryState | undefined {
173
+ if (typeof raw !== 'string') return undefined;
174
+ return (DELIVERY_STATES as readonly string[]).includes(raw)
175
+ ? (raw as DeliveryState)
176
+ : undefined;
177
+ }
178
+
63
179
  /**
64
180
  * The subset of a run's state this decision reads. The hook's `RunState`
65
181
  * satisfies it structurally; nothing here may widen beyond what the rungs use.
@@ -128,6 +244,10 @@ export function ladder(
128
244
  // `_lib/context_observation.premiseMoved`. Optional and defaulted false, so
129
245
  // a caller that cannot observe the world decides exactly as it did before.
130
246
  premiseInvalidated: boolean = false,
247
+ // `road-to-adversarial-verification-and-long-runs` 8.1. The delivery position
248
+ // the run RECORDED, or `null` where it recorded none. Optional and defaulted,
249
+ // so a caller that does not track delivery decides exactly as it did before.
250
+ delivery: DeliveryState | null = null,
131
251
  ): LadderAction {
132
252
  // A halt is terminal for this run id. Checked BEFORE `complete` so a
133
253
  // halted run whose roadmap later reads zero-open does not report a
@@ -136,7 +256,26 @@ export function ladder(
136
256
  // `scanOpenSteps` EXCLUDES `blocked-by:` steps from `openCount`, so zero-open
137
257
  // with blocked steps left is exhaustion of runnable work, not completion —
138
258
  // ADR-235's own terminal outcome, and never a sixth halt.
139
- if (openCount === 0) return blockedCount > 0 ? 'blocked' : 'complete';
259
+ //
260
+ // 8.1 — delivery is checked INSIDE the zero-open branch and only against
261
+ // `complete`. Two boundaries, both deliberate. It does not touch `blocked`:
262
+ // a run whose remaining work is blocked has not failed to deliver, it has
263
+ // run out of deliverable work, and holding it open on a PR it cannot advance
264
+ // would convert a nameable blocker into a stall. And it is not checked while
265
+ // steps are still open, because there the run continues anyway — adding a
266
+ // second reason to continue would only make the ledger's `open` count lie
267
+ // about why.
268
+ const deliveringOnly =
269
+ openCount === 0 && blockedCount === 0 && deliveryBlocksCompletion(delivery);
270
+ if (openCount === 0) {
271
+ if (blockedCount > 0) return 'blocked';
272
+ if (!deliveringOnly) return 'complete';
273
+ // Fall THROUGH to the budget rungs rather than returning `engage` here.
274
+ // Returning early would put the run outside every bound in this function
275
+ // — an unbounded loop, which is the failure the ladder exists against —
276
+ // so a run held open for delivery is still capped by iterations and the
277
+ // wall clock. Only the STALL rung is exempted, below.
278
+ }
140
279
  // BEFORE the counter rungs, deliberately: a missing credential, an absent
141
280
  // binary or an exhausted quota is not closed by iterating, so spending the
142
281
  // budget on it converts a nameable blocker into an anonymous cap-out.
@@ -151,8 +290,16 @@ export function ladder(
151
290
  if (Number.isFinite(started) && nowMs - started >= caps.wallClockMs) {
152
291
  return 'halt-wall-clock';
153
292
  }
293
+ // The stall rung reads OPEN-STEP progress, and during delivery open-step
294
+ // progress is definitionally zero: the checkboxes are all flipped and the
295
+ // remaining work is a CI red or a moved target. Applying it here would halt
296
+ // a healthy delivery loop on its third turn — a stall manufactured by the
297
+ // stall detector, which is exactly the failure `run_continuation_hook`'s own
298
+ // header warns about in the abstract and the one this file's mechanics call
299
+ // "a metric that stops moving because the MEASUREMENT broke". The iteration
300
+ // and wall-clock caps above still bound the loop; only this rung is skipped.
154
301
  const tail = state.history.slice(-caps.stallWindow);
155
- if (tail.length >= caps.stallWindow && tail.every((n) => n === openCount)) {
302
+ if (!deliveringOnly && tail.length >= caps.stallWindow && tail.every((n) => n === openCount)) {
156
303
  return 'halt-stall';
157
304
  }
158
305
  return 'engage';