@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
@@ -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';
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Council transport and cost — `road-to-adversarial-verification-and-long-runs`
3
+ * 10.1.
4
+ *
5
+ * The posture is CLI → CLI quota exhausted → API within the ceiling → API over
6
+ * the ceiling → **pause and report**. The last rung is the step's whole point:
7
+ * over the ceiling the run does not ask whether to buy more technical API usage.
8
+ * It reports.
9
+ *
10
+ * ```
11
+ * AN OVER-CEILING COUNCIL REQUIREMENT PRODUCES A REPORT, NEVER A QUESTION.
12
+ * TECHNICAL API SPEND IS NOT A DECISION THE OWNER IS ASKED FOR MID-RUN.
13
+ * BUSINESS SPEND IS A TYPED OP AND A DIFFERENT CATEGORY ENTIRELY.
14
+ * ```
15
+ *
16
+ * **Why a report rather than an ask, when both interrupt.** They do not
17
+ * interrupt the same way. An ask BLOCKS: the run stops until an answer arrives,
18
+ * and the thing being asked about — a few dollars of inference — is worth less
19
+ * than the run's remaining work. A report does not block: it says what needed the
20
+ * council, why the cheaper route was unavailable, what it would cost, and — the
21
+ * load-bearing field — **what can still proceed without it**. Most of a mission
22
+ * can.
23
+ *
24
+ * Pure.
25
+ */
26
+
27
+ /** The routes, cheapest first. */
28
+ export type Route = 'cli' | 'api' | 'paused';
29
+
30
+ export interface TransportInput {
31
+ /** Is the CLI route configured and within quota? */
32
+ readonly cliAvailable: boolean;
33
+ /** Why the CLI is unavailable, when it is not. */
34
+ readonly cliUnavailableReason: string | null;
35
+ /** Estimated USD for the API route. */
36
+ readonly estimateUsd: number;
37
+ /** The run's remaining technical-spend ceiling in USD. */
38
+ readonly ceilingUsd: number;
39
+ }
40
+
41
+ export interface PauseReport {
42
+ readonly needed: string;
43
+ readonly cliUnavailableBecause: string;
44
+ readonly estimateUsd: number;
45
+ readonly ceilingUsd: number;
46
+ readonly missionState: string;
47
+ /** What the run continues with meanwhile. Empty is a real, reportable answer. */
48
+ readonly canStillProceed: readonly string[];
49
+ }
50
+
51
+ export interface TransportDecision {
52
+ readonly route: Route;
53
+ readonly reason: string;
54
+ /** Present only on `paused`. */
55
+ readonly report: PauseReport | null;
56
+ }
57
+
58
+ export interface PauseContext {
59
+ readonly needed: string;
60
+ readonly missionState: string;
61
+ readonly canStillProceed: readonly string[];
62
+ }
63
+
64
+ /**
65
+ * Route one council requirement.
66
+ *
67
+ * A ceiling of zero pauses rather than dividing by it, and an estimate exactly
68
+ * AT the ceiling is within it — the ceiling is a limit, not an exclusive bound,
69
+ * and reading it the other way would pause a run that had budgeted exactly.
70
+ */
71
+ export function routeCouncil(input: TransportInput, ctx: PauseContext): TransportDecision {
72
+ if (input.cliAvailable) {
73
+ return { route: 'cli', reason: 'the CLI route is available and within quota', report: null };
74
+ }
75
+ const why = input.cliUnavailableReason ?? 'unstated';
76
+ if (input.estimateUsd <= input.ceilingUsd) {
77
+ return {
78
+ route: 'api',
79
+ reason: `CLI unavailable (${why}); the API estimate $${input.estimateUsd.toFixed(2)} is within the $${input.ceilingUsd.toFixed(2)} ceiling`,
80
+ report: null,
81
+ };
82
+ }
83
+ return {
84
+ route: 'paused',
85
+ reason: `the API estimate $${input.estimateUsd.toFixed(2)} exceeds the $${input.ceilingUsd.toFixed(2)} ceiling`,
86
+ report: {
87
+ needed: ctx.needed,
88
+ cliUnavailableBecause: why,
89
+ estimateUsd: input.estimateUsd,
90
+ ceilingUsd: input.ceilingUsd,
91
+ missionState: ctx.missionState,
92
+ canStillProceed: ctx.canStillProceed,
93
+ },
94
+ };
95
+ }
96
+
97
+ /** The six fields a pause report must carry. */
98
+ export const REPORT_FIELDS: readonly (keyof PauseReport)[] = [
99
+ 'needed',
100
+ 'cliUnavailableBecause',
101
+ 'estimateUsd',
102
+ 'ceilingUsd',
103
+ 'missionState',
104
+ 'canStillProceed',
105
+ ];
106
+
107
+ /**
108
+ * Render the report.
109
+ *
110
+ * It ends on what can still proceed, and that placement is deliberate: a reader
111
+ * who stops after the cost has read a complaint, and a reader who reaches the
112
+ * last line has read a status. Nothing here is phrased as a question, and the
113
+ * renderer emits no `?` — asserted by a test, because the failure mode is a
114
+ * report that drifts into an ask one helpful sentence at a time.
115
+ */
116
+ export function renderReport(r: PauseReport): string {
117
+ const proceeding =
118
+ r.canStillProceed.length > 0
119
+ ? r.canStillProceed.map((x) => ` - ${x}`).join('\n')
120
+ : ' - nothing; the mission is blocked on this council pass alone';
121
+ return [
122
+ `Council paused: ${r.needed}`,
123
+ `CLI route unavailable: ${r.cliUnavailableBecause}`,
124
+ `API estimate $${r.estimateUsd.toFixed(2)} over the $${r.ceilingUsd.toFixed(2)} technical ceiling`,
125
+ `Mission state: ${r.missionState}`,
126
+ 'Proceeding meanwhile with:',
127
+ proceeding,
128
+ ].join('\n');
129
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Is a PR delivery-ready? — `road-to-adversarial-verification-and-long-runs` 3.1.
3
+ *
4
+ * The step's acceptance clause is that *a fixture PR with a disabled required
5
+ * check cannot reach delivery-ready*, and that is the one case a naive reader
6
+ * gets wrong: a PR whose required context was SKIPPED has a green rollup. GitHub
7
+ * reports `SKIPPED` as a non-failure, so a predicate that asks "did anything
8
+ * fail" answers no, and the gate the context was supposed to enforce did not run.
9
+ *
10
+ * So this asks two questions rather than one: did every required context report,
11
+ * and did each of those reports actually pass. A context that is missing from
12
+ * the rollup entirely fails the first; a context present but skipped, cancelled
13
+ * or neutral fails the second.
14
+ *
15
+ * Pure — the caller supplies the rollup and the required set, so both polarities
16
+ * are testable without a forge.
17
+ */
18
+
19
+ /** One check as a forge rollup reports it. */
20
+ export interface CheckRun {
21
+ readonly name: string;
22
+ /** `SUCCESS` · `FAILURE` · `SKIPPED` · `CANCELLED` · `NEUTRAL` · `null` while running. */
23
+ readonly conclusion: string | null;
24
+ /** `COMPLETED` · `IN_PROGRESS` · `QUEUED`. */
25
+ readonly status: string;
26
+ }
27
+
28
+ /** Why a PR is not delivery-ready. Empty means it is. */
29
+ export type DeliveryBlock =
30
+ | { readonly kind: 'required-context-absent'; readonly context: string }
31
+ | { readonly kind: 'required-context-not-passing'; readonly context: string; readonly conclusion: string }
32
+ | { readonly kind: 'still-running'; readonly context: string }
33
+ | { readonly kind: 'stale-head'; readonly observed: string; readonly head: string };
34
+
35
+ /**
36
+ * The only conclusion that counts as a required check having done its job.
37
+ *
38
+ * `SKIPPED` is deliberately NOT here, and it is the reason this module exists:
39
+ * a skipped required check is an unenforced gate wearing a green rollup.
40
+ * `NEUTRAL` and `CANCELLED` are excluded on the same ground — a check that
41
+ * reached no verdict enforced nothing, whatever colour it rendered.
42
+ */
43
+ const PASSING = 'SUCCESS';
44
+
45
+ export interface DeliveryInput {
46
+ /** Contexts the forge requires, from `forge_protection`'s ruleset read. */
47
+ readonly requiredContexts: readonly string[];
48
+ /** The rollup on the head under test. */
49
+ readonly checks: readonly CheckRun[];
50
+ /** The head CI actually observed. */
51
+ readonly observedHead: string;
52
+ /** The branch head right now. */
53
+ readonly head: string;
54
+ }
55
+
56
+ /**
57
+ * Every reason this PR is not delivery-ready, in report order.
58
+ *
59
+ * Returns ALL blocks rather than the first: a run that fixes one and re-pushes
60
+ * to discover the next has spent a CI cycle per block, which is the cost layer 1
61
+ * of the delivery contract exists to avoid paying at layer 4.
62
+ */
63
+ export function deliveryBlocks(input: DeliveryInput): DeliveryBlock[] {
64
+ const out: DeliveryBlock[] = [];
65
+ const byName = new Map(input.checks.map((c) => [c.name, c]));
66
+
67
+ for (const context of input.requiredContexts) {
68
+ const run = byName.get(context);
69
+ if (run === undefined) {
70
+ out.push({ kind: 'required-context-absent', context });
71
+ continue;
72
+ }
73
+ if (run.status !== 'COMPLETED') {
74
+ out.push({ kind: 'still-running', context });
75
+ continue;
76
+ }
77
+ if (run.conclusion !== PASSING) {
78
+ out.push({
79
+ kind: 'required-context-not-passing',
80
+ context,
81
+ conclusion: run.conclusion ?? 'null',
82
+ });
83
+ }
84
+ }
85
+
86
+ // Checked LAST and always: a green rollup on a head that is no longer the
87
+ // branch head is a verdict about a different tree. `ci_settle` names the head
88
+ // its verdict is about precisely so this comparison is possible.
89
+ if (input.observedHead !== input.head) {
90
+ out.push({ kind: 'stale-head', observed: input.observedHead, head: input.head });
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /** True only when nothing blocks. */
96
+ export function isDeliveryReady(input: DeliveryInput): boolean {
97
+ return deliveryBlocks(input).length === 0;
98
+ }
99
+
100
+ /** One human-readable line per block. */
101
+ export function renderBlock(b: DeliveryBlock): string {
102
+ switch (b.kind) {
103
+ case 'required-context-absent':
104
+ return `required check "${b.context}" did not report at all`;
105
+ case 'required-context-not-passing':
106
+ return `required check "${b.context}" concluded ${b.conclusion}, not SUCCESS — a skipped or cancelled required check is an unenforced gate, not a pass`;
107
+ case 'still-running':
108
+ return `required check "${b.context}" has not completed`;
109
+ case 'stale-head':
110
+ return `CI was observed on ${b.observed} but the branch head is ${b.head} — the verdict describes a different tree`;
111
+ }
112
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Detector E of `turn-end-gate`, extracted so its prose costs nothing.
3
+ *
4
+ * It lives here rather than in the hook for one mechanical reason and one real
5
+ * one. The mechanical one: `turn_end_gate_hook.ts` sits past the 1500-line
6
+ * source ceiling, where every line is one unit of ratchet debt, and this module
7
+ * is under it. The real one: E is the only detector that reads a LIST of the
8
+ * turn's assistant texts rather than the single closing one, so it shares no
9
+ * input with A-D and F, and its dependencies are the reply-consistency parser
10
+ * rather than anything the hook owns.
11
+ */
12
+ import {
13
+ find_option_blocks,
14
+ recommendationsUnder,
15
+ splitAndMask,
16
+ } from '../check_reply_consistency.js';
17
+ import type { RefusalDetectorId } from './turn_end_refusals.js';
18
+
19
+ /** The shape the gate collects. Kept structural so this module imports no hook. */
20
+ export interface DroppedDecisionFinding {
21
+ detector: RefusalDetectorId;
22
+ evidence: string;
23
+ reason: string;
24
+ }
25
+
26
+ /**
27
+ * Detector E — a decision put to the user in this turn, dropped before the turn
28
+ * ended.
29
+ *
30
+ * THE MEASURED FAILURE. An assistant turn put three numbered options and a
31
+ * recommendation line to the user. The `end-review-nudge` stop concern then
32
+ * produced a SECOND assistant execution with no intervening user message, and
33
+ * that second reply handled the nudge while demoting the open question to a
34
+ * subordinate clause — "bleibt davon unberührt und liegt bei Dir". From the
35
+ * user's side the conversation ended without a question. No rule stated this
36
+ * when the detector was written: `user-interaction` Iron Law 1 inspects a reply
37
+ * that HAS an options block, `check_reply_consistency` receives a draft and not
38
+ * a transcript, `active-remediation` governs findings rather than issued
39
+ * decisions, and `no-cheap-questions` guards the opposite direction. That gap
40
+ * is now closed from the other side — `user-interaction` Iron Law 3 states the
41
+ * continuity obligation, and the refusal below cites it — so this paragraph
42
+ * records why the detector exists, not a gap that is still open.
43
+ *
44
+ * WHAT IT READS. `assistantTurnTexts` — the assistant texts since the last
45
+ * GENUINE user prompt, with synthetic prompts and sidechain entries already
46
+ * filtered by `readTranscriptTail`. So a task notification, a system reminder or
47
+ * a subagent's own prompt does not read as an answer, which is the whole point:
48
+ * the failure is defined by the absence of a real user turn.
49
+ *
50
+ * WHAT IT DELIBERATELY DOES NOT DO. It does not model the decision — no id, no
51
+ * `pending → answered` lifecycle, no semantic matching of a free-text reply
52
+ * against an option. That object is grant-shaped and
53
+ * `road-to-decision-closure` owns its ownership routing; a second obligation
54
+ * ledger here would be the parallel taxonomy that roadmap exists to prevent.
55
+ * This is transcript adjacency and nothing above it.
56
+ *
57
+ * AN ASK IS A BLOCK **PLUS** ITS RECOMMENDATION LINE, and that qualifier is
58
+ * load-bearing rather than pedantic. `find_option_blocks` calls any run of two
59
+ * or more consecutive numbered lines a block — a plan, a findings list, an
60
+ * ordinary enumeration. Keyed on the block alone this detector fired on 29 of
61
+ * this repository's own 592 assistant turns, and 8 of those blocks carried no
62
+ * recommendation line anywhere near them: pure false positives on narrative
63
+ * lists, in a detector that can REFUSE a turn. `user-interaction` Iron Law 1
64
+ * already says the recommendation line is the ask, so requiring it removes that
65
+ * whole class by applying the rule this refusal cites rather than by bolting a
66
+ * heuristic beside it.
67
+ *
68
+ * The cost, stated rather than discovered: an options block whose recommendation
69
+ * line was omitted is invisible here. That reply is ALREADY a
70
+ * `check_reply_consistency` finding and a stated Iron Law 1 violation, so it is
71
+ * the malformed-ask problem rather than the dropped-ask problem — a different
72
+ * defect with a different owner.
73
+ *
74
+ * REUSE. `find_option_blocks` and `recommendationsUnder` are both the
75
+ * spec-backed readings behind `check_reply_consistency`, extracted rather than
76
+ * copied. `interruption_ledger_hook` carries its own `hasNumberedOptions` for
77
+ * the capture-only ledger; a third reading of the same rule would drift from
78
+ * both.
79
+ *
80
+ * TWO KNOWN FALSE-POSITIVE PATHS, named because a refusing detector owes its
81
+ * residual rather than only its fix:
82
+ *
83
+ * · A BLOCKQUOTED illustration of an ask — `> 1.` / `> 2.` plus a quoted
84
+ * recommendation line — reads as live. `OPTION_LINE_RE` accepts a leading
85
+ * `>` deliberately, because this suite's own documented ask shape is often
86
+ * blockquoted, so excluding it would miss real asks to avoid a quoted one.
87
+ * Fenced illustrations ARE excluded and tested; blockquoted ones are not,
88
+ * and narrowing the shared parser to fix that would change what
89
+ * `check_reply_consistency` enforces about Iron Law 1.
90
+ * · A genuine user turn carrying NO `type: 'text'` block — an image-only
91
+ * message — does not reset the turn, because `readTranscriptTail` skips an
92
+ * entry with no text before reaching the reset. Changing that would move
93
+ * `turnOrdinal` for all four other detectors on a shape none of them has
94
+ * been measured against.
95
+ *
96
+ * Both cost one extra line in a reply, once: the re-entrancy layers cap a turn
97
+ * at ONE refusal, so neither can wedge a session.
98
+ */
99
+ export function detectDroppedDecision(
100
+ assistantTurnTexts: readonly string[],
101
+ closingFromPayload = false,
102
+ ): DroppedDecisionFinding | null {
103
+ if (assistantTurnTexts.length < 2) return null;
104
+ // Masked once per text, not once per block: this runs on the stop path over
105
+ // every assistant text of the turn, and a reply with four numbered blocks
106
+ // would otherwise split and fence-mask the whole reply four times.
107
+ const asksIn = (text: string): ReturnType<typeof find_option_blocks> => {
108
+ const blocks = find_option_blocks(text);
109
+ if (blocks.length === 0) return blocks;
110
+ const masked = splitAndMask(text);
111
+ return blocks.filter((b) => recommendationsUnder(text, b, masked).length > 0);
112
+ };
113
+ const closing = assistantTurnTexts[assistantTurnTexts.length - 1]!;
114
+ if (asksIn(closing).length > 0) return null;
115
+ for (let i = assistantTurnTexts.length - 2; i >= 0; i -= 1) {
116
+ const earlier = assistantTurnTexts[i]!;
117
+ const blocks = asksIn(earlier);
118
+ if (blocks.length === 0) continue;
119
+ const block = blocks[blocks.length - 1]!;
120
+ return {
121
+ detector: 'pending-decision',
122
+ // The option NUMBERS and the line span, never the option text: the
123
+ // evidence is quoted into a refusal that reaches the transcript, and
124
+ // an options block can carry anything the reply carried.
125
+ evidence:
126
+ `options ${block.numbers.join('/')} at lines ${block.startLine}-${block.endLine} ` +
127
+ `of an earlier reply this turn (closing reply read from ` +
128
+ `${closingFromPayload ? 'the stop payload' : 'the transcript'})`,
129
+ reason:
130
+ 'an earlier reply in THIS turn put numbered options to the user ' +
131
+ 'and no user answer followed — this closing reply carries no ' +
132
+ 'options block, so the decision disappeared without being ' +
133
+ 'answered, cancelled or superseded. A hook nudge, a reviewer ' +
134
+ 'result or a task notification may ADD to a turn; it may not ' +
135
+ 'displace a question already asked (user-interaction Iron Law 3: ' +
136
+ 'a decision handed to the user outlives the turn). ' +
137
+ 'Re-present the block and its recommendation line at the end of ' +
138
+ 'this reply',
139
+ };
140
+ }
141
+ return null;
142
+ }