@telora/daemon 0.17.33 → 0.17.40

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 (344) hide show
  1. package/build-info.json +5 -3
  2. package/dist/assembly-engine.d.ts +6 -0
  3. package/dist/assembly-engine.d.ts.map +1 -1
  4. package/dist/assembly-engine.js +19 -0
  5. package/dist/assembly-engine.js.map +1 -1
  6. package/dist/assembly-resolvers.d.ts +17 -8
  7. package/dist/assembly-resolvers.d.ts.map +1 -1
  8. package/dist/assembly-resolvers.js +19 -8
  9. package/dist/assembly-resolvers.js.map +1 -1
  10. package/dist/cli/connect.d.ts.map +1 -1
  11. package/dist/cli/connect.js +4 -0
  12. package/dist/cli/connect.js.map +1 -1
  13. package/dist/cli/session-state.d.ts +10 -0
  14. package/dist/cli/session-state.d.ts.map +1 -1
  15. package/dist/cli/session-state.js +31 -0
  16. package/dist/cli/session-state.js.map +1 -1
  17. package/dist/completion/completion-decision.d.ts +83 -0
  18. package/dist/completion/completion-decision.d.ts.map +1 -0
  19. package/dist/completion/completion-decision.js +48 -0
  20. package/dist/completion/completion-decision.js.map +1 -0
  21. package/dist/completion/event-escalations.d.ts +97 -0
  22. package/dist/completion/event-escalations.d.ts.map +1 -0
  23. package/dist/completion/event-escalations.js +213 -0
  24. package/dist/completion/event-escalations.js.map +1 -0
  25. package/dist/completion/event.d.ts +1 -72
  26. package/dist/completion/event.d.ts.map +1 -1
  27. package/dist/completion/event.js +149 -329
  28. package/dist/completion/event.js.map +1 -1
  29. package/dist/completion/exit-classification.d.ts +82 -0
  30. package/dist/completion/exit-classification.d.ts.map +1 -0
  31. package/dist/completion/exit-classification.js +61 -0
  32. package/dist/completion/exit-classification.js.map +1 -0
  33. package/dist/completion/index.d.ts +14 -5
  34. package/dist/completion/index.d.ts.map +1 -1
  35. package/dist/completion/index.js +14 -5
  36. package/dist/completion/index.js.map +1 -1
  37. package/dist/completion/merge-phase.d.ts +114 -0
  38. package/dist/completion/merge-phase.d.ts.map +1 -0
  39. package/dist/completion/merge-phase.js +198 -0
  40. package/dist/completion/merge-phase.js.map +1 -0
  41. package/dist/completion/review-exit-phase.d.ts +82 -0
  42. package/dist/completion/review-exit-phase.d.ts.map +1 -0
  43. package/dist/completion/review-exit-phase.js +228 -0
  44. package/dist/completion/review-exit-phase.js.map +1 -0
  45. package/dist/completion/status-advance-phase.d.ts +61 -0
  46. package/dist/completion/status-advance-phase.d.ts.map +1 -0
  47. package/dist/completion/status-advance-phase.js +182 -0
  48. package/dist/completion/status-advance-phase.js.map +1 -0
  49. package/dist/completion/team-completion.d.ts +28 -269
  50. package/dist/completion/team-completion.d.ts.map +1 -1
  51. package/dist/completion/team-completion.js +145 -676
  52. package/dist/completion/team-completion.js.map +1 -1
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +70 -41
  55. package/dist/config.js.map +1 -1
  56. package/dist/daemon-process.d.ts +18 -2
  57. package/dist/daemon-process.d.ts.map +1 -1
  58. package/dist/daemon-process.js +15 -2
  59. package/dist/daemon-process.js.map +1 -1
  60. package/dist/directive/close-loop-stage.d.ts +50 -0
  61. package/dist/directive/close-loop-stage.d.ts.map +1 -0
  62. package/dist/directive/close-loop-stage.js +196 -0
  63. package/dist/directive/close-loop-stage.js.map +1 -0
  64. package/dist/directive/directive-assembly.d.ts +33 -0
  65. package/dist/directive/directive-assembly.d.ts.map +1 -0
  66. package/dist/directive/directive-assembly.js +77 -0
  67. package/dist/directive/directive-assembly.js.map +1 -0
  68. package/dist/directive/directive-dispatch.d.ts +103 -0
  69. package/dist/directive/directive-dispatch.d.ts.map +1 -0
  70. package/dist/directive/directive-dispatch.js +279 -0
  71. package/dist/directive/directive-dispatch.js.map +1 -0
  72. package/dist/directive/phase-sync.d.ts +89 -0
  73. package/dist/directive/phase-sync.d.ts.map +1 -0
  74. package/dist/directive/phase-sync.js +173 -0
  75. package/dist/directive/phase-sync.js.map +1 -0
  76. package/dist/directive-executor.d.ts +21 -223
  77. package/dist/directive-executor.d.ts.map +1 -1
  78. package/dist/directive-executor.js +28 -687
  79. package/dist/directive-executor.js.map +1 -1
  80. package/dist/focus-engine.d.ts.map +1 -1
  81. package/dist/focus-engine.js +16 -10
  82. package/dist/focus-engine.js.map +1 -1
  83. package/dist/focus-executor.d.ts +29 -8
  84. package/dist/focus-executor.d.ts.map +1 -1
  85. package/dist/focus-executor.js +29 -10
  86. package/dist/focus-executor.js.map +1 -1
  87. package/dist/focus-stage-lifecycle.d.ts +7 -5
  88. package/dist/focus-stage-lifecycle.d.ts.map +1 -1
  89. package/dist/focus-stage-lifecycle.js +11 -6
  90. package/dist/focus-stage-lifecycle.js.map +1 -1
  91. package/dist/index.js +8 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/merge-back-loop.d.ts.map +1 -1
  94. package/dist/merge-back-loop.js +4 -10
  95. package/dist/merge-back-loop.js.map +1 -1
  96. package/dist/pipeline-config.d.ts +13 -0
  97. package/dist/pipeline-config.d.ts.map +1 -1
  98. package/dist/pipeline-config.js +15 -0
  99. package/dist/pipeline-config.js.map +1 -1
  100. package/dist/resolvers/agent-escalations.d.ts +8 -1
  101. package/dist/resolvers/agent-escalations.d.ts.map +1 -1
  102. package/dist/resolvers/agent-escalations.js +25 -22
  103. package/dist/resolvers/agent-escalations.js.map +1 -1
  104. package/dist/resolvers/agent-session-summaries.d.ts +8 -1
  105. package/dist/resolvers/agent-session-summaries.d.ts.map +1 -1
  106. package/dist/resolvers/agent-session-summaries.js +21 -18
  107. package/dist/resolvers/agent-session-summaries.js.map +1 -1
  108. package/dist/resolvers/delivery-acceptance-criteria.d.ts +7 -1
  109. package/dist/resolvers/delivery-acceptance-criteria.d.ts.map +1 -1
  110. package/dist/resolvers/delivery-acceptance-criteria.js +14 -11
  111. package/dist/resolvers/delivery-acceptance-criteria.js.map +1 -1
  112. package/dist/resolvers/delivery-description.d.ts +7 -1
  113. package/dist/resolvers/delivery-description.d.ts.map +1 -1
  114. package/dist/resolvers/delivery-description.js +14 -11
  115. package/dist/resolvers/delivery-description.js.map +1 -1
  116. package/dist/resolvers/delivery-issues.d.ts +8 -1
  117. package/dist/resolvers/delivery-issues.d.ts.map +1 -1
  118. package/dist/resolvers/delivery-issues.js +51 -48
  119. package/dist/resolvers/delivery-issues.js.map +1 -1
  120. package/dist/resolvers/delivery-listing.d.ts +16 -1
  121. package/dist/resolvers/delivery-listing.d.ts.map +1 -1
  122. package/dist/resolvers/delivery-listing.js +36 -33
  123. package/dist/resolvers/delivery-listing.js.map +1 -1
  124. package/dist/resolvers/delivery-tech-context.d.ts +7 -1
  125. package/dist/resolvers/delivery-tech-context.d.ts.map +1 -1
  126. package/dist/resolvers/delivery-tech-context.js +14 -11
  127. package/dist/resolvers/delivery-tech-context.js.map +1 -1
  128. package/dist/resolvers/deployment-profile.d.ts +8 -1
  129. package/dist/resolvers/deployment-profile.d.ts.map +1 -1
  130. package/dist/resolvers/deployment-profile.js +20 -17
  131. package/dist/resolvers/deployment-profile.js.map +1 -1
  132. package/dist/resolvers/focus-anchoring-injections.d.ts +26 -1
  133. package/dist/resolvers/focus-anchoring-injections.d.ts.map +1 -1
  134. package/dist/resolvers/focus-anchoring-injections.js +91 -88
  135. package/dist/resolvers/focus-anchoring-injections.js.map +1 -1
  136. package/dist/resolvers/focus-context.d.ts +9 -1
  137. package/dist/resolvers/focus-context.d.ts.map +1 -1
  138. package/dist/resolvers/focus-context.js +38 -35
  139. package/dist/resolvers/focus-context.js.map +1 -1
  140. package/dist/resolvers/focus-injections.d.ts +13 -1
  141. package/dist/resolvers/focus-injections.d.ts.map +1 -1
  142. package/dist/resolvers/focus-injections.js +60 -57
  143. package/dist/resolvers/focus-injections.js.map +1 -1
  144. package/dist/resolvers/focus-last-review-report.d.ts +2 -1
  145. package/dist/resolvers/focus-last-review-report.d.ts.map +1 -1
  146. package/dist/resolvers/focus-last-review-report.js +33 -30
  147. package/dist/resolvers/focus-last-review-report.js.map +1 -1
  148. package/dist/resolvers/focus-reality-tree.d.ts +15 -1
  149. package/dist/resolvers/focus-reality-tree.d.ts.map +1 -1
  150. package/dist/resolvers/focus-reality-tree.js +35 -32
  151. package/dist/resolvers/focus-reality-tree.js.map +1 -1
  152. package/dist/resolvers/git-diff-against-base.d.ts +8 -1
  153. package/dist/resolvers/git-diff-against-base.d.ts.map +1 -1
  154. package/dist/resolvers/git-diff-against-base.js +33 -30
  155. package/dist/resolvers/git-diff-against-base.js.map +1 -1
  156. package/dist/resolvers/guards-evaluation-results.d.ts +7 -1
  157. package/dist/resolvers/guards-evaluation-results.d.ts.map +1 -1
  158. package/dist/resolvers/guards-evaluation-results.js +25 -22
  159. package/dist/resolvers/guards-evaluation-results.js.map +1 -1
  160. package/dist/resolvers/index.d.ts +22 -40
  161. package/dist/resolvers/index.d.ts.map +1 -1
  162. package/dist/resolvers/index.js +112 -41
  163. package/dist/resolvers/index.js.map +1 -1
  164. package/dist/resolvers/loop-context.d.ts +18 -1
  165. package/dist/resolvers/loop-context.d.ts.map +1 -1
  166. package/dist/resolvers/loop-context.js +21 -18
  167. package/dist/resolvers/loop-context.js.map +1 -1
  168. package/dist/resolvers/loop-delivery-index.d.ts +17 -1
  169. package/dist/resolvers/loop-delivery-index.d.ts.map +1 -1
  170. package/dist/resolvers/loop-delivery-index.js +51 -48
  171. package/dist/resolvers/loop-delivery-index.js.map +1 -1
  172. package/dist/resolvers/loop-documents.d.ts +9 -1
  173. package/dist/resolvers/loop-documents.d.ts.map +1 -1
  174. package/dist/resolvers/loop-documents.js +22 -19
  175. package/dist/resolvers/loop-documents.js.map +1 -1
  176. package/dist/resolvers/loop-expected-effects.d.ts +11 -1
  177. package/dist/resolvers/loop-expected-effects.d.ts.map +1 -1
  178. package/dist/resolvers/loop-expected-effects.js +53 -50
  179. package/dist/resolvers/loop-expected-effects.js.map +1 -1
  180. package/dist/resolvers/loop-frt-statement.d.ts +11 -1
  181. package/dist/resolvers/loop-frt-statement.d.ts.map +1 -1
  182. package/dist/resolvers/loop-frt-statement.js +28 -25
  183. package/dist/resolvers/loop-frt-statement.js.map +1 -1
  184. package/dist/resolvers/loop-injection.d.ts +10 -1
  185. package/dist/resolvers/loop-injection.d.ts.map +1 -1
  186. package/dist/resolvers/loop-injection.js +38 -35
  187. package/dist/resolvers/loop-injection.js.map +1 -1
  188. package/dist/resolvers/loop-persona.d.ts +20 -1
  189. package/dist/resolvers/loop-persona.d.ts.map +1 -1
  190. package/dist/resolvers/loop-persona.js +20 -17
  191. package/dist/resolvers/loop-persona.js.map +1 -1
  192. package/dist/resolvers/loop-questions.d.ts +8 -1
  193. package/dist/resolvers/loop-questions.d.ts.map +1 -1
  194. package/dist/resolvers/loop-questions.js +39 -36
  195. package/dist/resolvers/loop-questions.js.map +1 -1
  196. package/dist/resolvers/loop-upstream-udes.d.ts +11 -1
  197. package/dist/resolvers/loop-upstream-udes.d.ts.map +1 -1
  198. package/dist/resolvers/loop-upstream-udes.js +74 -71
  199. package/dist/resolvers/loop-upstream-udes.js.map +1 -1
  200. package/dist/resolvers/prd-context.d.ts +2 -0
  201. package/dist/resolvers/prd-context.d.ts.map +1 -1
  202. package/dist/resolvers/prd-context.js +16 -13
  203. package/dist/resolvers/prd-context.js.map +1 -1
  204. package/dist/resolvers/prd-persona.d.ts +2 -0
  205. package/dist/resolvers/prd-persona.d.ts.map +1 -1
  206. package/dist/resolvers/prd-persona.js +6 -3
  207. package/dist/resolvers/prd-persona.js.map +1 -1
  208. package/dist/resolvers/reality-metrics.d.ts +8 -1
  209. package/dist/resolvers/reality-metrics.d.ts.map +1 -1
  210. package/dist/resolvers/reality-metrics.js +73 -70
  211. package/dist/resolvers/reality-metrics.js.map +1 -1
  212. package/dist/resolvers/reality-projections.d.ts +7 -1
  213. package/dist/resolvers/reality-projections.d.ts.map +1 -1
  214. package/dist/resolvers/reality-projections.js +35 -32
  215. package/dist/resolvers/reality-projections.js.map +1 -1
  216. package/dist/resolvers/reality-tree-snapshot.d.ts +10 -1
  217. package/dist/resolvers/reality-tree-snapshot.d.ts.map +1 -1
  218. package/dist/resolvers/reality-tree-snapshot.js +34 -31
  219. package/dist/resolvers/reality-tree-snapshot.js.map +1 -1
  220. package/dist/resolvers/retired-injections.d.ts +10 -1
  221. package/dist/resolvers/retired-injections.d.ts.map +1 -1
  222. package/dist/resolvers/retired-injections.js +68 -65
  223. package/dist/resolvers/retired-injections.js.map +1 -1
  224. package/dist/resolvers/review-outcomes.d.ts +11 -1
  225. package/dist/resolvers/review-outcomes.d.ts.map +1 -1
  226. package/dist/resolvers/review-outcomes.js +27 -24
  227. package/dist/resolvers/review-outcomes.js.map +1 -1
  228. package/dist/resolvers/security-advisory.d.ts +2 -1
  229. package/dist/resolvers/security-advisory.d.ts.map +1 -1
  230. package/dist/resolvers/security-advisory.js +47 -44
  231. package/dist/resolvers/security-advisory.js.map +1 -1
  232. package/dist/resolvers/wiki-search.d.ts +8 -1
  233. package/dist/resolvers/wiki-search.d.ts.map +1 -1
  234. package/dist/resolvers/wiki-search.js +17 -14
  235. package/dist/resolvers/wiki-search.js.map +1 -1
  236. package/dist/resolvers/wiki-topic.d.ts +9 -1
  237. package/dist/resolvers/wiki-topic.d.ts.map +1 -1
  238. package/dist/resolvers/wiki-topic.js +21 -18
  239. package/dist/resolvers/wiki-topic.js.map +1 -1
  240. package/dist/resolvers/workflow-stages.d.ts +7 -1
  241. package/dist/resolvers/workflow-stages.d.ts.map +1 -1
  242. package/dist/resolvers/workflow-stages.js +20 -17
  243. package/dist/resolvers/workflow-stages.js.map +1 -1
  244. package/dist/review-defect-detector.d.ts +4 -1
  245. package/dist/review-defect-detector.d.ts.map +1 -1
  246. package/dist/review-defect-detector.js +6 -8
  247. package/dist/review-defect-detector.js.map +1 -1
  248. package/dist/self-update.d.ts +6 -0
  249. package/dist/self-update.d.ts.map +1 -1
  250. package/dist/self-update.js +6 -1
  251. package/dist/self-update.js.map +1 -1
  252. package/dist/spawner/index.d.ts +2 -1
  253. package/dist/spawner/index.d.ts.map +1 -1
  254. package/dist/spawner/index.js +2 -1
  255. package/dist/spawner/index.js.map +1 -1
  256. package/dist/spawner/spawn-team.d.ts +14 -52
  257. package/dist/spawner/spawn-team.d.ts.map +1 -1
  258. package/dist/spawner/spawn-team.js +14 -469
  259. package/dist/spawner/spawn-team.js.map +1 -1
  260. package/dist/staleness.d.ts +124 -0
  261. package/dist/staleness.d.ts.map +1 -0
  262. package/dist/staleness.js +215 -0
  263. package/dist/staleness.js.map +1 -0
  264. package/dist/state-cascade.d.ts +3 -2
  265. package/dist/state-cascade.d.ts.map +1 -1
  266. package/dist/state-cascade.js +7 -3
  267. package/dist/state-cascade.js.map +1 -1
  268. package/dist/types/focus.d.ts +5 -4
  269. package/dist/types/focus.d.ts.map +1 -1
  270. package/dist/types/index.d.ts +0 -1
  271. package/dist/types/index.d.ts.map +1 -1
  272. package/dist/types/index.js +0 -1
  273. package/dist/types/index.js.map +1 -1
  274. package/dist/types/session.d.ts +2 -3
  275. package/dist/types/session.d.ts.map +1 -1
  276. package/dist/unified-engine-lifecycle.d.ts.map +1 -1
  277. package/dist/unified-engine-lifecycle.js +2 -23
  278. package/dist/unified-engine-lifecycle.js.map +1 -1
  279. package/dist/unified-shell-config.d.ts +2 -7
  280. package/dist/unified-shell-config.d.ts.map +1 -1
  281. package/dist/unified-shell-config.js +2 -23
  282. package/dist/unified-shell-config.js.map +1 -1
  283. package/dist/unified-shell-status.d.ts.map +1 -1
  284. package/dist/unified-shell-status.js +2 -4
  285. package/dist/unified-shell-status.js.map +1 -1
  286. package/dist/unified-shell.d.ts.map +1 -1
  287. package/dist/unified-shell.js +21 -24
  288. package/dist/unified-shell.js.map +1 -1
  289. package/dist/version-check.d.ts.map +1 -1
  290. package/dist/version-check.js +6 -4
  291. package/dist/version-check.js.map +1 -1
  292. package/package.json +3 -3
  293. package/dist/pm/adaptive-poller.d.ts +0 -26
  294. package/dist/pm/adaptive-poller.d.ts.map +0 -1
  295. package/dist/pm/adaptive-poller.js +0 -53
  296. package/dist/pm/adaptive-poller.js.map +0 -1
  297. package/dist/pm/cascade-evaluator.d.ts +0 -29
  298. package/dist/pm/cascade-evaluator.d.ts.map +0 -1
  299. package/dist/pm/cascade-evaluator.js +0 -135
  300. package/dist/pm/cascade-evaluator.js.map +0 -1
  301. package/dist/pm/channel-email.d.ts +0 -42
  302. package/dist/pm/channel-email.d.ts.map +0 -1
  303. package/dist/pm/channel-email.js +0 -47
  304. package/dist/pm/channel-email.js.map +0 -1
  305. package/dist/pm/channel-slack.d.ts +0 -40
  306. package/dist/pm/channel-slack.d.ts.map +0 -1
  307. package/dist/pm/channel-slack.js +0 -46
  308. package/dist/pm/channel-slack.js.map +0 -1
  309. package/dist/pm/communication-drafter.d.ts +0 -44
  310. package/dist/pm/communication-drafter.d.ts.map +0 -1
  311. package/dist/pm/communication-drafter.js +0 -84
  312. package/dist/pm/communication-drafter.js.map +0 -1
  313. package/dist/pm/dialog-handler.d.ts +0 -32
  314. package/dist/pm/dialog-handler.d.ts.map +0 -1
  315. package/dist/pm/dialog-handler.js +0 -107
  316. package/dist/pm/dialog-handler.js.map +0 -1
  317. package/dist/pm/mitigation-correlator.d.ts +0 -29
  318. package/dist/pm/mitigation-correlator.d.ts.map +0 -1
  319. package/dist/pm/mitigation-correlator.js +0 -147
  320. package/dist/pm/mitigation-correlator.js.map +0 -1
  321. package/dist/pm/risk-scanner.d.ts +0 -28
  322. package/dist/pm/risk-scanner.d.ts.map +0 -1
  323. package/dist/pm/risk-scanner.js +0 -112
  324. package/dist/pm/risk-scanner.js.map +0 -1
  325. package/dist/pm-engine.d.ts +0 -96
  326. package/dist/pm-engine.d.ts.map +0 -1
  327. package/dist/pm-engine.js +0 -540
  328. package/dist/pm-engine.js.map +0 -1
  329. package/dist/pm-process.d.ts +0 -45
  330. package/dist/pm-process.d.ts.map +0 -1
  331. package/dist/pm-process.js +0 -195
  332. package/dist/pm-process.js.map +0 -1
  333. package/dist/pm-prompt-builder.d.ts +0 -23
  334. package/dist/pm-prompt-builder.d.ts.map +0 -1
  335. package/dist/pm-prompt-builder.js +0 -64
  336. package/dist/pm-prompt-builder.js.map +0 -1
  337. package/dist/session-lifecycle.d.ts +0 -78
  338. package/dist/session-lifecycle.d.ts.map +0 -1
  339. package/dist/session-lifecycle.js +0 -382
  340. package/dist/session-lifecycle.js.map +0 -1
  341. package/dist/types/pm.d.ts +0 -53
  342. package/dist/types/pm.d.ts.map +0 -1
  343. package/dist/types/pm.js +0 -10
  344. package/dist/types/pm.js.map +0 -1
@@ -1,700 +1,41 @@
1
1
  /**
2
- * Directive Executor.
2
+ * Directive Executor -- thin orchestrating entry point.
3
3
  *
4
- * Detects focus workflow stage changes and executes the stage's
5
- * agent_directive. For inject mode: writes /compact + prompt via stdin.
6
- * For spawn mode: terminates current session, assembles directive content,
7
- * and stores it for the next spawn cycle to deliver.
4
+ * "What happens when a stage changes" reads from here: checkDirectives
5
+ * (called from the daemon poll loop) walks active focuses, syncs each
6
+ * focus's workflow stage, and edge-triggers the stage's agent_directive.
7
+ * The real logic lives in focused modules under directive/:
8
+ *
9
+ * - directive/phase-sync.ts stage detection + phase sync + transition triggers
10
+ * - directive/directive-assembly.ts assembly of directive content (recipe + prompt + tools)
11
+ * - directive/directive-dispatch.ts inject-to-stdin / spawn-queue execution + bounded retry
12
+ * - directive/close-loop-stage.ts close_loop stage dispatcher (bookkeeper + sweep)
13
+ * - directive/stage-tracker.ts lastProcessedStages map + accessors
14
+ * - directive/directive-queue.ts pendingSpawnDirectives map + identity/guard helpers
15
+ *
16
+ * This module RE-EXPORTS the full prior public surface so existing importers
17
+ * (listener, focus-executor, spawner/spawn-team, completion/team-completion,
18
+ * tests) are unaffected.
8
19
  */
9
20
  import { getActiveTeams } from './focus-team-state.js';
10
- import { resolveAssemblyRecipeWithManifest, } from './assembly-engine.js';
11
- // Side-effect import: registers assembly resolvers with assembly-engine at module load. Required - do not remove.
12
- import './assembly-resolvers.js';
13
- import { resolveLineageSpec } from './session-lineage.js';
14
- import { fetchFocusWorkflow, fetchFocusWorkflowWithTransitions, getFocusDeliveries, updateFocusStage, updateFocusWorkflowStage, } from './queries/focuses.js';
15
- import { listPendingEscalations } from './queries/issues.js';
16
- import { callApi } from './queries/shared.js';
17
- import { CLOSE_LOOP_MODEL, advanceFocusToDoneWithSweep, buildCloseLoopDirective, decideCloseLoop, getAnchoringDeliveries, } from './close-loop-dispatcher.js';
18
- import { verifyInjectionWithEvidence } from './queries/verification.js';
19
- import { ESCALATION_KINDS, FOCUS_STAGE } from '@telora/daemon-core';
20
- import { deriveFocusPhase } from './focus-phase.js';
21
- import { sendMessage } from '@telora/daemon-core';
22
- import { terminateTeam, waitForTeamExit } from './focus-executor.js';
21
+ import { fetchFocusWorkflow } from './queries/focuses.js';
22
+ import { FOCUS_STAGE } from '@telora/daemon-core';
23
23
  import { getActiveFocuses } from './supabase.js';
24
24
  import { configForProduct } from './config.js';
25
- import { fetchTransitionGuards } from './queries/guards.js';
26
- import { executeAdvanceLinkedInjections } from './trigger-executor.js';
27
- import { createEscalation } from './queries/issues.js';
28
- import { ESCALATION_REASONS } from '@telora/daemon-core';
29
25
  import { emitLoopTrigger } from './loop-event-bus.js';
30
- // ── Stage tracking (extracted to directive/stage-tracker.ts) ─────
31
- // The lastProcessedStages map + its accessors now live in a focused module.
32
- // directive-executor imports the map binding for its in-loop reads/writes and
33
- // RE-EXPORTS the accessors so existing importers are unaffected.
26
+ import { resolveLineageSpec } from './session-lineage.js';
34
27
  import { lastProcessedStages } from './directive/stage-tracker.js';
28
+ import { computeDirectiveHash, getPendingSpawnDirective, shouldSuppressRefireForPendingSpawn, } from './directive/directive-queue.js';
29
+ import { fireFocusTransitionTriggers, shouldRefireStrandedReview, syncFocusPhase, } from './directive/phase-sync.js';
30
+ import { executeDirective, resolveDirectiveExecutionMode, runDirectiveWithRetry, escalateDirectiveFailure, } from './directive/directive-dispatch.js';
31
+ import { dispatchCloseLoopStage } from './directive/close-loop-stage.js';
32
+ // ── Re-exported public surface (callers unchanged) ───────────────
35
33
  export { seedLastProcessedStage, getLastProcessedStages } from './directive/stage-tracker.js';
36
- // ── Pending spawn directives (extracted to directive/directive-queue.ts) ──
37
- // The pendingSpawnDirectives map + its accessors and the directive identity /
38
- // spawn-guard helpers now live in a focused module. directive-executor imports
39
- // the map binding + helpers it calls in-line and RE-EXPORTS the full surface so
40
- // existing importers (focus-executor, listener, tests) are unaffected.
41
- import { pendingSpawnDirectives, computeDirectiveHash, SPAWN_GUARD_WINDOW_MS, shouldSkipSpawnDirective, getPendingSpawnDirective, shouldSuppressRefireForPendingSpawn, } from './directive/directive-queue.js';
42
34
  export { computeDirectiveHash, SPAWN_GUARD_WINDOW_MS, shouldSkipSpawnDirective, consumePendingSpawnDirective, __setPendingSpawnDirectiveForTesting, hasPendingSpawnDirective, getPendingSpawnDirective, getPendingSpawnFocusIds, shouldSuppressRefireForPendingSpawn, } from './directive/directive-queue.js';
43
- /**
44
- * Assemble directive content from a StageDirective's recipe and prompt.
45
- * Shared by both inject and spawn modes, and by focus-executor for
46
- * initial spawn with a stage directive.
47
- */
48
- export async function assembleDirectiveContent(config, focusId, directive, worktreePath) {
49
- const { content } = await assembleDirectiveContentWithManifest(config, focusId, directive, worktreePath);
50
- return content;
51
- }
52
- /**
53
- * Assemble directive content AND return the assembly manifest (one entry per
54
- * recipe source). The composed `content` is identical to
55
- * assembleDirectiveContent; the manifest lets the spawn path persist what
56
- * context was injected. The manifest covers ONLY the assembly recipe -- the
57
- * static directive.prompt / tool guidance are not assembly sources.
58
- */
59
- export async function assembleDirectiveContentWithManifest(config, focusId, directive, worktreePath) {
60
- const recipe = [...directive.assembly];
61
- let assembledContext = '';
62
- let manifest = [];
63
- if (recipe.length > 0) {
64
- try {
65
- const deliveries = await getFocusDeliveries(focusId);
66
- const assemblyContext = {
67
- focusId,
68
- deliveryIds: deliveries.map(d => d.id),
69
- worktreePath,
70
- config,
71
- organizationId: config.organizationId,
72
- productId: config.productId,
73
- };
74
- const resolved = await resolveAssemblyRecipeWithManifest(recipe, assemblyContext);
75
- assembledContext = resolved.content;
76
- manifest = resolved.manifest;
77
- }
78
- catch (err) {
79
- console.warn(`[directive-executor] Assembly resolution failed for focus ${focusId.slice(0, 8)}: ${err.message}`);
80
- }
81
- }
82
- const parts = [];
83
- if (assembledContext.trim()) {
84
- parts.push(assembledContext);
85
- }
86
- if (directive.prompt) {
87
- parts.push(directive.prompt);
88
- }
89
- if (directive.tools) {
90
- const toolLines = ['## Tool Guidance', ''];
91
- if (directive.tools.include.length > 0) {
92
- toolLines.push(`**Preferred tools:** ${directive.tools.include.join(', ')}`);
93
- }
94
- if (directive.tools.discouraged.length > 0) {
95
- toolLines.push(`**Discouraged tools:** ${directive.tools.discouraged.join(', ')}`);
96
- }
97
- if (directive.tools.guidance) {
98
- toolLines.push('');
99
- toolLines.push(directive.tools.guidance);
100
- }
101
- parts.push(toolLines.join('\n'));
102
- }
103
- return { content: parts.join('\n\n'), manifest };
104
- }
105
- // ── Core execution ───────────────────────────────────────────────
106
- /**
107
- * Resolve the execution mode a directive should dispatch under.
108
- *
109
- * Pure + exported for testing. The rule:
110
- * - A valid mode ('inject' | 'spawn') passes through unchanged.
111
- * - A review-lineage directive whose execution is missing/invalid defaults
112
- * to 'spawn'. Review always spawns a fresh team, so 'spawn' is the only
113
- * sensible mode for that lineage; defaulting here self-heals malformed or
114
- * seed-drifted review directives (e.g. the focus 'review' stage whose
115
- * agent_directive lost its execution field -- see the backfill migration)
116
- * so the review-spawn path can no longer be silently stranded.
117
- * - Anything else is UNRESOLVABLE (returns null). The caller treats null as
118
- * a fail-loud condition (throw -> escalate + loop trigger) rather than a
119
- * silent no-op.
120
- *
121
- * Note: a review-lineage directive defaults to spawn even for a non-empty but
122
- * unrecognized execution value -- review is always spawnable, so we self-heal
123
- * rather than fail. Non-review lineages have no safe default and fail loud.
124
- */
125
- export function resolveDirectiveExecutionMode(execution, lineage) {
126
- if (execution === 'inject' || execution === 'spawn')
127
- return execution;
128
- if (lineage === 'review')
129
- return 'spawn';
130
- return null;
131
- }
132
- /**
133
- * Execute a stage directive for a focus.
134
- *
135
- * inject mode: sends /compact + assembled context + prompt to the active team's stdin.
136
- * spawn mode: terminates the current team, assembles directive content, stores for respawn.
137
- *
138
- * When the directive's execution mode cannot be resolved to a valid mode
139
- * (see resolveDirectiveExecutionMode), this THROWS instead of silently
140
- * no-oping. The throw propagates through runDirectiveWithRetry in
141
- * checkDirectives, which escalates and emits a loop trigger -- so a malformed
142
- * directive surfaces a signal instead of stranding the focus.
143
- *
144
- * Exported for testing.
145
- */
146
- export async function executeDirective(config, focusId, directive, focusWorkflowStageId, stageName) {
147
- // Resolve the session lineage + continuity policy DECLARED by the directive
148
- // (INJ-B), falling back to the stage-name-derived default when the directive
149
- // omits them ('reviewing' -> review/fresh; otherwise coding/resume). The
150
- // review-exit gate (focus-completion) still keys off sessionType, which we
151
- // derive from the resolved lineage: lineage 'review' tags a review session,
152
- // every other lineage tags 'coding'. This drops the old hardcoded
153
- // stageName==='reviewing' special-case while preserving the gate contract.
154
- const spec = resolveLineageSpec({ declared: directive, stageName });
155
- const sessionType = spec.lineage === 'review' ? 'review' : 'coding';
156
- const mode = resolveDirectiveExecutionMode(directive.execution, spec.lineage);
157
- console.log(`[directive-executor] Executing ${mode ?? `unresolved(${directive.execution})`} directive ` +
158
- `for focus ${focusId.slice(0, 8)} (stage ${focusWorkflowStageId.slice(0, 8)})`);
159
- if (mode === 'inject') {
160
- await executeInjectDirective(config, focusId, directive, sessionType, spec);
161
- }
162
- else if (mode === 'spawn') {
163
- await executeSpawnDirective(config, focusId, directive, sessionType, spec);
164
- }
165
- else {
166
- // Fail loud: an unknown/undefined execution mode on a non-review lineage
167
- // cannot be defaulted safely. Throwing routes through runDirectiveWithRetry
168
- // -> escalateDirectiveFailure + emitLoopTrigger (checkDirectives) so the
169
- // broken directive escalates instead of silently marking the stage
170
- // processed.
171
- throw new Error(`Unresolvable directive execution mode "${directive.execution}" ` +
172
- `(lineage "${spec.lineage}", stage "${stageName ?? 'unknown'}") for focus ${focusId.slice(0, 8)}`);
173
- }
174
- }
175
- /**
176
- * Inject mode: send /compact + assembled context + prompt to the active team's stdin.
177
- */
178
- export async function executeInjectDirective(config, focusId, directive, sessionType = 'coding', spec = resolveLineageSpec({ declared: directive, stageName: undefined })) {
179
- const activeTeams = getActiveTeams();
180
- const team = activeTeams.get(focusId);
181
- if (!team || !team.leadStdin) {
182
- // No active team -- fall back to spawn mode so the directive is
183
- // delivered when the next team spawns (e.g., review triggered while
184
- // pipeline was stopped; the pipeline gets activated but the team
185
- // hasn't spawned yet by the time the directive executor runs).
186
- console.log(`[directive-executor] No active team for focus ${focusId.slice(0, 8)} -- ` +
187
- `falling back to spawn directive for delivery on next spawn`);
188
- const message = await assembleDirectiveContent(config, focusId, directive, null);
189
- pendingSpawnDirectives.set(focusId, {
190
- message,
191
- model: directive.model ?? null,
192
- sessionType,
193
- lineage: spec.lineage,
194
- continuity: spec.continuity,
195
- contentHash: computeDirectiveHash(directive),
196
- });
197
- return;
198
- }
199
- // Don't retask a live review team. Phase changes can fire while a review
200
- // team is still working (e.g., the team mutates delivery state and the
201
- // aggregate flips the focus's derived phase). Without this guard, the
202
- // resulting inject sends /compact + a coding-stage prompt to the review
203
- // team's stdin, which silently converts a reviewer into a developer
204
- // mid-flight -- exactly what the sessionType separation is meant to
205
- // prevent. Skip the inject and let the review team finish its directive
206
- // on its own terms; if a coding-phase directive really is needed, the
207
- // listener will spawn a fresh team after the review team exits.
208
- if (team.sessionType === 'review') {
209
- console.warn(`[directive-executor] Skipping inject for focus ${focusId.slice(0, 8)} -- ` +
210
- `active team is a review session; phase-driven retasking would convert it ` +
211
- `into a coder. Directive not delivered.`);
212
- return;
213
- }
214
- // Tag the live team with the injected directive's sessionType. Without this,
215
- // a keep-alive coding team that receives the reviewing-stage directive does
216
- // the review work but stays labelled 'coding'. On a clean review exit (no
217
- // gaps filed, no focus_reviews report) handleReviewExit's auto-approve gate
218
- // requires sessionType === 'review' (focus-completion.ts), so it never fires:
219
- // the verify delivery is left in verify and the next poll re-triggers
220
- // auto-review -- an infinite review/respawn loop. Flipping the label here
221
- // also arms the `team.sessionType === 'review'` retask guard above for any
222
- // subsequent inject. (Guaranteed `!== 'review'` at this point by the guard.)
223
- team.sessionType = sessionType;
224
- // Keep the team's session-id map slot in step with the injected directive's
225
- // lineage so a subsequent resume reads/writes the right slot (INJ-B).
226
- team.lineage = spec.lineage;
227
- // Step 1: Send /compact command if recipe is specified
228
- if (directive.compact) {
229
- const compactMessage = `/compact Preserve: ${directive.compact.preserve}. Shed: ${directive.compact.shed}.`;
230
- console.log(`[directive-executor] Sending /compact for focus ${focusId.slice(0, 8)}`);
231
- sendMessage(team.leadStdin, compactMessage);
232
- }
233
- // Step 2: Assemble and send context + prompt
234
- const fullMessage = await assembleDirectiveContent(config, focusId, directive, team.worktreePath);
235
- if (fullMessage.trim()) {
236
- console.log(`[directive-executor] Sending directive message to focus ${focusId.slice(0, 8)} ` +
237
- `(${fullMessage.length} chars)`);
238
- sendMessage(team.leadStdin, fullMessage);
239
- }
240
- }
241
- /**
242
- * Spawn mode: terminate the current team, assemble directive content,
243
- * and store it for the next spawn cycle to deliver.
244
- */
245
- async function executeSpawnDirective(config, focusId, directive, sessionType = 'coding', spec = resolveLineageSpec({ declared: directive, stageName: undefined })) {
246
- const activeTeams = getActiveTeams();
247
- const team = activeTeams.get(focusId);
248
- const worktreePath = team?.worktreePath ?? null;
249
- // Assemble the directive content before terminating the team
250
- // (the team's worktree path is needed for git diff resolution)
251
- const message = await assembleDirectiveContent(config, focusId, directive, worktreePath);
252
- const contentHash = computeDirectiveHash(directive);
253
- // Store the assembled content for the next spawn to pick up
254
- pendingSpawnDirectives.set(focusId, {
255
- message,
256
- model: directive.model ?? null,
257
- sessionType,
258
- lineage: spec.lineage,
259
- continuity: spec.continuity,
260
- contentHash,
261
- });
262
- // If a team was recently spawned, it may have already consumed this exact
263
- // directive via the pending spawn directive mechanism. Skip the kill/respawn
264
- // churn only when the directive content hash matches what the team last
265
- // consumed -- divergent content (e.g. an updated stage directive) must
266
- // trigger a respawn so the team operates under fresh intent instead of
267
- // waiting for a natural exit.
268
- if (team?.startedAt) {
269
- const decision = shouldSkipSpawnDirective({
270
- team: { startedAt: team.startedAt, lastConsumedDirectiveHash: team.lastConsumedDirectiveHash },
271
- contentHash,
272
- now: Date.now(),
273
- });
274
- const ageSec = Math.round((Date.now() - team.startedAt.getTime()) / 1000);
275
- if (decision === 'skip') {
276
- console.log(`[directive-executor] Team for focus ${focusId.slice(0, 8)} was recently spawned ` +
277
- `(${ageSec}s ago) with identical directive content -- skipping respawn`);
278
- return;
279
- }
280
- if (ageSec < SPAWN_GUARD_WINDOW_MS / 1000) {
281
- console.log(`[directive-executor] Team for focus ${focusId.slice(0, 8)} was recently spawned ` +
282
- `(${ageSec}s ago) but directive content diverged -- proceeding with respawn`);
283
- }
284
- }
285
- // Terminate existing team if one exists
286
- if (team) {
287
- console.log(`[directive-executor] Terminating existing team for focus ${focusId.slice(0, 8)} before spawn`);
288
- terminateTeam(focusId, 'handoff');
289
- const exited = await waitForTeamExit(focusId, 30000);
290
- if (!exited) {
291
- console.warn(`[directive-executor] Team for focus ${focusId.slice(0, 8)} did not exit cleanly`);
292
- }
293
- }
294
- console.log(`[directive-executor] Spawn directive stored for focus ${focusId.slice(0, 8)} ` +
295
- `(${message.length} chars, model: ${directive.model ?? 'default'}) -- team will respawn on next poll cycle`);
296
- }
297
- // ── Focus transition trigger execution ────────────────────────
298
- /**
299
- * Fire on_enter triggers for a focus stage transition.
300
- *
301
- * Looks up the workflow transition from lastStageId to currentStageId,
302
- * fetches triggers on that transition, and executes on_enter triggers.
303
- * Currently supports advance_linked_injections; other action types
304
- * are logged and skipped.
305
- */
306
- async function fireFocusTransitionTriggers(focusId, lastStageId, currentStageId) {
307
- if (!lastStageId)
308
- return; // First time seeing this focus, no transition to fire
309
- try {
310
- // Find the transition between the two stages
311
- const workflow = await fetchFocusWorkflowWithTransitions(focusId);
312
- const transition = workflow.transitions?.find(t => t.from_stage_id === lastStageId && t.to_stage_id === currentStageId);
313
- if (!transition)
314
- return; // No transition found (possible for non-standard stage jumps)
315
- // Fetch triggers on this transition
316
- const resolved = await fetchTransitionGuards(transition.id);
317
- const onEnterTriggers = resolved.triggers.filter(t => t.trigger_event === 'on_enter');
318
- if (onEnterTriggers.length === 0)
319
- return;
320
- for (const trigger of onEnterTriggers) {
321
- try {
322
- if (trigger.action_type === 'advance_linked_injections') {
323
- await executeAdvanceLinkedInjections(focusId, trigger.action_config);
324
- }
325
- else {
326
- console.log(`[directive-executor] Focus trigger "${trigger.name}" (${trigger.action_type}) ` +
327
- `skipped -- not supported in focus context`);
328
- }
329
- }
330
- catch (err) {
331
- console.warn(`[directive-executor] Failed to execute focus trigger "${trigger.name}":`, err.message);
332
- }
333
- }
334
- }
335
- catch (err) {
336
- console.warn(`[directive-executor] Failed to fire focus transition triggers for ${focusId.slice(0, 8)}:`, err.message);
337
- }
338
- }
339
- // ── Phase sync ───────────────────────────────────────────────────
340
- /**
341
- * New focus workflow lifecycle stage names introduced by D1
342
- * (20260513152817_focus_workflow_lifecycle_and_tree_closure.sql).
343
- *
344
- * When `product_focuses.stage` is one of these, the daemon takes the
345
- * denormalized `focus.stage` as authoritative and maps it directly to the
346
- * matching workflow_stages row — this is how the new lifecycle (verify ->
347
- * review -> close_loop -> done) drives the daemon, rather than the legacy
348
- * delivery-aggregate phase derivation. The legacy derivation is kept as a
349
- * fallback for focuses still on the older
350
- * kickoff/coding/verifying/reviewing/winding_down flow.
351
- */
352
- const LIFECYCLE_STAGE_NAMES = new Set([
353
- 'queued',
354
- 'verify',
355
- 'review',
356
- 'close_loop',
357
- 'done',
358
- ]);
359
- /**
360
- * Decide which workflow stage name a focus should be in.
361
- *
362
- * Pure helper, exported for testing. The selection rule is:
363
- * 1. If `focus.stage` is one of the new lifecycle stage names
364
- * (`queued`/`verify`/`review`/`close_loop`/`done`), use it directly.
365
- * The denormalized stage is the authoritative signal under the new
366
- * workflow.
367
- * 2. Otherwise, fall back to the legacy phase derived from the delivery
368
- * aggregate (kickoff/coding/verifying/reviewing/winding_down/blocked).
369
- *
370
- * Note: `review_requested_at` is still read by the legacy derivation for
371
- * back-compat -- but on the new lifecycle path the `review` stage transition
372
- * is driven by writes to `product_focuses.stage`, not by writes to
373
- * `review_requested_at`. This is the Issue 1 contract.
374
- */
375
- export function resolveFocusStageName(input) {
376
- if (input.focusStage && LIFECYCLE_STAGE_NAMES.has(input.focusStage)) {
377
- return input.focusStage;
378
- }
379
- return deriveFocusPhase({
380
- deliveries: input.deliveries,
381
- reviewRequestedAt: input.reviewRequestedAt,
382
- });
383
- }
384
- /**
385
- * Stage names that represent the focus review window: 'reviewing' on the
386
- * legacy delivery-aggregate phase derivation, 'review' on the new focus
387
- * workflow lifecycle. Either means "a review is the gate to done".
388
- */
389
- const REVIEW_STAGE_NAMES = new Set(['reviewing', 'review']);
390
- /**
391
- * Decide whether to re-fire a focus's review-stage directive even though the
392
- * edge-trigger (lastProcessedStages) already marked the stage processed.
393
- *
394
- * The edge-trigger fires a stage directive exactly once per stage entry. For
395
- * the review stage that is normally correct -- a clean review completes, clears
396
- * review_requested_at, and the focus leaves the review phase. But if the review
397
- * team never spawned or was killed (e.g. a lifecycle SIGTERM) before routing,
398
- * the focus stays in the review stage with review_requested_at still set and the
399
- * directive never re-fires: a silent stuck focus (the founding bug of this focus).
400
- *
401
- * A focus STILL in the review stage with review_requested_at set is provably
402
- * stranded -- any completed review clears review_requested_at and moves the
403
- * phase. So when no review team is running, re-firing the directive is the
404
- * self-healing recovery: it respawns the review team. Once a team is running the
405
- * active-team guard suppresses the re-fire, and a successful review auto-approves
406
- * and leaves the stage -- so the re-fire is bounded, not a per-poll spin.
407
- *
408
- * Pure and exported for unit testing.
409
- */
410
- export function shouldRefireStrandedReview(input) {
411
- if (!input.stageName || !REVIEW_STAGE_NAMES.has(input.stageName))
412
- return false;
413
- if (!input.reviewRequestedAt)
414
- return false;
415
- if (input.hasActiveTeam)
416
- return false;
417
- return true;
418
- }
419
- /**
420
- * Synchronize `product_focuses.current_workflow_stage_id` with the focus's
421
- * authoritative stage. The authoritative stage is taken from `focus.stage`
422
- * when it names one of the new lifecycle stages (Issue 1); otherwise the
423
- * legacy delivery-aggregate phase derivation applies.
424
- *
425
- * Returns the stage ID the focus should be in (post-sync). Falls back to
426
- * the recorded stage id on error so callers don't have to handle a third
427
- * "unknown" state.
428
- */
429
- export async function syncFocusPhase(focus) {
430
- try {
431
- const deliveries = await getFocusDeliveries(focus.focus_id);
432
- const stageName = resolveFocusStageName({
433
- focusStage: focus.stage ?? null,
434
- deliveries: deliveries.map((d) => ({ executionStatus: d.executionStatus })),
435
- reviewRequestedAt: focus.review_requested_at,
436
- });
437
- const workflow = await fetchFocusWorkflow(focus.focus_id);
438
- const stage = workflow.stages.find((s) => s.name === stageName);
439
- if (!stage) {
440
- console.warn(`[directive-executor] No "${stageName}" stage in workflow for focus ` +
441
- `${focus.focus_id.slice(0, 8)}; phase sync skipped.`);
442
- return focus.current_workflow_stage_id;
443
- }
444
- if (stage.id !== focus.current_workflow_stage_id) {
445
- await updateFocusWorkflowStage(focus.focus_id, stage.id);
446
- console.log(`[directive-executor] Focus ${focus.focus_id.slice(0, 8)} stage -> ${stageName}`);
447
- }
448
- return stage.id;
449
- }
450
- catch (err) {
451
- console.warn(`[directive-executor] Phase sync failed for focus ${focus.focus_id.slice(0, 8)}:`, err.message);
452
- return focus.current_workflow_stage_id;
453
- }
454
- }
455
- /**
456
- * Run a directive function with bounded retry and exponential backoff.
457
- * Returns a result tuple instead of throwing so the caller can decide
458
- * whether to escalate or continue.
459
- */
460
- export async function runDirectiveWithRetry(fn, options) {
461
- const maxAttempts = options.maxAttempts ?? 3;
462
- const baseDelayMs = options.baseDelayMs ?? 1000;
463
- const sleep = options.sleep ?? ((ms) => new Promise(r => setTimeout(r, ms)));
464
- let lastError = null;
465
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
466
- try {
467
- await fn();
468
- return { ok: true, attempts: attempt, error: null };
469
- }
470
- catch (err) {
471
- lastError = err;
472
- if (attempt < maxAttempts) {
473
- options.onAttemptFailed?.(attempt, lastError);
474
- const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
475
- console.warn(`[directive-executor] ${options.label} failed (attempt ${attempt}/${maxAttempts}): ` +
476
- `${lastError.message}. Retrying in ${delayMs}ms...`);
477
- await sleep(delayMs);
478
- }
479
- }
480
- }
481
- return { ok: false, attempts: maxAttempts, error: lastError };
482
- }
483
- /**
484
- * Escalate when bounded retry of a stage directive is exhausted.
485
- *
486
- * Visible signal so operators can find which focus + stage failed and
487
- * why, instead of a buried console.warn. Mirrors escalatePlanningFailure
488
- * from focus-completion-event.ts.
489
- */
490
- export async function escalateDirectiveFailure(params, deps = { createEscalation }) {
491
- const stageLabel = params.stageName ?? 'unknown';
492
- try {
493
- await deps.createEscalation({
494
- organizationId: params.organizationId,
495
- sessionId: params.sessionId,
496
- issueId: null,
497
- reasonType: ESCALATION_REASONS.ERROR_ENCOUNTERED,
498
- description: `Stage directive execution for focus "${params.focusName}" failed after ` +
499
- `${params.attempts} attempts on stage "${stageLabel}".\n\n` +
500
- `**Focus:** ${params.focusName}\n` +
501
- `**Focus ID:** ${params.focusId}\n` +
502
- `**Stage:** ${stageLabel}\n` +
503
- `**Last error:** ${params.error.message}\n\n` +
504
- `The focus has been marked as processed for this stage to prevent a ` +
505
- `poll-cycle retry loop. A human must investigate why the directive ` +
506
- `cannot run (assembly resolver failure, transient network outage, etc.) ` +
507
- `and re-trigger the stage if appropriate.`,
508
- whatWasTried: `Bounded retry with exponential backoff (${params.attempts} attempts) on ` +
509
- `executeDirective for focus "${params.focusName}" stage "${stageLabel}".`,
510
- helpNeeded: `Inspect the daemon log for the failing assembly recipe or directive prompt. ` +
511
- `If the error is transient, the focus will recover on the next legitimate ` +
512
- `phase change. If it is structural (broken resolver, malformed directive), ` +
513
- `fix the directive or assembly recipe.`,
514
- });
515
- console.log(`[directive-executor] Created directive-failure escalation for "${params.focusName}" ` +
516
- `(stage "${stageLabel}", ${params.attempts} attempts)`);
517
- }
518
- catch (err) {
519
- console.error(`[directive-executor] Failed to create directive-failure escalation for ` +
520
- `"${params.focusName}": ${err.message}`);
521
- }
522
- }
523
- // ── Close-loop stage dispatcher ──────────────────────────────────
524
- /**
525
- * Per-focus tracker for whether the close_loop bookkeeper has been spawned
526
- * at least once. Used to avoid respawning the bookkeeper on every poll
527
- * cycle while non-terminal injections are still being worked.
528
- *
529
- * Cleared when the focus leaves close_loop (advances to done or is
530
- * manually reset). The set is in-process state -- on daemon restart it is
531
- * empty, which is fine because the dispatcher's decideCloseLoop function
532
- * will see the bookkeeper has not run, respawn it, and the new bookkeeper
533
- * will sweep idempotently (already-verified injections stay verified;
534
- * already-escalated ones still surface their open escalation).
535
- */
536
- const closeLoopBookkeeperSpawned = new Set();
537
- /** Test helper: clear the bookkeeper-spawned set. */
538
- export function __resetCloseLoopStateForTesting() {
539
- closeLoopBookkeeperSpawned.clear();
540
- }
541
- /**
542
- * Inspect open escalations and return the set of injection ids that have
543
- * an active `injection_unverified` escalation (status pending or
544
- * in_review). Used by the close_loop dispatcher to classify "escalated"
545
- * injections as terminal.
546
- *
547
- * The `metadata.injection_id` field is set by the bookkeeper persona when
548
- * filing the escalation -- see the close_loop directive prompt.
549
- */
550
- export async function getOpenInjectionUnverifiedEscalationIds() {
551
- const escalations = await listPendingEscalations();
552
- const ids = new Set();
553
- for (const e of escalations) {
554
- if (e.escalationKind !== ESCALATION_KINDS.INJECTION_UNVERIFIED)
555
- continue;
556
- const injectionId = e.metadata?.injection_id;
557
- if (typeof injectionId === 'string' && injectionId.length > 0) {
558
- ids.add(injectionId);
559
- }
560
- }
561
- return ids;
562
- }
563
- /**
564
- * Dispatch the close_loop stage for one focus.
565
- *
566
- * Reads the focus's anchored deliveries + injection statuses + open
567
- * escalations, then runs the pure `decideCloseLoop` to determine whether
568
- * to short-circuit, spawn a bookkeeper, advance to done, or hold.
569
- *
570
- * Returns the decision so the caller (or a test) can log/assert.
571
- *
572
- * Idempotent across poll cycles -- the `closeLoopBookkeeperSpawned` set
573
- * ensures the bookkeeper is spawned at most once per focus visit to
574
- * close_loop. Tests reset this via `__resetCloseLoopStateForTesting`.
575
- */
576
- export async function dispatchCloseLoopStage(config, focus) {
577
- const deliveries = await getFocusDeliveries(focus.focus_id);
578
- const anchored = getAnchoringDeliveries(deliveries);
579
- // Build the per-injection terminal-state snapshot.
580
- //
581
- // `statusById` is hoisted to function scope so the close_loop sweep (in
582
- // the advance_to_done branch below) can reuse the snapshot without a
583
- // second round-trip to reality_tree_focus_injections.
584
- const states = [];
585
- const statusById = new Map();
586
- if (anchored.length > 0) {
587
- let openEscalationIds;
588
- try {
589
- openEscalationIds = await getOpenInjectionUnverifiedEscalationIds();
590
- }
591
- catch (err) {
592
- console.warn(`[close-loop-dispatcher] Failed to fetch open injection_unverified escalations ` +
593
- `for focus ${focus.focus_id.slice(0, 8)}: ${err.message}`);
594
- openEscalationIds = new Set();
595
- }
596
- let snapshots = [];
597
- try {
598
- const result = await callApi('reality_tree_focus_injections', { focusId: focus.focus_id });
599
- snapshots = result.items ?? [];
600
- }
601
- catch (err) {
602
- console.warn(`[close-loop-dispatcher] Failed to fetch focus injections for ${focus.focus_id.slice(0, 8)}: ` +
603
- `${err.message}`);
604
- }
605
- for (const item of snapshots) {
606
- statusById.set(item.injection.id, item.injection.injectionStatus);
607
- }
608
- for (const delivery of anchored) {
609
- const injectionId = delivery.injectionId;
610
- states.push({
611
- injectionId,
612
- injectionStatus: statusById.get(injectionId) ?? null,
613
- hasOpenUnverifiedEscalation: openEscalationIds.has(injectionId),
614
- });
615
- }
616
- }
617
- const bookkeeperHasRun = closeLoopBookkeeperSpawned.has(focus.focus_id);
618
- const decision = decideCloseLoop(deliveries, states, bookkeeperHasRun);
619
- switch (decision.kind) {
620
- case 'short_circuit_to_done': {
621
- try {
622
- await updateFocusStage(focus.focus_id, 'done');
623
- console.log(`[close-loop-dispatcher] Focus "${focus.focus_name}" has no anchoring injections -- ` +
624
- `short-circuiting close_loop -> done`);
625
- }
626
- catch (err) {
627
- console.warn(`[close-loop-dispatcher] Failed to short-circuit focus ${focus.focus_id.slice(0, 8)} to done: ` +
628
- `${err.message}`);
629
- }
630
- closeLoopBookkeeperSpawned.delete(focus.focus_id);
631
- return decision;
632
- }
633
- case 'advance_to_done': {
634
- // Focus-level injection sweep + stage advance. The helper realizes
635
- // anchored injections whose anchoring deliveries closed successfully
636
- // but whose status hasn't yet flipped to 'verified' (e.g. deliveries
637
- // that exited via review instead of the per-delivery verify gate),
638
- // then advances close_loop -> done. On sweep failure, the helper
639
- // returns early WITHOUT calling updateFocusStage -- the focus stays
640
- // at close_loop and the next poll cycle retries.
641
- const outcome = await advanceFocusToDoneWithSweep(focus, anchored, statusById, { verify: verifyInjectionWithEvidence, updateFocusStage });
642
- // Clear the bookkeeper-spawned flag only when the focus has
643
- // terminally exited close_loop -- either advanced, or attempted to
644
- // advance and the stage-update itself failed. A sweep failure leaves
645
- // the flag in place so the same bookkeeper context is reused on the
646
- // next poll cycle's retry.
647
- if (outcome.advanced || outcome.advanceError) {
648
- closeLoopBookkeeperSpawned.delete(focus.focus_id);
649
- }
650
- return decision;
651
- }
652
- case 'spawn_bookkeeper': {
653
- const team = getActiveTeams().get(focus.focus_id);
654
- const worktreePath = team?.worktreePath ?? null;
655
- try {
656
- const message = await assembleDirectiveContent(config, focus.focus_id, decision.directive, worktreePath);
657
- const contentHash = computeDirectiveHash(decision.directive);
658
- pendingSpawnDirectives.set(focus.focus_id, {
659
- message,
660
- model: decision.directive.model ?? CLOSE_LOOP_MODEL,
661
- sessionType: 'coding',
662
- // The close_loop bookkeeper is a distinct one-shot session: its own
663
- // lineage slot, always fresh -- it must not resume (or clobber) the
664
- // coder's context (INJ-B).
665
- lineage: 'close_loop',
666
- continuity: 'fresh',
667
- contentHash,
668
- });
669
- if (team) {
670
- terminateTeam(focus.focus_id, 'handoff');
671
- await waitForTeamExit(focus.focus_id, 30000);
672
- }
673
- closeLoopBookkeeperSpawned.add(focus.focus_id);
674
- console.log(`[close-loop-dispatcher] Spawned bookkeeper team for focus "${focus.focus_name}" ` +
675
- `(${anchored.length} anchored injection(s), model: ${CLOSE_LOOP_MODEL})`);
676
- }
677
- catch (err) {
678
- console.warn(`[close-loop-dispatcher] Failed to spawn bookkeeper for focus ${focus.focus_id.slice(0, 8)}: ` +
679
- `${err.message}`);
680
- }
681
- return decision;
682
- }
683
- case 'hold': {
684
- console.log(`[close-loop-dispatcher] Focus "${focus.focus_name}" held at close_loop: ${decision.reason}`);
685
- return decision;
686
- }
687
- }
688
- }
689
- /**
690
- * Pure helper exported for tests: build the close_loop directive and verify
691
- * it carries the expected assembly + model. The buildCloseLoopDirective
692
- * function in close-loop-dispatcher.ts already does this; this is a
693
- * dispatcher-side smoke export so the tests can grep through it.
694
- */
695
- export function getCloseLoopDirectiveForTesting() {
696
- return buildCloseLoopDirective();
697
- }
35
+ export { assembleDirectiveContent, assembleDirectiveContentWithManifest, } from './directive/directive-assembly.js';
36
+ export { fireFocusTransitionTriggers, resolveFocusStageName, shouldRefireStrandedReview, syncFocusPhase, } from './directive/phase-sync.js';
37
+ export { resolveDirectiveExecutionMode, executeDirective, executeInjectDirective, runDirectiveWithRetry, escalateDirectiveFailure, } from './directive/directive-dispatch.js';
38
+ export { __resetCloseLoopStateForTesting, getOpenInjectionUnverifiedEscalationIds, dispatchCloseLoopStage, getCloseLoopDirectiveForTesting, } from './directive/close-loop-stage.js';
698
39
  // ── Poll loop integration ────────────────────────────────────────
699
40
  /**
700
41
  * Check all active focuses for stage changes with directives.