@telora/daemon 0.21.83 → 0.21.134

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 (267) hide show
  1. package/build-info.json +10 -3
  2. package/dist/audit/audit-hooks.d.ts +106 -0
  3. package/dist/audit/audit-hooks.d.ts.map +1 -1
  4. package/dist/audit/audit-hooks.js +149 -17
  5. package/dist/audit/audit-hooks.js.map +1 -1
  6. package/dist/ci/ci-sensor-loop.d.ts +6 -0
  7. package/dist/ci/ci-sensor-loop.d.ts.map +1 -1
  8. package/dist/ci/ci-sensor-loop.js +50 -3
  9. package/dist/ci/ci-sensor-loop.js.map +1 -1
  10. package/dist/ci/ci-status.d.ts +63 -11
  11. package/dist/ci/ci-status.d.ts.map +1 -1
  12. package/dist/ci/ci-status.js +94 -27
  13. package/dist/ci/ci-status.js.map +1 -1
  14. package/dist/completion/coverage-drift-state.d.ts +29 -2
  15. package/dist/completion/coverage-drift-state.d.ts.map +1 -1
  16. package/dist/completion/coverage-drift-state.js +42 -2
  17. package/dist/completion/coverage-drift-state.js.map +1 -1
  18. package/dist/completion/coverage-stabilized.d.ts +58 -0
  19. package/dist/completion/coverage-stabilized.d.ts.map +1 -0
  20. package/dist/completion/coverage-stabilized.js +84 -0
  21. package/dist/completion/coverage-stabilized.js.map +1 -0
  22. package/dist/completion/delivery-advancement-guard.d.ts +21 -0
  23. package/dist/completion/delivery-advancement-guard.d.ts.map +1 -1
  24. package/dist/completion/delivery-advancement-guard.js +31 -3
  25. package/dist/completion/delivery-advancement-guard.js.map +1 -1
  26. package/dist/completion/event-escalations.d.ts +15 -0
  27. package/dist/completion/event-escalations.d.ts.map +1 -1
  28. package/dist/completion/event-escalations.js +42 -4
  29. package/dist/completion/event-escalations.js.map +1 -1
  30. package/dist/completion/event-phases.d.ts.map +1 -1
  31. package/dist/completion/event-phases.js +10 -5
  32. package/dist/completion/event-phases.js.map +1 -1
  33. package/dist/completion/machine-remediable-demotion.d.ts +74 -0
  34. package/dist/completion/machine-remediable-demotion.d.ts.map +1 -0
  35. package/dist/completion/machine-remediable-demotion.js +147 -0
  36. package/dist/completion/machine-remediable-demotion.js.map +1 -0
  37. package/dist/completion/review-exit-escalations.d.ts +36 -1
  38. package/dist/completion/review-exit-escalations.d.ts.map +1 -1
  39. package/dist/completion/review-exit-escalations.js +81 -0
  40. package/dist/completion/review-exit-escalations.js.map +1 -1
  41. package/dist/completion/review-exit-phase.d.ts +4 -2
  42. package/dist/completion/review-exit-phase.d.ts.map +1 -1
  43. package/dist/completion/review-exit-phase.js +49 -54
  44. package/dist/completion/review-exit-phase.js.map +1 -1
  45. package/dist/completion/settle-injection-sweep.d.ts +55 -0
  46. package/dist/completion/settle-injection-sweep.d.ts.map +1 -0
  47. package/dist/completion/settle-injection-sweep.js +88 -0
  48. package/dist/completion/settle-injection-sweep.js.map +1 -0
  49. package/dist/completion/state-cascade.d.ts.map +1 -1
  50. package/dist/completion/state-cascade.js +41 -0
  51. package/dist/completion/state-cascade.js.map +1 -1
  52. package/dist/completion/status-advance-phase.d.ts.map +1 -1
  53. package/dist/completion/status-advance-phase.js +28 -30
  54. package/dist/completion/status-advance-phase.js.map +1 -1
  55. package/dist/completion/team-lifecycle.d.ts +42 -0
  56. package/dist/completion/team-lifecycle.d.ts.map +1 -1
  57. package/dist/completion/team-lifecycle.js +79 -2
  58. package/dist/completion/team-lifecycle.js.map +1 -1
  59. package/dist/delivery/delivery-lifecycle.d.ts +19 -33
  60. package/dist/delivery/delivery-lifecycle.d.ts.map +1 -1
  61. package/dist/delivery/delivery-lifecycle.js +26 -160
  62. package/dist/delivery/delivery-lifecycle.js.map +1 -1
  63. package/dist/delivery/delivery-merge-ops.d.ts +46 -0
  64. package/dist/delivery/delivery-merge-ops.d.ts.map +1 -0
  65. package/dist/delivery/delivery-merge-ops.js +154 -0
  66. package/dist/delivery/delivery-merge-ops.js.map +1 -0
  67. package/dist/delivery/delivery-reconciler-apply.d.ts +108 -66
  68. package/dist/delivery/delivery-reconciler-apply.d.ts.map +1 -1
  69. package/dist/delivery/delivery-reconciler-apply.js +87 -61
  70. package/dist/delivery/delivery-reconciler-apply.js.map +1 -1
  71. package/dist/delivery/delivery-reconciler.d.ts +7 -2
  72. package/dist/delivery/delivery-reconciler.d.ts.map +1 -1
  73. package/dist/delivery/delivery-reconciler.js +34 -5
  74. package/dist/delivery/delivery-reconciler.js.map +1 -1
  75. package/dist/delivery/external-hold-surface.d.ts +32 -0
  76. package/dist/delivery/external-hold-surface.d.ts.map +1 -0
  77. package/dist/delivery/external-hold-surface.js +67 -0
  78. package/dist/delivery/external-hold-surface.js.map +1 -0
  79. package/dist/delivery/external-hold.d.ts +38 -0
  80. package/dist/delivery/external-hold.d.ts.map +1 -1
  81. package/dist/delivery/external-hold.js +69 -6
  82. package/dist/delivery/external-hold.js.map +1 -1
  83. package/dist/delivery/operator-hold.d.ts +1 -2
  84. package/dist/delivery/operator-hold.d.ts.map +1 -1
  85. package/dist/delivery/operator-hold.js +1 -2
  86. package/dist/delivery/operator-hold.js.map +1 -1
  87. package/dist/delivery/paused-revival.d.ts +140 -0
  88. package/dist/delivery/paused-revival.d.ts.map +1 -0
  89. package/dist/delivery/paused-revival.js +229 -0
  90. package/dist/delivery/paused-revival.js.map +1 -0
  91. package/dist/delivery/wait-reachability.d.ts +149 -0
  92. package/dist/delivery/wait-reachability.d.ts.map +1 -0
  93. package/dist/delivery/wait-reachability.js +193 -0
  94. package/dist/delivery/wait-reachability.js.map +1 -0
  95. package/dist/directive/close-loop-dispatcher.d.ts +16 -263
  96. package/dist/directive/close-loop-dispatcher.d.ts.map +1 -1
  97. package/dist/directive/close-loop-dispatcher.js +21 -259
  98. package/dist/directive/close-loop-dispatcher.js.map +1 -1
  99. package/dist/directive/directive-dispatch.d.ts +19 -0
  100. package/dist/directive/directive-dispatch.d.ts.map +1 -1
  101. package/dist/directive/directive-dispatch.js +18 -0
  102. package/dist/directive/directive-dispatch.js.map +1 -1
  103. package/dist/directive/directive-executor.d.ts +1 -3
  104. package/dist/directive/directive-executor.d.ts.map +1 -1
  105. package/dist/directive/directive-executor.js +23 -21
  106. package/dist/directive/directive-executor.js.map +1 -1
  107. package/dist/directive/phase-sync.d.ts +28 -15
  108. package/dist/directive/phase-sync.d.ts.map +1 -1
  109. package/dist/directive/phase-sync.js +28 -36
  110. package/dist/directive/phase-sync.js.map +1 -1
  111. package/dist/directive/resolver-dispatch.d.ts +19 -92
  112. package/dist/directive/resolver-dispatch.d.ts.map +1 -1
  113. package/dist/directive/resolver-dispatch.js +19 -65
  114. package/dist/directive/resolver-dispatch.js.map +1 -1
  115. package/dist/focus/focus-engine.d.ts.map +1 -1
  116. package/dist/focus/focus-engine.js +26 -0
  117. package/dist/focus/focus-engine.js.map +1 -1
  118. package/dist/focus/focus-executor.d.ts.map +1 -1
  119. package/dist/focus/focus-executor.js +290 -256
  120. package/dist/focus/focus-executor.js.map +1 -1
  121. package/dist/focus/focus-loop-registry.d.ts.map +1 -1
  122. package/dist/focus/focus-loop-registry.js +43 -1
  123. package/dist/focus/focus-loop-registry.js.map +1 -1
  124. package/dist/focus/focus-respawn-guard.d.ts.map +1 -1
  125. package/dist/focus/focus-respawn-guard.js +11 -2
  126. package/dist/focus/focus-respawn-guard.js.map +1 -1
  127. package/dist/focus/focus-team-state.d.ts +11 -0
  128. package/dist/focus/focus-team-state.d.ts.map +1 -1
  129. package/dist/focus/focus-team-state.js +17 -0
  130. package/dist/focus/focus-team-state.js.map +1 -1
  131. package/dist/focus/listener-auto-advance.d.ts +13 -1
  132. package/dist/focus/listener-auto-advance.d.ts.map +1 -1
  133. package/dist/focus/listener-auto-advance.js +112 -76
  134. package/dist/focus/listener-auto-advance.js.map +1 -1
  135. package/dist/focus/listener.d.ts.map +1 -1
  136. package/dist/focus/listener.js +141 -75
  137. package/dist/focus/listener.js.map +1 -1
  138. package/dist/focus/tick-deadline.d.ts +55 -0
  139. package/dist/focus/tick-deadline.d.ts.map +1 -0
  140. package/dist/focus/tick-deadline.js +91 -0
  141. package/dist/focus/tick-deadline.js.map +1 -0
  142. package/dist/focus-stage-lifecycle/index.d.ts +18 -25
  143. package/dist/focus-stage-lifecycle/index.d.ts.map +1 -1
  144. package/dist/focus-stage-lifecycle/index.js +26 -38
  145. package/dist/focus-stage-lifecycle/index.js.map +1 -1
  146. package/dist/focus-stage-lifecycle/wedge-cause-arbiter.d.ts +52 -0
  147. package/dist/focus-stage-lifecycle/wedge-cause-arbiter.d.ts.map +1 -0
  148. package/dist/focus-stage-lifecycle/wedge-cause-arbiter.js +94 -0
  149. package/dist/focus-stage-lifecycle/wedge-cause-arbiter.js.map +1 -0
  150. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts +18 -1
  151. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
  152. package/dist/focus-stage-lifecycle/wedge-watchdog.js +57 -7
  153. package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
  154. package/dist/git/merge.d.ts.map +1 -1
  155. package/dist/git/merge.js +12 -3
  156. package/dist/git/merge.js.map +1 -1
  157. package/dist/guard/condition-evaluators.d.ts +9 -0
  158. package/dist/guard/condition-evaluators.d.ts.map +1 -1
  159. package/dist/guard/condition-evaluators.js +15 -0
  160. package/dist/guard/condition-evaluators.js.map +1 -1
  161. package/dist/guard/guard-evaluator.d.ts +17 -1
  162. package/dist/guard/guard-evaluator.d.ts.map +1 -1
  163. package/dist/guard/guard-evaluator.js +61 -2
  164. package/dist/guard/guard-evaluator.js.map +1 -1
  165. package/dist/guard/trigger-executor.d.ts +30 -18
  166. package/dist/guard/trigger-executor.d.ts.map +1 -1
  167. package/dist/guard/trigger-executor.js +55 -31
  168. package/dist/guard/trigger-executor.js.map +1 -1
  169. package/dist/prompt/team-prompt-base.d.ts +1 -1
  170. package/dist/prompt/team-prompt-base.d.ts.map +1 -1
  171. package/dist/prompt/team-prompt-base.js +1 -1
  172. package/dist/prompt/team-prompt-base.js.map +1 -1
  173. package/dist/prompt-sections/persona-sections.d.ts +0 -32
  174. package/dist/prompt-sections/persona-sections.d.ts.map +1 -1
  175. package/dist/prompt-sections/persona-sections.js +19 -88
  176. package/dist/prompt-sections/persona-sections.js.map +1 -1
  177. package/dist/queries/deliveries.d.ts +33 -0
  178. package/dist/queries/deliveries.d.ts.map +1 -1
  179. package/dist/queries/deliveries.js +7 -0
  180. package/dist/queries/deliveries.js.map +1 -1
  181. package/dist/queries/focuses.d.ts +46 -0
  182. package/dist/queries/focuses.d.ts.map +1 -1
  183. package/dist/queries/focuses.js +60 -0
  184. package/dist/queries/focuses.js.map +1 -1
  185. package/dist/queries/guards.d.ts +10 -0
  186. package/dist/queries/guards.d.ts.map +1 -1
  187. package/dist/queries/guards.js +14 -1
  188. package/dist/queries/guards.js.map +1 -1
  189. package/dist/queries/issues.d.ts +65 -1
  190. package/dist/queries/issues.d.ts.map +1 -1
  191. package/dist/queries/issues.js +135 -2
  192. package/dist/queries/issues.js.map +1 -1
  193. package/dist/queries/schemas.d.ts +53 -0
  194. package/dist/queries/schemas.d.ts.map +1 -1
  195. package/dist/queries/schemas.js +38 -0
  196. package/dist/queries/schemas.js.map +1 -1
  197. package/dist/queries/verification.d.ts +5 -6
  198. package/dist/queries/verification.d.ts.map +1 -1
  199. package/dist/queries/verification.js +5 -6
  200. package/dist/queries/verification.js.map +1 -1
  201. package/dist/resolvers/loop-persona.d.ts +1 -11
  202. package/dist/resolvers/loop-persona.d.ts.map +1 -1
  203. package/dist/resolvers/loop-persona.js +2 -16
  204. package/dist/resolvers/loop-persona.js.map +1 -1
  205. package/dist/shell/heartbeat.d.ts +7 -0
  206. package/dist/shell/heartbeat.d.ts.map +1 -1
  207. package/dist/shell/heartbeat.js +1 -1
  208. package/dist/shell/heartbeat.js.map +1 -1
  209. package/dist/shell/unified-shell-provisioning.d.ts +40 -0
  210. package/dist/shell/unified-shell-provisioning.d.ts.map +1 -1
  211. package/dist/shell/unified-shell-provisioning.js +72 -0
  212. package/dist/shell/unified-shell-provisioning.js.map +1 -1
  213. package/dist/shell/unified-shell.d.ts.map +1 -1
  214. package/dist/shell/unified-shell.js +18 -1
  215. package/dist/shell/unified-shell.js.map +1 -1
  216. package/dist/spawn/helpers.d.ts.map +1 -1
  217. package/dist/spawn/helpers.js +6 -1
  218. package/dist/spawn/helpers.js.map +1 -1
  219. package/dist/spawn/process-spawn.d.ts.map +1 -1
  220. package/dist/spawn/process-spawn.js +7 -0
  221. package/dist/spawn/process-spawn.js.map +1 -1
  222. package/dist/spawn/respawn-cycle-escalation.d.ts +41 -0
  223. package/dist/spawn/respawn-cycle-escalation.d.ts.map +1 -0
  224. package/dist/spawn/respawn-cycle-escalation.js +83 -0
  225. package/dist/spawn/respawn-cycle-escalation.js.map +1 -0
  226. package/dist/spawn/spawn-cooldown.d.ts +119 -2
  227. package/dist/spawn/spawn-cooldown.d.ts.map +1 -1
  228. package/dist/spawn/spawn-cooldown.js +120 -10
  229. package/dist/spawn/spawn-cooldown.js.map +1 -1
  230. package/dist/spawn/spawn-decision.d.ts.map +1 -1
  231. package/dist/spawn/spawn-decision.js +33 -1
  232. package/dist/spawn/spawn-decision.js.map +1 -1
  233. package/dist/testing/fixtures/index.d.ts +64 -0
  234. package/dist/testing/fixtures/index.d.ts.map +1 -0
  235. package/dist/testing/fixtures/index.js +220 -0
  236. package/dist/testing/fixtures/index.js.map +1 -0
  237. package/dist/update/build-info.d.ts +1 -0
  238. package/dist/update/build-info.d.ts.map +1 -1
  239. package/dist/update/build-info.js +1 -1
  240. package/dist/update/build-info.js.map +1 -1
  241. package/dist/verify/stall-detector-config.d.ts +8 -0
  242. package/dist/verify/stall-detector-config.d.ts.map +1 -1
  243. package/dist/verify/stall-detector-config.js +40 -0
  244. package/dist/verify/stall-detector-config.js.map +1 -1
  245. package/dist/verify/stall-detectors.d.ts +57 -14
  246. package/dist/verify/stall-detectors.d.ts.map +1 -1
  247. package/dist/verify/stall-detectors.js +72 -55
  248. package/dist/verify/stall-detectors.js.map +1 -1
  249. package/dist/verify/verification-feedback.d.ts +3 -4
  250. package/dist/verify/verification-feedback.d.ts.map +1 -1
  251. package/dist/verify/verification-feedback.js +3 -12
  252. package/dist/verify/verification-feedback.js.map +1 -1
  253. package/dist/workflow/gate-failure-policy.d.ts +48 -0
  254. package/dist/workflow/gate-failure-policy.d.ts.map +1 -0
  255. package/dist/workflow/gate-failure-policy.js +57 -0
  256. package/dist/workflow/gate-failure-policy.js.map +1 -0
  257. package/dist/workflow/pipeline-config.d.ts +2 -2
  258. package/dist/workflow/pipeline-config.js +2 -2
  259. package/package.json +3 -3
  260. package/dist/directive/close-loop-stage.d.ts +0 -50
  261. package/dist/directive/close-loop-stage.d.ts.map +0 -1
  262. package/dist/directive/close-loop-stage.js +0 -210
  263. package/dist/directive/close-loop-stage.js.map +0 -1
  264. package/dist/focus-stage-lifecycle/review-convergence.d.ts +0 -133
  265. package/dist/focus-stage-lifecycle/review-convergence.d.ts.map +0 -1
  266. package/dist/focus-stage-lifecycle/review-convergence.js +0 -237
  267. package/dist/focus-stage-lifecycle/review-convergence.js.map +0 -1
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Paused-delivery revival -- the daemon-traversable EXIT from `paused` (TEL-1:
3
+ * "Every path into `paused` has a daemon-traversable exit or a named human-act
4
+ * hold").
5
+ *
6
+ * A `paused` delivery is a guard BLOCK on a transition (coding->verify or
7
+ * verify->done), written with an open `transition_blocks` row carrying the
8
+ * blocked transition and its human decision_paths (retry / override). Before
9
+ * this consumer, that block row was written but "nothing read it back": a paused
10
+ * delivery was skipped by every actionable-status pass, never re-evaluated, and
11
+ * -- because paused BLOCKS lower-ranked deliveries -- it wedged the whole ranked
12
+ * queue until a human re-queued it. On an ARMED focus that human never comes
13
+ * (2026-07-28 ruling: an armed focus reaches its terminal without a human;
14
+ * decide-and-record, reversibility as tiebreaker).
15
+ *
16
+ * This module reads the block row back. Each sweep, for every paused delivery it
17
+ * re-evaluates the SAME blocked transition's guards and routes the fresh verdict
18
+ * through the single-writer reconciler (delivery-reconciler `paused` case):
19
+ * - guard CLEARED -> revive `paused` -> `queued` (the canonical revival edge);
20
+ * the normal walk re-drives and re-hits the gate. This is
21
+ * the daemon-traversable exit.
22
+ * - guard STILL blocks -> hold. The pause stays a NAMED human-act hold (its
23
+ * gate_state blocking conditions + the block's `override`
24
+ * decision path name the act only a human can perform), and
25
+ * the wedge watchdog surfaces it. The daemon never OVERRIDES
26
+ * an unclear gate -- that irreversible act is the human's.
27
+ *
28
+ * A paused delivery with NO open block row is NOT abandoned: the blocked FORWARD
29
+ * transition (coding->verify / verify->done) is recovered from the delivery's
30
+ * preserved source stage and re-evaluated the same way -- so a hard-guard pause
31
+ * that wrote no block_transition row still gets a daemon-traversable exit. Only a
32
+ * pause whose stage yields no recoverable forward transition (unknown/ambiguous)
33
+ * stays `no-block` for the watchdog.
34
+ *
35
+ * DI-first: `reconcilePausedDelivery` + `checkPausedDeliveriesForRevival` take a
36
+ * deps object so they unit-test with plain stubs (no `mock.module`, which would
37
+ * poison the daemon-core bare-import loader). `buildDefaultPausedRevivalDeps`
38
+ * wires the real query/guard functions for the listener.
39
+ */
40
+ import { DELIVERY_STATUS } from '@telora/daemon-core';
41
+ import { resolveReconciledWrite } from './delivery-reconciler-apply.js';
42
+ import { getActiveTransitionBlock, resolveTransitionBlock } from '../queries/guards.js';
43
+ import { evaluateGuardsAtTransition } from '../guard/guard-evaluator.js';
44
+ import { fetchEffectiveWorkflow } from '../queries/workflows.js';
45
+ import { updateDeliveryStatus, getActiveFocuses } from '../queries/deliveries.js';
46
+ import { getFocusDeliveries } from '../queries/focuses.js';
47
+ import { createLogger } from '../shell/log.js';
48
+ const log = createLogger({ module: 'paused-revival' });
49
+ /**
50
+ * The canonical FORWARD delivery-stage a pause blocks. A `paused` delivery was
51
+ * blocked advancing coding->verify or verify->done (the only two guard-block
52
+ * pause producers, delivery-reconciler.ts:314/329); the blocked transition is the
53
+ * one FROM the source stage TO this forward stage. Used to recover the transition
54
+ * for re-evaluation when there is no transition_blocks row to read it from.
55
+ */
56
+ const FORWARD_DELIVERY_STAGE = {
57
+ [DELIVERY_STATUS.CODING]: DELIVERY_STATUS.VERIFY,
58
+ [DELIVERY_STATUS.VERIFY]: DELIVERY_STATUS.DONE,
59
+ };
60
+ /**
61
+ * The `decision_path` stamped on a block the daemon closes on auto-revival. The
62
+ * block leaves `decided_by` NULL (a machine action has no auth.users id), which
63
+ * is what keeps it OUT of the human-routing resolved-block query (that path
64
+ * requires decided_by IS NOT NULL). This value is the audit marker for "the
65
+ * daemon revived past this block because its guard cleared", never a human's
66
+ * retry/override choice.
67
+ */
68
+ const AUTO_REVIVAL_DECISION_PATH = 'daemon_auto_revival';
69
+ /**
70
+ * Re-evaluate one paused delivery's blocking guard and, if it has cleared,
71
+ * traverse it out of paused via the single-writer reconciler. Returns the
72
+ * outcome for sweep accounting. Never throws for an expected I/O failure -- a
73
+ * guard-reeval failure holds the delivery (safe: stays paused) rather than
74
+ * risking a wrong revival.
75
+ */
76
+ export async function reconcilePausedDelivery(delivery, organizationId, deps) {
77
+ // Recover the blocked transition: prefer the open block row (the block_transition
78
+ // producer), else derive the forward transition from the preserved source stage
79
+ // (a hard-guard pause with no block row). Either yields the transition whose
80
+ // guards we re-evaluate; neither => nothing recoverable, leave for the watchdog.
81
+ const block = await deps.getActiveTransitionBlock(delivery.id);
82
+ const transitionId = block?.transitionId ??
83
+ (await deps.resolveForwardTransitionId(delivery.id, delivery.currentWorkflowStageId));
84
+ if (!transitionId)
85
+ return 'no-block';
86
+ let stillBlocked;
87
+ try {
88
+ stillBlocked = await deps.reEvaluateGuardBlocked(delivery.id, organizationId, transitionId);
89
+ }
90
+ catch (err) {
91
+ log.warn(`[paused-revival] guard re-eval failed for "${delivery.name}" (${delivery.id.slice(0, 8)}): ` +
92
+ `${err.message} -- holding paused this tick`);
93
+ return 'held';
94
+ }
95
+ // Route the fresh verdict through the single reconciler computation. The
96
+ // paused case revives to `queued` only when the guard has cleared; otherwise
97
+ // it stays paused.
98
+ const decision = resolveReconciledWrite({
99
+ input: {
100
+ currentStatus: DELIVERY_STATUS.PAUSED,
101
+ issues: [],
102
+ gitState: { worktreeCommitted: true },
103
+ guardVerdict: { blocked: stillBlocked },
104
+ reviewState: null,
105
+ },
106
+ });
107
+ if (decision.target === DELIVERY_STATUS.PAUSED)
108
+ return 'held';
109
+ // Close out the block this revival acted on BEFORE the status write, so even a
110
+ // crash between the two leaves the block resolved (the next sweep then derives
111
+ // the forward transition from the preserved stage rather than re-reading a
112
+ // stale row). A derived-forward revival (block === null) has nothing to close.
113
+ // Best-effort: a close failure never blocks the already-decided revival.
114
+ if (block) {
115
+ try {
116
+ await deps.resolveTransitionBlock(block.id, AUTO_REVIVAL_DECISION_PATH);
117
+ }
118
+ catch (err) {
119
+ log.warn(`[paused-revival] failed to close block ${block.id.slice(0, 8)} for ` +
120
+ `"${delivery.name}" (${delivery.id.slice(0, 8)}): ${err.message} -- ` +
121
+ `revival proceeds; a stale-open block may be re-read on a later pause episode`);
122
+ }
123
+ }
124
+ const stageId = await deps.resolveStageId(delivery.id, decision.target);
125
+ // Clear gate_state + completion_artifacts explicitly (null, not undefined): the
126
+ // block that held this delivery no longer applies once it leaves paused, so the
127
+ // row must carry no gate state describing the block it has left. queries/
128
+ // deliveries.ts only clears the column on null; undefined would leave it stale.
129
+ await deps.updateDeliveryStatus(delivery.id, decision.target, stageId, null, null, { organizationId, fromStatus: DELIVERY_STATUS.PAUSED });
130
+ log.info(`[paused-revival] revived "${delivery.name}" (${delivery.id.slice(0, 8)}) ` +
131
+ `paused -> ${decision.target}: blocking guard cleared on re-eval`);
132
+ return 'revived';
133
+ }
134
+ /**
135
+ * Sweep every active focus's paused deliveries and revive the ones whose
136
+ * blocking guard has cleared. Runs on TEAMLESS focuses too (a paused delivery
137
+ * wedges the queue so no team spawns) -- hence it iterates active focuses, not
138
+ * activeTeams. Best-effort per focus/delivery: one failure never aborts the
139
+ * sweep.
140
+ */
141
+ export async function checkPausedDeliveriesForRevival(config, deps) {
142
+ let revived = 0;
143
+ let held = 0;
144
+ let noBlock = 0;
145
+ let focuses;
146
+ try {
147
+ focuses = await deps.getActiveFocuses(config.organizationId, config.productId);
148
+ }
149
+ catch (err) {
150
+ log.warn(`[paused-revival] failed to fetch active focuses: ${err.message}`);
151
+ return { revived, held, noBlock };
152
+ }
153
+ for (const focus of focuses) {
154
+ let deliveries;
155
+ try {
156
+ deliveries = await deps.getFocusDeliveries(focus.focus_id);
157
+ }
158
+ catch (err) {
159
+ log.warn(`[paused-revival] failed to fetch deliveries for focus ${focus.focus_id.slice(0, 8)}: ${err.message}`);
160
+ continue;
161
+ }
162
+ for (const delivery of deliveries) {
163
+ if ((delivery.executionStatus ?? '') !== DELIVERY_STATUS.PAUSED)
164
+ continue;
165
+ try {
166
+ const outcome = await reconcilePausedDelivery(delivery, config.organizationId, deps);
167
+ if (outcome === 'revived')
168
+ revived++;
169
+ else if (outcome === 'held')
170
+ held++;
171
+ else
172
+ noBlock++;
173
+ }
174
+ catch (err) {
175
+ log.warn(`[paused-revival] revival check failed for "${delivery.name}" (${delivery.id.slice(0, 8)}): ${err.message}`);
176
+ }
177
+ }
178
+ }
179
+ return { revived, held, noBlock };
180
+ }
181
+ /**
182
+ * Wire the real query + guard functions into a sweep-deps object for the
183
+ * listener. Kept as thin adapters (not direct assignments) so the wide,
184
+ * strictly-typed writer/query signatures reduce to this consumer's narrow seam.
185
+ * `reEvaluateGuardBlocked` is PURE measurement: evaluateGuardsAtTransition
186
+ * fetches guards + builds context and returns a verdict; it does NOT write the
187
+ * delivery's execution_status (that stays the reconciler's job here).
188
+ */
189
+ export function buildDefaultPausedRevivalDeps() {
190
+ return {
191
+ getActiveTransitionBlock: async (deliveryId) => {
192
+ const block = await getActiveTransitionBlock(deliveryId);
193
+ return block ? { id: block.id, transitionId: block.transition_id } : null;
194
+ },
195
+ resolveForwardTransitionId: async (deliveryId, currentStageId) => {
196
+ if (!currentStageId)
197
+ return null;
198
+ const workflow = await fetchEffectiveWorkflow(deliveryId);
199
+ const sourceStage = workflow.stages.find((s) => s.id === currentStageId);
200
+ if (!sourceStage)
201
+ return null;
202
+ const forwardName = FORWARD_DELIVERY_STAGE[sourceStage.name];
203
+ if (!forwardName)
204
+ return null;
205
+ const forwardStage = workflow.stages.find((s) => s.name === forwardName);
206
+ if (!forwardStage)
207
+ return null;
208
+ const transition = (workflow.transitions ?? []).find((t) => t.from_stage_id === currentStageId && t.to_stage_id === forwardStage.id);
209
+ return transition?.id ?? null;
210
+ },
211
+ reEvaluateGuardBlocked: async (deliveryId, organizationId, transitionId) => {
212
+ const result = await evaluateGuardsAtTransition(deliveryId, organizationId, transitionId, 0);
213
+ return !result.should_proceed;
214
+ },
215
+ resolveStageId: async (deliveryId, statusName) => {
216
+ const workflow = await fetchEffectiveWorkflow(deliveryId);
217
+ return workflow.stages.find((s) => s.name === statusName)?.id;
218
+ },
219
+ updateDeliveryStatus: (deliveryId, status, stageId, gateState, completionArtifacts, transition) =>
220
+ // Pass the caller's gate_state + completion_artifacts THROUGH (reconcile-
221
+ // PausedDelivery clears them with null on the way out of paused) instead of
222
+ // hardcoding, so the clear is driven by -- and testable at -- the caller.
223
+ updateDeliveryStatus(deliveryId, status, stageId, gateState, completionArtifacts, transition),
224
+ resolveTransitionBlock: (blockId, decisionPath) => resolveTransitionBlock(blockId, decisionPath),
225
+ getActiveFocuses: (organizationId, productId) => getActiveFocuses(organizationId, productId),
226
+ getFocusDeliveries: (focusId) => getFocusDeliveries(focusId),
227
+ };
228
+ }
229
+ //# sourceMappingURL=paused-revival.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paused-revival.js","sourceRoot":"","sources":["../../src/delivery/paused-revival.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Wait reachability: can the condition a wait is blocked on still fire?
3
+ *
4
+ * The daemon waits in several places (an external hold, a stall-cycle counter, a
5
+ * mid-focus handoff, a CI merge gate). Every one of them holds against some
6
+ * awaited condition, but none of them ask the one question that tells patience
7
+ * from a stall: CAN THE THING BEING WAITED FOR STILL ARRIVE? A wait blocked on
8
+ * another actor (that actor can still act) and a wait blocked on itself -- or on
9
+ * a recorded fact that never re-evaluates -- look identical from the outside.
10
+ *
11
+ * This module is the shared answer. It classifies an awaited condition by WHAT
12
+ * it is blocked on, and derives reachability from that:
13
+ * - other_actor -> REACHABLE (another actor can still satisfy it)
14
+ * - self -> UNREACHABLE (only the waiter can produce it -> deadlock)
15
+ * - static_fact -> UNREACHABLE (a recorded fact that never re-evaluates
16
+ * without new input, e.g. a CI result on an
17
+ * unchanged sha)
18
+ *
19
+ * It is PURE and dependency-free so the decision is unit-testable without a live
20
+ * daemon, and so the hold sites (next task) and the shared wait descriptor
21
+ * (delivery 2) can both reuse the same classification.
22
+ *
23
+ * Reference incident (2026-08-17, f1): the lead held on three triggers, none of
24
+ * which could fire -- integration advancing (blocked by the very guard failure it
25
+ * was waiting out -> `self`), a CI-red result "auto-resolving" (a recorded fact
26
+ * on a sha that never re-evaluates -> `static_fact`), and a re-verify only that
27
+ * lead could initiate (-> `self`). The named constructors below encode exactly
28
+ * those classes so a caller states its wait's KIND, not a raw blocker enum.
29
+ *
30
+ * This GENERALIZES the reachability notion external-hold.ts already carries: a
31
+ * hold with no machine-readable clearing reference ("will not self-clear") is a
32
+ * hold whose condition cannot fire. Cross-product request holds are the live
33
+ * `other_actor` case -- an OPEN request the peer product can still close.
34
+ *
35
+ * @module wait-reachability
36
+ */
37
+ /**
38
+ * What a wait is blocked on -- the single axis that decides reachability.
39
+ * - other_actor: another actor (a peer product, a CI runner, integration
40
+ * merge-back) can still act, so the wait can still be satisfied.
41
+ * - self: only the waiter itself could produce what it waits for -- waiting is
42
+ * a deadlock (e.g. a re-verify only the holding lead could initiate).
43
+ * - static_fact: a recorded fact that never re-evaluates without new input
44
+ * (e.g. a CI result on an unchanged sha; it cannot change without a NEW
45
+ * commit, so waiting for it to change cannot succeed).
46
+ */
47
+ export type WaitBlocker = 'other_actor' | 'self' | 'static_fact';
48
+ /** A described awaited condition: what is being waited for, and what blocks it. */
49
+ export interface AwaitedCondition {
50
+ /** Human-readable statement of what is being waited for (for logs/records). */
51
+ condition: string;
52
+ /** What the wait is blocked on -- decides whether it can still fire. */
53
+ blockedOn: WaitBlocker;
54
+ }
55
+ /**
56
+ * The shared form EVERY place the daemon waits describes itself in: the awaited
57
+ * condition plus what it is blocked on. A `WaitDescriptor` IS an
58
+ * {@link AwaitedCondition} -- the alias names its role as the one shared wait
59
+ * vocabulary the independent waiting sites (external hold, CI merge gate, the
60
+ * no-progress counters, the handoff loop, the verify_failed cap) all speak, so a
61
+ * wait blocked on another actor is tellable from a wait blocked on itself
62
+ * WITHOUT reconstructing daemon state. Render it with {@link renderWait}.
63
+ */
64
+ export type WaitDescriptor = AwaitedCondition;
65
+ /** The reachability verdict for an awaited condition. */
66
+ export interface WaitReachability {
67
+ /** True when the condition can still fire (blocked on another actor). */
68
+ reachable: boolean;
69
+ /** The blocker the verdict was derived from (echoed for logs/records). */
70
+ blockedOn: WaitBlocker;
71
+ /** Human-readable reason, suitable for a daemon log line or a hold record. */
72
+ reason: string;
73
+ }
74
+ /**
75
+ * Classify whether an awaited condition can still fire. Pure and total: the
76
+ * verdict depends ONLY on `blockedOn`, so the same condition always classifies
77
+ * the same way regardless of who asks.
78
+ */
79
+ export declare function classifyWaitReachability(c: AwaitedCondition): WaitReachability;
80
+ /** Convenience predicate: can this awaited condition still fire? */
81
+ export declare function canStillFire(c: AwaitedCondition): boolean;
82
+ /**
83
+ * A stable, machine-checkable marker naming what a wait is blocked on, so a
84
+ * daemon log line lets a reader tell a wait blocked on ANOTHER ACTOR (can still
85
+ * arrive) from one blocked on ITSELF or on a RECORDED FACT (cannot arrive)
86
+ * WITHOUT reconstructing daemon state from git + DB. The three markers are
87
+ * disjoint substrings by construction. This is the shared vocabulary the hold
88
+ * log sites emit; delivery 2 folds it into the single wait-descriptor render.
89
+ */
90
+ export declare function describeWaitReachability(r: WaitReachability): string;
91
+ /**
92
+ * The ONE canonical render of a wait for the daemon log: states WHAT is awaited
93
+ * and WHETHER it can still arrive, on one line, so a stall is legible from the
94
+ * log alone. Every waiting site routes its wait through this single function, so
95
+ * a future change to how a wait reads -- or a new fact to surface about every
96
+ * wait -- is made here, once, rather than in each component that waits.
97
+ */
98
+ export declare function renderWait(wait: WaitDescriptor): string;
99
+ /**
100
+ * A cross-product request the peer product can still close. REACHABLE: the peer
101
+ * is another actor and an OPEN request can still reach closure. Mirrors
102
+ * external-hold.ts `clearsWhenRequestsClosed`.
103
+ */
104
+ export declare function crossProductRequestWait(requestIds: readonly string[]): AwaitedCondition;
105
+ /**
106
+ * A recorded CI-status result on an UNCHANGED sha. A `red` (or otherwise
107
+ * non-green) result at refs/ci-status/<sha> never re-evaluates for the same
108
+ * commit -- it can only change with a NEW commit -- so waiting for it to turn
109
+ * green is waiting on a static fact. UNREACHABLE.
110
+ *
111
+ * A `pending` run is NOT this: the CI runner is still an active other actor, so
112
+ * that is a `ciRunPendingWait` (reachable). Only a settled, non-green result on
113
+ * a sha that will not change is the static-fact trap.
114
+ */
115
+ export declare function recordedCiResultWait(sha: string, ciStatus: string): AwaitedCondition;
116
+ /**
117
+ * A CI run still in progress for a sha. REACHABLE: the CI runner (another actor)
118
+ * has not finished, so a result can still arrive. Distinct from
119
+ * `recordedCiResultWait`, which is a SETTLED result on a sha that will not change.
120
+ */
121
+ export declare function ciRunPendingWait(sha: string): AwaitedCondition;
122
+ /**
123
+ * A re-verify (or any re-run) that only the holding lead itself could initiate.
124
+ * UNREACHABLE: the awaited event can only be produced by the waiter, so waiting
125
+ * for it is a deadlock.
126
+ */
127
+ export declare function selfInitiatedReverifyWait(detail: string): AwaitedCondition;
128
+ /**
129
+ * The daemon awaiting a team's own progress on its non-terminal deliveries.
130
+ * From the daemon's vantage the TEAM is another actor that can still act, so
131
+ * this is REACHABLE -- the daemon legitimately waits on it. The bound on how
132
+ * long it waits is the no-progress / scope-coverage guard (the stall counters),
133
+ * NOT unreachability: a stall here is "the actor has not acted for N cycles",
134
+ * surfaced by the guard tripping, not "the wait can never be satisfied".
135
+ */
136
+ export declare function teamProgressWait(nonTerminalCount: number): AwaitedCondition;
137
+ /**
138
+ * An integration branch advancing (merge-back). REACHABLE by default -- merge-back
139
+ * is driven by another actor (the daemon merge loop). But when the advance is
140
+ * blocked by the very condition this wait is itself waiting out (a circular
141
+ * wait, as in the 2026-08-17 incident where merge-back skipped the lead's branch
142
+ * as active_team because the lead was still executing), it is UNREACHABLE:
143
+ * nothing external can clear it while the wait persists.
144
+ */
145
+ export declare function integrationAdvanceWait(opts: {
146
+ circular: boolean;
147
+ detail: string;
148
+ }): AwaitedCondition;
149
+ //# sourceMappingURL=wait-reachability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-reachability.d.ts","sourceRoot":"","sources":["../../src/delivery/wait-reachability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,MAAM,GAAG,aAAa,CAAC;AAEjE,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,EAAE,WAAW,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE9C,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,SAAS,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,SAAS,EAAE,WAAW,CAAC;IACvB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,CAqB9E;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAEzD;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CASpE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAEvD;AAOD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAgBvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAKpF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAK9D;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAK1E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,GAAG,gBAAgB,CAK3E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,gBAAgB,CAUpG"}
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Wait reachability: can the condition a wait is blocked on still fire?
3
+ *
4
+ * The daemon waits in several places (an external hold, a stall-cycle counter, a
5
+ * mid-focus handoff, a CI merge gate). Every one of them holds against some
6
+ * awaited condition, but none of them ask the one question that tells patience
7
+ * from a stall: CAN THE THING BEING WAITED FOR STILL ARRIVE? A wait blocked on
8
+ * another actor (that actor can still act) and a wait blocked on itself -- or on
9
+ * a recorded fact that never re-evaluates -- look identical from the outside.
10
+ *
11
+ * This module is the shared answer. It classifies an awaited condition by WHAT
12
+ * it is blocked on, and derives reachability from that:
13
+ * - other_actor -> REACHABLE (another actor can still satisfy it)
14
+ * - self -> UNREACHABLE (only the waiter can produce it -> deadlock)
15
+ * - static_fact -> UNREACHABLE (a recorded fact that never re-evaluates
16
+ * without new input, e.g. a CI result on an
17
+ * unchanged sha)
18
+ *
19
+ * It is PURE and dependency-free so the decision is unit-testable without a live
20
+ * daemon, and so the hold sites (next task) and the shared wait descriptor
21
+ * (delivery 2) can both reuse the same classification.
22
+ *
23
+ * Reference incident (2026-08-17, f1): the lead held on three triggers, none of
24
+ * which could fire -- integration advancing (blocked by the very guard failure it
25
+ * was waiting out -> `self`), a CI-red result "auto-resolving" (a recorded fact
26
+ * on a sha that never re-evaluates -> `static_fact`), and a re-verify only that
27
+ * lead could initiate (-> `self`). The named constructors below encode exactly
28
+ * those classes so a caller states its wait's KIND, not a raw blocker enum.
29
+ *
30
+ * This GENERALIZES the reachability notion external-hold.ts already carries: a
31
+ * hold with no machine-readable clearing reference ("will not self-clear") is a
32
+ * hold whose condition cannot fire. Cross-product request holds are the live
33
+ * `other_actor` case -- an OPEN request the peer product can still close.
34
+ *
35
+ * @module wait-reachability
36
+ */
37
+ /**
38
+ * Classify whether an awaited condition can still fire. Pure and total: the
39
+ * verdict depends ONLY on `blockedOn`, so the same condition always classifies
40
+ * the same way regardless of who asks.
41
+ */
42
+ export function classifyWaitReachability(c) {
43
+ switch (c.blockedOn) {
44
+ case 'other_actor':
45
+ return {
46
+ reachable: true,
47
+ blockedOn: 'other_actor',
48
+ reason: `blocked on another actor (can still arrive): ${c.condition}`,
49
+ };
50
+ case 'self':
51
+ return {
52
+ reachable: false,
53
+ blockedOn: 'self',
54
+ reason: `blocked on itself -- only the waiter could produce this, so it cannot arrive: ${c.condition}`,
55
+ };
56
+ case 'static_fact':
57
+ return {
58
+ reachable: false,
59
+ blockedOn: 'static_fact',
60
+ reason: `blocked on a recorded fact that never re-evaluates without new input, so it cannot arrive: ${c.condition}`,
61
+ };
62
+ }
63
+ }
64
+ /** Convenience predicate: can this awaited condition still fire? */
65
+ export function canStillFire(c) {
66
+ return classifyWaitReachability(c).reachable;
67
+ }
68
+ /**
69
+ * A stable, machine-checkable marker naming what a wait is blocked on, so a
70
+ * daemon log line lets a reader tell a wait blocked on ANOTHER ACTOR (can still
71
+ * arrive) from one blocked on ITSELF or on a RECORDED FACT (cannot arrive)
72
+ * WITHOUT reconstructing daemon state from git + DB. The three markers are
73
+ * disjoint substrings by construction. This is the shared vocabulary the hold
74
+ * log sites emit; delivery 2 folds it into the single wait-descriptor render.
75
+ */
76
+ export function describeWaitReachability(r) {
77
+ switch (r.blockedOn) {
78
+ case 'other_actor':
79
+ return 'wait: blocked on ANOTHER ACTOR (can still arrive)';
80
+ case 'self':
81
+ return 'wait: blocked on ITSELF (cannot arrive -- only the waiter could produce it)';
82
+ case 'static_fact':
83
+ return 'wait: blocked on a RECORDED FACT (cannot arrive without new input)';
84
+ }
85
+ }
86
+ /**
87
+ * The ONE canonical render of a wait for the daemon log: states WHAT is awaited
88
+ * and WHETHER it can still arrive, on one line, so a stall is legible from the
89
+ * log alone. Every waiting site routes its wait through this single function, so
90
+ * a future change to how a wait reads -- or a new fact to surface about every
91
+ * wait -- is made here, once, rather than in each component that waits.
92
+ */
93
+ export function renderWait(wait) {
94
+ return `${wait.condition} [${describeWaitReachability(classifyWaitReachability(wait))}]`;
95
+ }
96
+ // ── Named constructors for the recognized wait kinds ────────────────────────
97
+ // Callers state their wait's KIND; the constructor sets the correct blocker so
98
+ // the incident's three unreachable classes cannot be mis-classified at a call
99
+ // site. These are the vocabulary delivery 2's shared descriptor builds on.
100
+ /**
101
+ * A cross-product request the peer product can still close. REACHABLE: the peer
102
+ * is another actor and an OPEN request can still reach closure. Mirrors
103
+ * external-hold.ts `clearsWhenRequestsClosed`.
104
+ */
105
+ export function crossProductRequestWait(requestIds) {
106
+ if (requestIds.length === 0) {
107
+ // No request reference: nothing names an actor that could ever close this, so
108
+ // it cannot arrive -- the REFERENCE-LESS external hold. Classifying it as a
109
+ // recorded fact (UNREACHABLE) rather than other_actor is what lets a caller
110
+ // branch on the reachability verdict (e.g. isReferenceLessHoldStale) instead
111
+ // of mistaking a hold with nothing to re-read for one a peer will close.
112
+ return {
113
+ condition: 'cross-product hold with NO request reference to close (nothing to re-read)',
114
+ blockedOn: 'static_fact',
115
+ };
116
+ }
117
+ return {
118
+ condition: `cross-product request(s) to close: ${requestIds.join(', ')}`,
119
+ blockedOn: 'other_actor',
120
+ };
121
+ }
122
+ /**
123
+ * A recorded CI-status result on an UNCHANGED sha. A `red` (or otherwise
124
+ * non-green) result at refs/ci-status/<sha> never re-evaluates for the same
125
+ * commit -- it can only change with a NEW commit -- so waiting for it to turn
126
+ * green is waiting on a static fact. UNREACHABLE.
127
+ *
128
+ * A `pending` run is NOT this: the CI runner is still an active other actor, so
129
+ * that is a `ciRunPendingWait` (reachable). Only a settled, non-green result on
130
+ * a sha that will not change is the static-fact trap.
131
+ */
132
+ export function recordedCiResultWait(sha, ciStatus) {
133
+ return {
134
+ condition: `CI status '${ciStatus}' recorded for unchanged sha ${sha} to change (needs a new commit)`,
135
+ blockedOn: 'static_fact',
136
+ };
137
+ }
138
+ /**
139
+ * A CI run still in progress for a sha. REACHABLE: the CI runner (another actor)
140
+ * has not finished, so a result can still arrive. Distinct from
141
+ * `recordedCiResultWait`, which is a SETTLED result on a sha that will not change.
142
+ */
143
+ export function ciRunPendingWait(sha) {
144
+ return {
145
+ condition: `CI run to finish for sha ${sha}`,
146
+ blockedOn: 'other_actor',
147
+ };
148
+ }
149
+ /**
150
+ * A re-verify (or any re-run) that only the holding lead itself could initiate.
151
+ * UNREACHABLE: the awaited event can only be produced by the waiter, so waiting
152
+ * for it is a deadlock.
153
+ */
154
+ export function selfInitiatedReverifyWait(detail) {
155
+ return {
156
+ condition: `a re-verify only this lead could initiate: ${detail}`,
157
+ blockedOn: 'self',
158
+ };
159
+ }
160
+ /**
161
+ * The daemon awaiting a team's own progress on its non-terminal deliveries.
162
+ * From the daemon's vantage the TEAM is another actor that can still act, so
163
+ * this is REACHABLE -- the daemon legitimately waits on it. The bound on how
164
+ * long it waits is the no-progress / scope-coverage guard (the stall counters),
165
+ * NOT unreachability: a stall here is "the actor has not acted for N cycles",
166
+ * surfaced by the guard tripping, not "the wait can never be satisfied".
167
+ */
168
+ export function teamProgressWait(nonTerminalCount) {
169
+ return {
170
+ condition: `progress from the team on ${nonTerminalCount} non-terminal delivery(ies)`,
171
+ blockedOn: 'other_actor',
172
+ };
173
+ }
174
+ /**
175
+ * An integration branch advancing (merge-back). REACHABLE by default -- merge-back
176
+ * is driven by another actor (the daemon merge loop). But when the advance is
177
+ * blocked by the very condition this wait is itself waiting out (a circular
178
+ * wait, as in the 2026-08-17 incident where merge-back skipped the lead's branch
179
+ * as active_team because the lead was still executing), it is UNREACHABLE:
180
+ * nothing external can clear it while the wait persists.
181
+ */
182
+ export function integrationAdvanceWait(opts) {
183
+ return opts.circular
184
+ ? {
185
+ condition: `integration to advance, blocked by the condition this wait is itself holding out: ${opts.detail}`,
186
+ blockedOn: 'self',
187
+ }
188
+ : {
189
+ condition: `integration to advance (merge-back): ${opts.detail}`,
190
+ blockedOn: 'other_actor',
191
+ };
192
+ }
193
+ //# sourceMappingURL=wait-reachability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-reachability.js","sourceRoot":"","sources":["../../src/delivery/wait-reachability.ts"],"names":[],"mappings":"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"}