@telora/daemon 0.19.47 → 0.19.82

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 (597) hide show
  1. package/build-info.json +8 -3
  2. package/dist/assembly-engine.d.ts +9 -0
  3. package/dist/assembly-engine.d.ts.map +1 -1
  4. package/dist/assembly-engine.js.map +1 -1
  5. package/dist/audit-hooks.js +1 -1
  6. package/dist/branch-status.js +2 -2
  7. package/dist/branch-status.js.map +1 -1
  8. package/dist/ci-sensor-loop.d.ts +47 -0
  9. package/dist/ci-sensor-loop.d.ts.map +1 -0
  10. package/dist/ci-sensor-loop.js +116 -0
  11. package/dist/ci-sensor-loop.js.map +1 -0
  12. package/dist/ci-status-git.d.ts +52 -0
  13. package/dist/ci-status-git.d.ts.map +1 -0
  14. package/dist/ci-status-git.js +86 -0
  15. package/dist/ci-status-git.js.map +1 -0
  16. package/dist/ci-status.d.ts +122 -0
  17. package/dist/ci-status.d.ts.map +1 -0
  18. package/dist/ci-status.js +114 -0
  19. package/dist/ci-status.js.map +1 -0
  20. package/dist/cli/connect/agents.d.ts +24 -0
  21. package/dist/cli/connect/agents.d.ts.map +1 -0
  22. package/dist/cli/connect/agents.js +65 -0
  23. package/dist/cli/connect/agents.js.map +1 -0
  24. package/dist/cli/connect/claude.d.ts +31 -0
  25. package/dist/cli/connect/claude.d.ts.map +1 -0
  26. package/dist/cli/connect/claude.js +104 -0
  27. package/dist/cli/connect/claude.js.map +1 -0
  28. package/dist/cli/connect/cline.d.ts +37 -0
  29. package/dist/cli/connect/cline.d.ts.map +1 -0
  30. package/dist/cli/connect/cline.js +115 -0
  31. package/dist/cli/connect/cline.js.map +1 -0
  32. package/dist/cli/connect/codex.d.ts +44 -0
  33. package/dist/cli/connect/codex.d.ts.map +1 -0
  34. package/dist/cli/connect/codex.js +111 -0
  35. package/dist/cli/connect/codex.js.map +1 -0
  36. package/dist/cli/connect/ide.d.ts +69 -0
  37. package/dist/cli/connect/ide.d.ts.map +1 -0
  38. package/dist/cli/connect/ide.js +134 -0
  39. package/dist/cli/connect/ide.js.map +1 -0
  40. package/dist/cli/connect/scaffolding.d.ts +31 -0
  41. package/dist/cli/connect/scaffolding.d.ts.map +1 -0
  42. package/dist/cli/connect/scaffolding.js +52 -0
  43. package/dist/cli/connect/scaffolding.js.map +1 -0
  44. package/dist/cli/connect/shared.d.ts +30 -0
  45. package/dist/cli/connect/shared.d.ts.map +1 -0
  46. package/dist/cli/connect/shared.js +11 -0
  47. package/dist/cli/connect/shared.js.map +1 -0
  48. package/dist/cli/connect.d.ts +9 -210
  49. package/dist/cli/connect.d.ts.map +1 -1
  50. package/dist/cli/connect.js +10 -526
  51. package/dist/cli/connect.js.map +1 -1
  52. package/dist/cli/open.js +2 -2
  53. package/dist/completion/event.d.ts +2 -0
  54. package/dist/completion/event.d.ts.map +1 -1
  55. package/dist/completion/event.js +19 -2
  56. package/dist/completion/event.js.map +1 -1
  57. package/dist/completion/merge-phase.d.ts +3 -0
  58. package/dist/completion/merge-phase.d.ts.map +1 -1
  59. package/dist/completion/merge-phase.js +35 -3
  60. package/dist/completion/merge-phase.js.map +1 -1
  61. package/dist/completion/read-only-pristine.d.ts +1 -1
  62. package/dist/completion/read-only-pristine.d.ts.map +1 -1
  63. package/dist/completion/read-only-pristine.js +1 -1
  64. package/dist/completion/read-only-pristine.js.map +1 -1
  65. package/dist/completion/review-exit-phase.d.ts +30 -3
  66. package/dist/completion/review-exit-phase.d.ts.map +1 -1
  67. package/dist/completion/review-exit-phase.js +203 -15
  68. package/dist/completion/review-exit-phase.js.map +1 -1
  69. package/dist/completion/status-advance-phase.js +1 -1
  70. package/dist/completion/status-advance-phase.js.map +1 -1
  71. package/dist/completion/team-completion.d.ts.map +1 -1
  72. package/dist/completion/team-completion.js +22 -2
  73. package/dist/completion/team-completion.js.map +1 -1
  74. package/dist/completion/team-lifecycle.js +1 -1
  75. package/dist/completion/terminated-work-reconcile.js +1 -1
  76. package/dist/completion/terminated-work-reconcile.js.map +1 -1
  77. package/dist/condition-evaluators.d.ts +41 -2
  78. package/dist/condition-evaluators.d.ts.map +1 -1
  79. package/dist/condition-evaluators.js +37 -288
  80. package/dist/condition-evaluators.js.map +1 -1
  81. package/dist/config.d.ts.map +1 -1
  82. package/dist/config.js +23 -0
  83. package/dist/config.js.map +1 -1
  84. package/dist/context-audit.d.ts +1 -1
  85. package/dist/context-audit.d.ts.map +1 -1
  86. package/dist/context-lock.d.ts +1 -1
  87. package/dist/context-lock.d.ts.map +1 -1
  88. package/dist/control-state.d.ts +2 -2
  89. package/dist/control-state.d.ts.map +1 -1
  90. package/dist/control-state.js +4 -3
  91. package/dist/control-state.js.map +1 -1
  92. package/dist/daemon-process.d.ts +1 -1
  93. package/dist/daemon-process.d.ts.map +1 -1
  94. package/dist/delivery-advance-block.d.ts +1 -1
  95. package/dist/delivery-advance-block.d.ts.map +1 -1
  96. package/dist/delivery-advance-block.js +1 -1
  97. package/dist/delivery-advance-block.js.map +1 -1
  98. package/dist/delivery-lifecycle.js +1 -1
  99. package/dist/delivery-reconciler-shadow.d.ts +1 -1
  100. package/dist/delivery-reconciler-shadow.d.ts.map +1 -1
  101. package/dist/delivery-reconciler-shadow.js +1 -1
  102. package/dist/delivery-reconciler-shadow.js.map +1 -1
  103. package/dist/directive-executor.js +1 -1
  104. package/dist/directive-executor.js.map +1 -1
  105. package/dist/drift-eval-loop.d.ts.map +1 -1
  106. package/dist/drift-eval-loop.js +6 -14
  107. package/dist/drift-eval-loop.js.map +1 -1
  108. package/dist/escalations.d.ts +1 -1
  109. package/dist/escalations.d.ts.map +1 -1
  110. package/dist/escalations.js +1 -1
  111. package/dist/escalations.js.map +1 -1
  112. package/dist/evaluation-context.d.ts +2 -33
  113. package/dist/evaluation-context.d.ts.map +1 -1
  114. package/dist/evaluation-context.js +2 -56
  115. package/dist/evaluation-context.js.map +1 -1
  116. package/dist/focus-engine.d.ts +14 -5
  117. package/dist/focus-engine.d.ts.map +1 -1
  118. package/dist/focus-engine.js +35 -289
  119. package/dist/focus-engine.js.map +1 -1
  120. package/dist/focus-executor.d.ts +10 -791
  121. package/dist/focus-executor.d.ts.map +1 -1
  122. package/dist/focus-executor.js +81 -1572
  123. package/dist/focus-executor.js.map +1 -1
  124. package/dist/focus-lifecycle.js +2 -2
  125. package/dist/focus-lifecycle.js.map +1 -1
  126. package/dist/focus-loop-registry.d.ts +52 -0
  127. package/dist/focus-loop-registry.d.ts.map +1 -0
  128. package/dist/focus-loop-registry.js +328 -0
  129. package/dist/focus-loop-registry.js.map +1 -0
  130. package/dist/focus-merge.d.ts +1 -1
  131. package/dist/focus-merge.d.ts.map +1 -1
  132. package/dist/focus-merge.js +6 -6
  133. package/dist/focus-merge.js.map +1 -1
  134. package/dist/focus-phase.d.ts +4 -0
  135. package/dist/focus-phase.d.ts.map +1 -1
  136. package/dist/focus-phase.js +3 -3
  137. package/dist/focus-phase.js.map +1 -1
  138. package/dist/focus-prompt-builder.d.ts.map +1 -1
  139. package/dist/focus-prompt-builder.js +4 -1
  140. package/dist/focus-prompt-builder.js.map +1 -1
  141. package/dist/focus-stage-lifecycle/planning-release.d.ts +197 -0
  142. package/dist/focus-stage-lifecycle/planning-release.d.ts.map +1 -0
  143. package/dist/focus-stage-lifecycle/planning-release.js +312 -0
  144. package/dist/focus-stage-lifecycle/planning-release.js.map +1 -0
  145. package/dist/focus-stage-lifecycle/review-convergence.d.ts +133 -0
  146. package/dist/focus-stage-lifecycle/review-convergence.d.ts.map +1 -0
  147. package/dist/focus-stage-lifecycle/review-convergence.js +235 -0
  148. package/dist/focus-stage-lifecycle/review-convergence.js.map +1 -0
  149. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts +133 -0
  150. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -0
  151. package/dist/focus-stage-lifecycle/wedge-watchdog.js +365 -0
  152. package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -0
  153. package/dist/focus-stage-lifecycle.d.ts +22 -421
  154. package/dist/focus-stage-lifecycle.d.ts.map +1 -1
  155. package/dist/focus-stage-lifecycle.js +24 -832
  156. package/dist/focus-stage-lifecycle.js.map +1 -1
  157. package/dist/{git-activity.d.ts → git/activity.d.ts} +1 -1
  158. package/dist/git/activity.d.ts.map +1 -0
  159. package/dist/{git-activity.js → git/activity.js} +2 -2
  160. package/dist/git/activity.js.map +1 -0
  161. package/dist/{git-branch.d.ts → git/branch.d.ts} +2 -2
  162. package/dist/git/branch.d.ts.map +1 -0
  163. package/dist/{git-branch.js → git/branch.js} +2 -2
  164. package/dist/git/branch.js.map +1 -0
  165. package/dist/{git-fetch.d.ts → git/fetch.d.ts} +2 -2
  166. package/dist/git/fetch.d.ts.map +1 -0
  167. package/dist/{git-fetch.js → git/fetch.js} +1 -1
  168. package/dist/git/fetch.js.map +1 -0
  169. package/dist/{git-genesis.d.ts → git/genesis.d.ts} +3 -3
  170. package/dist/git/genesis.d.ts.map +1 -0
  171. package/dist/{git-genesis.js → git/genesis.js} +5 -5
  172. package/dist/git/genesis.js.map +1 -0
  173. package/dist/git/index.d.ts +36 -0
  174. package/dist/git/index.d.ts.map +1 -0
  175. package/dist/git/index.js +36 -0
  176. package/dist/git/index.js.map +1 -0
  177. package/dist/{git-integration.d.ts → git/integration.d.ts} +2 -2
  178. package/dist/git/integration.d.ts.map +1 -0
  179. package/dist/{git-integration.js → git/integration.js} +4 -4
  180. package/dist/git/integration.js.map +1 -0
  181. package/dist/{git-merge-helpers.d.ts → git/merge-helpers.d.ts} +3 -3
  182. package/dist/git/merge-helpers.d.ts.map +1 -0
  183. package/dist/{git-merge-helpers.js → git/merge-helpers.js} +3 -3
  184. package/dist/git/merge-helpers.js.map +1 -0
  185. package/dist/{git-merge-lock.d.ts → git/merge-lock.d.ts} +1 -1
  186. package/dist/git/merge-lock.d.ts.map +1 -0
  187. package/dist/{git-merge-lock.js → git/merge-lock.js} +1 -1
  188. package/dist/git/merge-lock.js.map +1 -0
  189. package/dist/{git-merge-strategies.d.ts → git/merge-strategies.d.ts} +2 -2
  190. package/dist/git/merge-strategies.d.ts.map +1 -0
  191. package/dist/{git-merge-strategies.js → git/merge-strategies.js} +3 -3
  192. package/dist/git/merge-strategies.js.map +1 -0
  193. package/dist/{git-merge.d.ts → git/merge.d.ts} +4 -4
  194. package/dist/git/merge.d.ts.map +1 -0
  195. package/dist/{git-merge.js → git/merge.js} +4 -4
  196. package/dist/git/merge.js.map +1 -0
  197. package/dist/{git-state-detector.d.ts → git/state-detector.d.ts} +2 -2
  198. package/dist/git/state-detector.d.ts.map +1 -0
  199. package/dist/{git-state-detector.js → git/state-detector.js} +7 -7
  200. package/dist/git/state-detector.js.map +1 -0
  201. package/dist/{git-types.d.ts → git/types.d.ts} +2 -2
  202. package/dist/git/types.d.ts.map +1 -0
  203. package/dist/{git-types.js → git/types.js} +1 -1
  204. package/dist/git/types.js.map +1 -0
  205. package/dist/{git-utils.d.ts → git/utils.d.ts} +1 -1
  206. package/dist/git/utils.d.ts.map +1 -0
  207. package/dist/{git-utils.js → git/utils.js} +2 -2
  208. package/dist/git/utils.js.map +1 -0
  209. package/dist/git.d.ts +5 -5
  210. package/dist/git.d.ts.map +1 -1
  211. package/dist/git.js +10 -10
  212. package/dist/git.js.map +1 -1
  213. package/dist/guard-engine.d.ts +3 -2
  214. package/dist/guard-engine.d.ts.map +1 -1
  215. package/dist/guard-engine.js +2 -2
  216. package/dist/guard-engine.js.map +1 -1
  217. package/dist/install-probe-tick.d.ts +63 -0
  218. package/dist/install-probe-tick.d.ts.map +1 -0
  219. package/dist/install-probe-tick.js +137 -0
  220. package/dist/install-probe-tick.js.map +1 -0
  221. package/dist/install-probe.d.ts +115 -0
  222. package/dist/install-probe.d.ts.map +1 -0
  223. package/dist/install-probe.js +197 -0
  224. package/dist/install-probe.js.map +1 -0
  225. package/dist/listener-auto-advance.d.ts.map +1 -1
  226. package/dist/listener-auto-advance.js +31 -0
  227. package/dist/listener-auto-advance.js.map +1 -1
  228. package/dist/listener.js +5 -5
  229. package/dist/listener.js.map +1 -1
  230. package/dist/{loop-context-audit-pass.d.ts → loops/context-audit-pass.d.ts} +4 -4
  231. package/dist/loops/context-audit-pass.d.ts.map +1 -0
  232. package/dist/{loop-context-audit-pass.js → loops/context-audit-pass.js} +7 -7
  233. package/dist/loops/context-audit-pass.js.map +1 -0
  234. package/dist/{loop-engine-types.d.ts → loops/engine-types.d.ts} +1 -1
  235. package/dist/loops/engine-types.d.ts.map +1 -0
  236. package/dist/{loop-engine-types.js → loops/engine-types.js} +1 -1
  237. package/dist/loops/engine-types.js.map +1 -0
  238. package/dist/{loop-engine.d.ts → loops/engine.d.ts} +6 -6
  239. package/dist/loops/engine.d.ts.map +1 -0
  240. package/dist/{loop-engine.js → loops/engine.js} +13 -13
  241. package/dist/loops/engine.js.map +1 -0
  242. package/dist/{loop-event-bus.d.ts → loops/event-bus.d.ts} +1 -1
  243. package/dist/loops/event-bus.d.ts.map +1 -0
  244. package/dist/{loop-event-bus.js → loops/event-bus.js} +1 -1
  245. package/dist/loops/event-bus.js.map +1 -0
  246. package/dist/loops/index.d.ts +33 -0
  247. package/dist/loops/index.d.ts.map +1 -0
  248. package/dist/loops/index.js +32 -0
  249. package/dist/loops/index.js.map +1 -0
  250. package/dist/{loop-llm-client.d.ts → loops/llm-client.d.ts} +3 -3
  251. package/dist/loops/llm-client.d.ts.map +1 -0
  252. package/dist/{loop-llm-client.js → loops/llm-client.js} +3 -3
  253. package/dist/loops/llm-client.js.map +1 -0
  254. package/dist/{loop-llm-flag.d.ts → loops/llm-flag.d.ts} +1 -1
  255. package/dist/loops/llm-flag.d.ts.map +1 -0
  256. package/dist/{loop-llm-flag.js → loops/llm-flag.js} +1 -1
  257. package/dist/loops/llm-flag.js.map +1 -0
  258. package/dist/{loop-operational-block.d.ts → loops/operational-block.d.ts} +1 -1
  259. package/dist/loops/operational-block.d.ts.map +1 -0
  260. package/dist/{loop-operational-block.js → loops/operational-block.js} +1 -1
  261. package/dist/loops/operational-block.js.map +1 -0
  262. package/dist/{loop-prompt-builder.d.ts → loops/prompt-builder.d.ts} +2 -2
  263. package/dist/loops/prompt-builder.d.ts.map +1 -0
  264. package/dist/{loop-prompt-builder.js → loops/prompt-builder.js} +1 -1
  265. package/dist/loops/prompt-builder.js.map +1 -0
  266. package/dist/{loop-variance.d.ts → loops/variance.d.ts} +1 -1
  267. package/dist/loops/variance.d.ts.map +1 -0
  268. package/dist/{loop-variance.js → loops/variance.js} +1 -1
  269. package/dist/loops/variance.js.map +1 -0
  270. package/dist/merge-back-loop.d.ts.map +1 -1
  271. package/dist/merge-back-loop.js +4 -3
  272. package/dist/merge-back-loop.js.map +1 -1
  273. package/dist/merge-detector.js +2 -2
  274. package/dist/merge-detector.js.map +1 -1
  275. package/dist/{nexus-endpoint-clearance.d.ts → nexus/endpoint-clearance.d.ts} +2 -2
  276. package/dist/nexus/endpoint-clearance.d.ts.map +1 -0
  277. package/dist/{nexus-endpoint-clearance.js → nexus/endpoint-clearance.js} +2 -2
  278. package/dist/nexus/endpoint-clearance.js.map +1 -0
  279. package/dist/{nexus-escalation-bridge.d.ts → nexus/escalation-bridge.d.ts} +1 -1
  280. package/dist/nexus/escalation-bridge.d.ts.map +1 -0
  281. package/dist/{nexus-escalation-bridge.js → nexus/escalation-bridge.js} +2 -2
  282. package/dist/nexus/escalation-bridge.js.map +1 -0
  283. package/dist/{nexus-incoming.d.ts → nexus/incoming.d.ts} +2 -2
  284. package/dist/nexus/incoming.d.ts.map +1 -0
  285. package/dist/{nexus-incoming.js → nexus/incoming.js} +2 -2
  286. package/dist/nexus/incoming.js.map +1 -0
  287. package/dist/nexus/index.d.ts +20 -0
  288. package/dist/nexus/index.d.ts.map +1 -0
  289. package/dist/nexus/index.js +20 -0
  290. package/dist/nexus/index.js.map +1 -0
  291. package/dist/{nexus-intake-spawn.d.ts → nexus/intake-spawn.d.ts} +3 -3
  292. package/dist/nexus/intake-spawn.d.ts.map +1 -0
  293. package/dist/{nexus-intake-spawn.js → nexus/intake-spawn.js} +1 -1
  294. package/dist/nexus/intake-spawn.js.map +1 -0
  295. package/dist/{nexus-intake.d.ts → nexus/intake.d.ts} +2 -2
  296. package/dist/nexus/intake.d.ts.map +1 -0
  297. package/dist/{nexus-intake.js → nexus/intake.js} +4 -4
  298. package/dist/nexus/intake.js.map +1 -0
  299. package/dist/{nexus-return.d.ts → nexus/return.d.ts} +3 -3
  300. package/dist/nexus/return.d.ts.map +1 -0
  301. package/dist/{nexus-return.js → nexus/return.js} +4 -4
  302. package/dist/nexus/return.js.map +1 -0
  303. package/dist/otlp/index.d.ts +19 -0
  304. package/dist/otlp/index.d.ts.map +1 -0
  305. package/dist/otlp/index.js +19 -0
  306. package/dist/otlp/index.js.map +1 -0
  307. package/dist/{otlp-log-parser.d.ts → otlp/log-parser.d.ts} +2 -2
  308. package/dist/otlp/log-parser.d.ts.map +1 -0
  309. package/dist/{otlp-log-parser.js → otlp/log-parser.js} +5 -6
  310. package/dist/otlp/log-parser.js.map +1 -0
  311. package/dist/{otlp-metric-parser.d.ts → otlp/metric-parser.d.ts} +2 -2
  312. package/dist/otlp/metric-parser.d.ts.map +1 -0
  313. package/dist/{otlp-metric-parser.js → otlp/metric-parser.js} +4 -5
  314. package/dist/otlp/metric-parser.js.map +1 -0
  315. package/dist/{otlp-port-manager.d.ts → otlp/port-manager.d.ts} +1 -1
  316. package/dist/otlp/port-manager.d.ts.map +1 -0
  317. package/dist/{otlp-port-manager.js → otlp/port-manager.js} +1 -1
  318. package/dist/otlp/port-manager.js.map +1 -0
  319. package/dist/{otlp-receiver.d.ts → otlp/receiver.d.ts} +2 -2
  320. package/dist/otlp/receiver.d.ts.map +1 -0
  321. package/dist/{otlp-receiver.js → otlp/receiver.js} +5 -33
  322. package/dist/otlp/receiver.js.map +1 -0
  323. package/dist/{otlp-types.d.ts → otlp/types.d.ts} +1 -9
  324. package/dist/otlp/types.d.ts.map +1 -0
  325. package/dist/{otlp-types.js → otlp/types.js} +1 -27
  326. package/dist/otlp/types.js.map +1 -0
  327. package/dist/pipeline-config.d.ts +1 -1
  328. package/dist/{prd-controller.d.ts → prd/controller.d.ts} +7 -7
  329. package/dist/prd/controller.d.ts.map +1 -0
  330. package/dist/{prd-controller.js → prd/controller.js} +11 -11
  331. package/dist/prd/controller.js.map +1 -0
  332. package/dist/{prd-divergence.d.ts → prd/divergence.d.ts} +2 -2
  333. package/dist/prd/divergence.d.ts.map +1 -0
  334. package/dist/{prd-divergence.js → prd/divergence.js} +1 -1
  335. package/dist/prd/divergence.js.map +1 -0
  336. package/dist/{prd-focus-pushdown.d.ts → prd/focus-pushdown.d.ts} +2 -2
  337. package/dist/prd/focus-pushdown.d.ts.map +1 -0
  338. package/dist/{prd-focus-pushdown.js → prd/focus-pushdown.js} +3 -3
  339. package/dist/prd/focus-pushdown.js.map +1 -0
  340. package/dist/{prd-frontier.d.ts → prd/frontier.d.ts} +2 -2
  341. package/dist/prd/frontier.d.ts.map +1 -0
  342. package/dist/{prd-frontier.js → prd/frontier.js} +1 -1
  343. package/dist/prd/frontier.js.map +1 -0
  344. package/dist/prd/index.d.ts +26 -0
  345. package/dist/prd/index.d.ts.map +1 -0
  346. package/dist/prd/index.js +25 -0
  347. package/dist/prd/index.js.map +1 -0
  348. package/dist/{prd-steward.d.ts → prd/steward.d.ts} +4 -4
  349. package/dist/prd/steward.d.ts.map +1 -0
  350. package/dist/{prd-steward.js → prd/steward.js} +4 -4
  351. package/dist/prd/steward.js.map +1 -0
  352. package/dist/{prd-viability.d.ts → prd/viability.d.ts} +2 -2
  353. package/dist/prd/viability.d.ts.map +1 -0
  354. package/dist/{prd-viability.js → prd/viability.js} +1 -1
  355. package/dist/prd/viability.js.map +1 -0
  356. package/dist/product-scaffold.js +2 -2
  357. package/dist/product-scaffold.js.map +1 -1
  358. package/dist/prompt-contradictions.d.ts +55 -0
  359. package/dist/prompt-contradictions.d.ts.map +1 -0
  360. package/dist/prompt-contradictions.js +114 -0
  361. package/dist/prompt-contradictions.js.map +1 -0
  362. package/dist/prompt-listing.d.ts +19 -0
  363. package/dist/prompt-listing.d.ts.map +1 -1
  364. package/dist/prompt-listing.js +49 -0
  365. package/dist/prompt-listing.js.map +1 -1
  366. package/dist/prompt-sections/execution-sections.d.ts +3 -2
  367. package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
  368. package/dist/prompt-sections/execution-sections.js +70 -97
  369. package/dist/prompt-sections/execution-sections.js.map +1 -1
  370. package/dist/prompt-sections/persona-sections.d.ts +12 -0
  371. package/dist/prompt-sections/persona-sections.d.ts.map +1 -1
  372. package/dist/prompt-sections/persona-sections.js +29 -2
  373. package/dist/prompt-sections/persona-sections.js.map +1 -1
  374. package/dist/prompt-sections/role-sections.js +2 -2
  375. package/dist/prompt-sections/role-sections.js.map +1 -1
  376. package/dist/queries/context-assembly.d.ts +14 -4
  377. package/dist/queries/context-assembly.d.ts.map +1 -1
  378. package/dist/queries/context-assembly.js +14 -4
  379. package/dist/queries/context-assembly.js.map +1 -1
  380. package/dist/queries/context-lock-queries.js +1 -1
  381. package/dist/queries/context-lock-queries.js.map +1 -1
  382. package/dist/queries/deliveries.d.ts +45 -0
  383. package/dist/queries/deliveries.d.ts.map +1 -1
  384. package/dist/queries/deliveries.js +2 -0
  385. package/dist/queries/deliveries.js.map +1 -1
  386. package/dist/queries/focuses.d.ts +17 -7
  387. package/dist/queries/focuses.d.ts.map +1 -1
  388. package/dist/queries/focuses.js +27 -9
  389. package/dist/queries/focuses.js.map +1 -1
  390. package/dist/queries/index.d.ts +1 -1
  391. package/dist/queries/index.d.ts.map +1 -1
  392. package/dist/queries/index.js +1 -1
  393. package/dist/queries/index.js.map +1 -1
  394. package/dist/queries/schemas.d.ts +4 -0
  395. package/dist/queries/schemas.d.ts.map +1 -1
  396. package/dist/queries/schemas.js +8 -0
  397. package/dist/queries/schemas.js.map +1 -1
  398. package/dist/reality-pole.js +1 -1
  399. package/dist/resolvers/escalation-answers.d.ts +40 -0
  400. package/dist/resolvers/escalation-answers.d.ts.map +1 -1
  401. package/dist/resolvers/escalation-answers.js +54 -35
  402. package/dist/resolvers/escalation-answers.js.map +1 -1
  403. package/dist/resolvers/git-diff-against-base.d.ts.map +1 -1
  404. package/dist/resolvers/git-diff-against-base.js +4 -1
  405. package/dist/resolvers/git-diff-against-base.js.map +1 -1
  406. package/dist/resolvers/prd-context.js +1 -1
  407. package/dist/resolvers/shared/wiki.d.ts +27 -0
  408. package/dist/resolvers/shared/wiki.d.ts.map +1 -1
  409. package/dist/resolvers/shared/wiki.js +39 -0
  410. package/dist/resolvers/shared/wiki.js.map +1 -1
  411. package/dist/sensor-sweep.js +1 -1
  412. package/dist/spawn/delivered-manifest.d.ts +42 -0
  413. package/dist/spawn/delivered-manifest.d.ts.map +1 -0
  414. package/dist/spawn/delivered-manifest.js +64 -0
  415. package/dist/spawn/delivered-manifest.js.map +1 -0
  416. package/dist/spawn/model-routing.d.ts +277 -0
  417. package/dist/spawn/model-routing.d.ts.map +1 -0
  418. package/dist/spawn/model-routing.js +464 -0
  419. package/dist/spawn/model-routing.js.map +1 -0
  420. package/dist/spawn/process-spawn.d.ts +137 -0
  421. package/dist/spawn/process-spawn.d.ts.map +1 -0
  422. package/dist/spawn/process-spawn.js +385 -0
  423. package/dist/spawn/process-spawn.js.map +1 -0
  424. package/dist/spawn/prompt-assembly.d.ts +170 -0
  425. package/dist/spawn/prompt-assembly.d.ts.map +1 -0
  426. package/dist/spawn/prompt-assembly.js +272 -0
  427. package/dist/spawn/prompt-assembly.js.map +1 -0
  428. package/dist/spawn/shared.d.ts +96 -0
  429. package/dist/spawn/shared.d.ts.map +1 -0
  430. package/dist/spawn/shared.js +14 -0
  431. package/dist/spawn/shared.js.map +1 -0
  432. package/dist/spawn/spawn-decision.d.ts +239 -0
  433. package/dist/spawn/spawn-decision.d.ts.map +1 -0
  434. package/dist/spawn/spawn-decision.js +437 -0
  435. package/dist/spawn/spawn-decision.js.map +1 -0
  436. package/dist/spawn/worktree-session.d.ts +105 -0
  437. package/dist/spawn/worktree-session.d.ts.map +1 -0
  438. package/dist/spawn/worktree-session.js +216 -0
  439. package/dist/spawn/worktree-session.js.map +1 -0
  440. package/dist/spawner/lifecycle.js +5 -5
  441. package/dist/spawner/lifecycle.js.map +1 -1
  442. package/dist/spawner/resolution.js +4 -4
  443. package/dist/spawner/resolution.js.map +1 -1
  444. package/dist/stall-detector-config.d.ts +4 -0
  445. package/dist/stall-detector-config.d.ts.map +1 -1
  446. package/dist/stall-detector-config.js +6 -0
  447. package/dist/stall-detector-config.js.map +1 -1
  448. package/dist/state-cascade.js +1 -1
  449. package/dist/state-cascade.js.map +1 -1
  450. package/dist/trigger-executor.d.ts +5 -9
  451. package/dist/trigger-executor.d.ts.map +1 -1
  452. package/dist/trigger-executor.js +14 -36
  453. package/dist/trigger-executor.js.map +1 -1
  454. package/dist/types/config.d.ts +44 -0
  455. package/dist/types/config.d.ts.map +1 -1
  456. package/dist/types/focus.d.ts +13 -0
  457. package/dist/types/focus.d.ts.map +1 -1
  458. package/dist/types/workflow.d.ts +1 -3
  459. package/dist/types/workflow.d.ts.map +1 -1
  460. package/dist/unified-engine-lifecycle.js +1 -1
  461. package/dist/unified-init.d.ts +3 -3
  462. package/dist/unified-init.d.ts.map +1 -1
  463. package/dist/unified-init.js +9 -25
  464. package/dist/unified-init.js.map +1 -1
  465. package/dist/unified-shell.d.ts +3 -3
  466. package/dist/unified-shell.js +4 -4
  467. package/dist/unified-shell.js.map +1 -1
  468. package/dist/verification-baseline.d.ts +112 -0
  469. package/dist/verification-baseline.d.ts.map +1 -0
  470. package/dist/verification-baseline.js +264 -0
  471. package/dist/verification-baseline.js.map +1 -0
  472. package/dist/verification-config.d.ts +53 -0
  473. package/dist/verification-config.d.ts.map +1 -0
  474. package/dist/verification-config.js +72 -0
  475. package/dist/verification-config.js.map +1 -0
  476. package/dist/verification-default-on.d.ts +87 -0
  477. package/dist/verification-default-on.d.ts.map +1 -0
  478. package/dist/verification-default-on.js +186 -0
  479. package/dist/verification-default-on.js.map +1 -0
  480. package/dist/verification-deterministic.d.ts +56 -0
  481. package/dist/verification-deterministic.d.ts.map +1 -0
  482. package/dist/verification-deterministic.js +123 -0
  483. package/dist/verification-deterministic.js.map +1 -0
  484. package/dist/verification-engine.d.ts +22 -15
  485. package/dist/verification-engine.d.ts.map +1 -1
  486. package/dist/verification-engine.js +94 -65
  487. package/dist/verification-engine.js.map +1 -1
  488. package/dist/verification-failure-parser.d.ts +48 -0
  489. package/dist/verification-failure-parser.d.ts.map +1 -0
  490. package/dist/verification-failure-parser.js +102 -0
  491. package/dist/verification-failure-parser.js.map +1 -0
  492. package/dist/verification-feedback.d.ts +58 -0
  493. package/dist/verification-feedback.d.ts.map +1 -0
  494. package/dist/verification-feedback.js +110 -0
  495. package/dist/verification-feedback.js.map +1 -0
  496. package/dist/{worktree-focus.d.ts → worktree/focus.d.ts} +2 -2
  497. package/dist/worktree/focus.d.ts.map +1 -0
  498. package/dist/{worktree-focus.js → worktree/focus.js} +7 -7
  499. package/dist/worktree/focus.js.map +1 -0
  500. package/dist/worktree/index.d.ts +16 -0
  501. package/dist/worktree/index.d.ts.map +1 -0
  502. package/dist/worktree/index.js +16 -0
  503. package/dist/worktree/index.js.map +1 -0
  504. package/dist/{worktree-safety.d.ts → worktree/safety.d.ts} +1 -1
  505. package/dist/worktree/safety.d.ts.map +1 -0
  506. package/dist/{worktree-safety.js → worktree/safety.js} +2 -2
  507. package/dist/worktree/safety.js.map +1 -0
  508. package/dist/{worktree-state.d.ts → worktree/state.d.ts} +1 -1
  509. package/dist/worktree/state.d.ts.map +1 -0
  510. package/dist/{worktree-state.js → worktree/state.js} +6 -6
  511. package/dist/worktree/state.js.map +1 -0
  512. package/dist/worktree.d.ts +1 -1
  513. package/dist/worktree.js +5 -5
  514. package/dist/worktree.js.map +1 -1
  515. package/package.json +5 -4
  516. package/dist/git-activity.d.ts.map +0 -1
  517. package/dist/git-activity.js.map +0 -1
  518. package/dist/git-branch.d.ts.map +0 -1
  519. package/dist/git-branch.js.map +0 -1
  520. package/dist/git-fetch.d.ts.map +0 -1
  521. package/dist/git-fetch.js.map +0 -1
  522. package/dist/git-genesis.d.ts.map +0 -1
  523. package/dist/git-genesis.js.map +0 -1
  524. package/dist/git-integration.d.ts.map +0 -1
  525. package/dist/git-integration.js.map +0 -1
  526. package/dist/git-merge-helpers.d.ts.map +0 -1
  527. package/dist/git-merge-helpers.js.map +0 -1
  528. package/dist/git-merge-lock.d.ts.map +0 -1
  529. package/dist/git-merge-lock.js.map +0 -1
  530. package/dist/git-merge-strategies.d.ts.map +0 -1
  531. package/dist/git-merge-strategies.js.map +0 -1
  532. package/dist/git-merge.d.ts.map +0 -1
  533. package/dist/git-merge.js.map +0 -1
  534. package/dist/git-state-detector.d.ts.map +0 -1
  535. package/dist/git-state-detector.js.map +0 -1
  536. package/dist/git-types.d.ts.map +0 -1
  537. package/dist/git-types.js.map +0 -1
  538. package/dist/git-utils.d.ts.map +0 -1
  539. package/dist/git-utils.js.map +0 -1
  540. package/dist/loop-context-audit-pass.d.ts.map +0 -1
  541. package/dist/loop-context-audit-pass.js.map +0 -1
  542. package/dist/loop-engine-types.d.ts.map +0 -1
  543. package/dist/loop-engine-types.js.map +0 -1
  544. package/dist/loop-engine.d.ts.map +0 -1
  545. package/dist/loop-engine.js.map +0 -1
  546. package/dist/loop-event-bus.d.ts.map +0 -1
  547. package/dist/loop-event-bus.js.map +0 -1
  548. package/dist/loop-llm-client.d.ts.map +0 -1
  549. package/dist/loop-llm-client.js.map +0 -1
  550. package/dist/loop-llm-flag.d.ts.map +0 -1
  551. package/dist/loop-llm-flag.js.map +0 -1
  552. package/dist/loop-operational-block.d.ts.map +0 -1
  553. package/dist/loop-operational-block.js.map +0 -1
  554. package/dist/loop-prompt-builder.d.ts.map +0 -1
  555. package/dist/loop-prompt-builder.js.map +0 -1
  556. package/dist/loop-variance.d.ts.map +0 -1
  557. package/dist/loop-variance.js.map +0 -1
  558. package/dist/nexus-endpoint-clearance.d.ts.map +0 -1
  559. package/dist/nexus-endpoint-clearance.js.map +0 -1
  560. package/dist/nexus-escalation-bridge.d.ts.map +0 -1
  561. package/dist/nexus-escalation-bridge.js.map +0 -1
  562. package/dist/nexus-incoming.d.ts.map +0 -1
  563. package/dist/nexus-incoming.js.map +0 -1
  564. package/dist/nexus-intake-spawn.d.ts.map +0 -1
  565. package/dist/nexus-intake-spawn.js.map +0 -1
  566. package/dist/nexus-intake.d.ts.map +0 -1
  567. package/dist/nexus-intake.js.map +0 -1
  568. package/dist/nexus-return.d.ts.map +0 -1
  569. package/dist/nexus-return.js.map +0 -1
  570. package/dist/otlp-log-parser.d.ts.map +0 -1
  571. package/dist/otlp-log-parser.js.map +0 -1
  572. package/dist/otlp-metric-parser.d.ts.map +0 -1
  573. package/dist/otlp-metric-parser.js.map +0 -1
  574. package/dist/otlp-port-manager.d.ts.map +0 -1
  575. package/dist/otlp-port-manager.js.map +0 -1
  576. package/dist/otlp-receiver.d.ts.map +0 -1
  577. package/dist/otlp-receiver.js.map +0 -1
  578. package/dist/otlp-types.d.ts.map +0 -1
  579. package/dist/otlp-types.js.map +0 -1
  580. package/dist/prd-controller.d.ts.map +0 -1
  581. package/dist/prd-controller.js.map +0 -1
  582. package/dist/prd-divergence.d.ts.map +0 -1
  583. package/dist/prd-divergence.js.map +0 -1
  584. package/dist/prd-focus-pushdown.d.ts.map +0 -1
  585. package/dist/prd-focus-pushdown.js.map +0 -1
  586. package/dist/prd-frontier.d.ts.map +0 -1
  587. package/dist/prd-frontier.js.map +0 -1
  588. package/dist/prd-steward.d.ts.map +0 -1
  589. package/dist/prd-steward.js.map +0 -1
  590. package/dist/prd-viability.d.ts.map +0 -1
  591. package/dist/prd-viability.js.map +0 -1
  592. package/dist/worktree-focus.d.ts.map +0 -1
  593. package/dist/worktree-focus.js.map +0 -1
  594. package/dist/worktree-safety.d.ts.map +0 -1
  595. package/dist/worktree-safety.js.map +0 -1
  596. package/dist/worktree-state.d.ts.map +0 -1
  597. package/dist/worktree-state.js.map +0 -1
@@ -27,72 +27,54 @@
27
27
  * When tracing a symbol imported from this file, check those modules first;
28
28
  * only the spawn choreography itself is implemented here.
29
29
  */
30
- import { spawn } from 'node:child_process';
31
30
  import { mkdirSync, existsSync } from 'node:fs';
32
- import { hostname } from 'node:os';
33
- import { createSession, updateSession, getActiveFocuses, } from './supabase.js';
34
- import { createWorktree, runGitSync, repoHasCommits } from './git.js';
35
- import { installReadOnlyAuditGuards } from './audit-hooks.js';
36
- import { getFocusWorktree, setFocusWorktree } from './focus-worktree-state.js';
37
- import { withRetry, productLabel, loadEnvConfig, ESCALATION_REASONS } from '@telora/daemon-core';
38
- import { createEscalation } from './queries/issues.js';
31
+ import { updateSession, } from './supabase.js';
32
+ import { productLabel } from '@telora/daemon-core';
39
33
  import { recordActivity } from './heartbeat.js';
40
- import { buildFocusTeamPrompt } from './focus-prompt-builder.js';
41
- import { buildRoleFrameworkPrompt, buildRemediationFramingSection, buildReviewOutcomeStampingSection, buildReviewSensorAuditSection, buildResumeDeltaMessage } from './team-prompt-base.js';
42
- import { getFocusDeliveries, getFocusIssues, getProductContextForFocus, getProductDeploymentProfileSnapshot, fetchFocusWorkflow } from './queries/focuses.js';
43
34
  import { applyRespawnGuard } from './focus-respawn-guard.js';
44
- import { buildLeveredSpawnEnvironment, fetchBackendEnvExtras } from './spawn-environment.js';
45
- import { wrapSpawnCommand, SandboxUnavailableError } from './spawn-sandbox.js';
46
- import { sanitizeGitSegment, resolveRef } from './git-utils.js';
47
- import { resolveResumeId, resolveLineageSpec } from './session-lineage.js';
48
- import { resolveTeamModel, resolveLocalModelRefPrecedence } from './model-resolution.js';
49
- import { recordFocusTeardown } from './spawn-cooldown.js';
50
- import { recordWorkerPid, removeWorkerPid } from './worker-pidfile.js';
51
- import { markSessionPersisted } from './telemetry-writer.js';
52
- import { consumePendingSpawnDirective, assembleDirectiveContentWithManifest, syncFocusPhase, isAgentlessDirective } from './directive-executor.js';
53
- import { resolveAssemblyRecipeWithManifest } from './assembly-engine.js';
54
- import { resolveFocusMilestoneContext } from './prd-focus-pushdown.js';
55
- import { insertAssemblyManifest } from './queries/context-assembly.js';
35
+ import { sanitizeGitSegment, resolveRef } from './git/index.js';
36
+ import { resolveResumeId } from './session-lineage.js';
56
37
  // Side-effect import: registers assembly resolvers with assembly-engine at module load. Required - do not remove.
57
38
  import './assembly-resolvers.js';
58
- import { isStatusAgentActionable, isStatusBlocking } from './stage-classifier.js';
59
- import { filterReviewFiledIssues } from './review-defect-detector.js';
60
- import { getActiveFocuses as fetchActiveFocusesForReviewState } from './queries/deliveries.js';
61
- import { getLocalModel, getLocalModelSecret } from './queries/local-models.js';
62
- import { codexSafeModelAlias, writeLocalModelCodexHome, TELORA_LOCAL_API_KEY_ENV, } from './cli/codex-provision.js';
63
- import { preflightLocalModelMcp } from './backends/codex/mcp-preflight.js';
64
- import { startCodexMcpProxy, shouldRouteLocalModelThroughProxy, } from './backends/codex/codex-mcp-proxy.js';
65
- import { detectHostCapability, ensureLoaded, resolveSafeLoadOptions, getStatus, decideSwap, hostRootFromBaseUrl, observeHostSwap, claimResidentHost, releaseHostByFocus, detectSingleHostModelConflict, shouldSurfaceHostConflictOnce, } from './host-control/index.js';
39
+ import { releaseHostByFocus } from './host-control/index.js';
66
40
  // ── Imports from extracted modules ──────────────────────────────────────
67
- import { getActiveTeams, hasActiveTeam, deriveExecutionConfig, } from './focus-team-state.js';
41
+ import { getActiveTeams, deriveExecutionConfig, } from './focus-team-state.js';
68
42
  // Import the spawn helpers from their defining module (NOT the spawner barrel):
69
43
  // the barrel re-exports spawnFocusTeam from THIS module via the spawn-team
70
44
  // shim, so importing the barrel here would close a static import cycle.
71
- import { setupTeamLogStreams, attachStreamHandlers, handleTeamProcessClose, } from './spawner/helpers.js';
72
- import { terminateTeam, } from './completion/index.js';
73
- import { handleCompletionEvent, } from './completion/index.js';
74
- import { getBackend, resolveBackend } from './backends/index.js';
45
+ import { setupTeamLogStreams, } from './spawner/helpers.js';
46
+ // Model-routing seam (extracted to ./spawn/model-routing.js). Imported here for
47
+ // the two symbols spawnFocusTeam calls directly; the full moved surface is
48
+ // re-exported below so external importers keep resolving it from this module.
49
+ import { resolveSpawnModel, surfaceSingleHostModelConflict } from './spawn/model-routing.js';
50
+ // Prompt-assembly seam (extracted to ./spawn/prompt-assembly.js). Imported here
51
+ // for the symbols spawnFocusTeam calls directly; the full moved surface is
52
+ // re-exported below so external importers keep resolving it from this module.
53
+ import { buildFocusPromptContext, buildSpawnPrompt, buildSpawnMessage, resolveScopingSpawnResume, } from './spawn/prompt-assembly.js';
54
+ // Worktree-session seam (extracted to ./spawn/worktree-session.js). Imported here
55
+ // for the symbols spawnFocusTeam calls directly; the full moved surface is
56
+ // re-exported below so external importers keep resolving it from this module.
57
+ import { createInitialTeamState, resolveSpawnWorktree, createTeamSession, resolveReviewContext, resolveEngineForPass, } from './spawn/worktree-session.js';
58
+ // Spawn-decision seam (extracted to ./spawn/spawn-decision.js). Imported here for
59
+ // the symbols spawnFocusTeam calls directly; the full moved surface is re-exported
60
+ // below so external importers keep resolving it from this module.
61
+ import { fetchSpawnContext, resolveActionableDeliveries, resolveDirectiveOrFallback, resolveLegacyLoopContext, } from './spawn/spawn-decision.js';
62
+ // Process-spawn seam (extracted to ./spawn/process-spawn.js). Imported here for
63
+ // spawnTeamProcess, which the orchestrator spawnFocusTeam calls directly; the
64
+ // full moved surface is re-exported below so external importers (and tests) keep
65
+ // resolving it from this module.
66
+ import { spawnTeamProcess } from './spawn/process-spawn.js';
67
+ import { buildDeliveredManifest } from './spawn/delivered-manifest.js';
75
68
  // ── Re-exports for backward compatibility ───────────────────────────────
69
+ export { selectLocalModelRef, codexHomeForSpawn, decideLocalModelRouting, shouldSwapForPass, resolveEffectiveModel, isClaudeModelId, selectPipelineModelForSpawn, selectDirectiveModelForSpawn, resolveLocalModelConfig, surfaceSingleHostModelConflict, resolveSpawnModel, buildAgentArgs, } from './spawn/model-routing.js';
70
+ export { buildFocusPromptContext, buildSpawnPrompt, buildSpawnMessage, resolveScopingSpawnResume, findQueuedDeliveriesWithoutTaskIssues, buildQueuedDeliveryScopingMessage, } from './spawn/prompt-assembly.js';
71
+ export { createInitialTeamState, resolveSpawnWorktree, createTeamSession, resolveReviewContext, resolveEngineForPass, } from './spawn/worktree-session.js';
72
+ export { findActionableDeliveries, resolveActionableDeliveries, resolveDirectiveOrFallback, fetchSpawnContext, DEFAULT_LOOP_CONTEXT_RECIPE, } from './spawn/spawn-decision.js';
73
+ export { gateLocalModelBackendSpawn, backendUnavailableSessionUpdate, spawnCrashIsolated, persistSpawnFailureAfterRunning, closeTeamProxyQuietly, spawnTeamProcess, governorDeniedSessionUpdate, sandboxUnavailableSessionUpdate, } from './spawn/process-spawn.js';
76
74
  export { getActiveTeams, hasActiveTeam, getActiveTeamCount, } from './focus-team-state.js';
77
75
  export { mergeFocusBranch, } from './focus-merge.js';
78
76
  export { handleTeamCompletion, advanceDeliveryStatuses, isStatusTerminal, isStatusAgentActionable, } from './completion/index.js';
79
77
  export { terminateTeam, waitForTeamExit, terminateAllTeams, detectDeactivatedFocuses, checkAndMergeCompletedDeliveries, } from './completion/index.js';
80
- /**
81
- * Default loop-context assembly recipe for a focus-team spawn.
82
- *
83
- * Composed once per spawn into the team prompt. `focus.endpoint_catalog`
84
- * is placed right after `focus.injections` so the exposed/consumed endpoint
85
- * catalog reads alongside the tree context -- surfacing cross-product
86
- * interrelations at scoping time (shift-left) so a dependency on another
87
- * product can be mapped into the reality tree as an externally-owned obstacle
88
- * rather than surfacing reactively as a blocked_by_external escalation.
89
- */
90
- export const DEFAULT_LOOP_CONTEXT_RECIPE = [
91
- 'loop.context',
92
- 'focus.injections',
93
- 'focus.endpoint_catalog',
94
- 'escalation.answers',
95
- ];
96
78
  // ── Resource governor (optional, injected by FocusEngine) ─────────
97
79
  let governor = null;
98
80
  /** Inject the resource governor for global concurrency limiting. */
@@ -107,69 +89,6 @@ export function initGovernor(gov) {
107
89
  export function getGovernor() {
108
90
  return governor;
109
91
  }
110
- /**
111
- * Pre-spawn guard: rank-ordered walk that collects every agent-actionable
112
- * delivery into the spawn set.
113
- *
114
- * "Actionable" and "blocking" are independent properties of a status:
115
- *
116
- * - actionable = `isStatusAgentActionable(status)` -- queued | coding |
117
- * verify_failed. Work the team can pick up.
118
- * - blocking = `isStatusBlocking(status)` -- statuses that gate
119
- * subsequent ranks (queued, coding, planning, paused).
120
- *
121
- * A delivery may be actionable and blocking (queued, coding -- the common
122
- * case), actionable and non-blocking (`verify_failed` -- has remediation
123
- * work from a review pass, but the next-ranked queued delivery should not
124
- * wait on it), or non-actionable and blocking (planning, paused -- gates
125
- * the queue and the team has nothing to do here).
126
- *
127
- * Walk semantics:
128
- * - Collect every actionable delivery into the spawn set.
129
- * - Stop the walk when we hit the first blocking-and-not-actionable status
130
- * (planning, paused). Lower-ranked deliveries past this point are gated.
131
- * - Skip past non-blocking non-actionable statuses (done, cancelled, verify)
132
- * without breaking the walk.
133
- *
134
- * Returns:
135
- * - 'planning' when the focus has zero deliveries (agent will scope them).
136
- * - 'execute' with the collected actionable deliveries.
137
- * - 'skip' when the walk found zero actionable deliveries.
138
- *
139
- * Why verify_failed must be in the spawn set: the auto-review loop
140
- * routes a delivery from `verify` -> `verify_failed` when the review
141
- * agent finds remediation work, clears `review_requested_at`, and emits a
142
- * `review_completed` loop trigger. The next poll re-derives phase from
143
- * `ACTIVE_STATUSES` (which includes verify_failed), so the focus is
144
- * eligible to spawn. If this guard then skips past `verify_failed`, no
145
- * team ever picks up the remediation issues and the loop dies after the
146
- * first pass. See delivery ac5c69f7.
147
- */
148
- export function findActionableDeliveries(deliveries, focusName) {
149
- if (deliveries.length === 0) {
150
- return { kind: 'planning' };
151
- }
152
- const actionable = [];
153
- for (const d of deliveries) {
154
- const status = d.executionStatus ?? '';
155
- if (isStatusAgentActionable(status)) {
156
- actionable.push(d);
157
- continue;
158
- }
159
- if (isStatusBlocking(status)) {
160
- console.log(`[focus-executor] Delivery "${d.name}" (rank ${d.priorityRank}) ` +
161
- `is in "${status}" state -- blocking subsequent deliveries in focus "${focusName}"`);
162
- break;
163
- }
164
- // non-blocking + non-actionable (done, cancelled, verify):
165
- // skip past without gating
166
- }
167
- if (actionable.length === 0) {
168
- console.log(`[focus-executor] No actionable deliveries for focus "${focusName}" -- skipping spawn`);
169
- return { kind: 'skip' };
170
- }
171
- return { kind: 'execute', deliveries: actionable };
172
- }
173
92
  // ── Team spawning ────────────────────────────────────────────────────
174
93
  /**
175
94
  * Generate branch name for focus-level work.
@@ -180,601 +99,6 @@ export function generateFocusBranchName(role, focusName, focusId) {
180
99
  const sanitizedFocusName = sanitizeGitSegment(focusName);
181
100
  return `agent/${sanitizedRoleName}/${sanitizedFocusName}-${shortId}`;
182
101
  }
183
- /**
184
- * Resolve the isolated CODEX_HOME to set on a spawn, or undefined when none
185
- * applies. Pure + testable: only a codex-engine spawn against a product that
186
- * carries a provisioned codexHome gets the override; every other case (claude
187
- * engine, or a cloud product with no codexHome) returns undefined so the spawn
188
- * environment is byte-for-byte unchanged.
189
- */
190
- export function codexHomeForSpawn(engineId, config) {
191
- return engineId === 'codex' && config.codexHome ? config.codexHome : undefined;
192
- }
193
- /**
194
- * Select the per-pass local model ref to resolve for this spawn, or null.
195
- *
196
- * Pure + testable. A ref applies ONLY to a codex-engine spawn. The ref resolves
197
- * under the unified Focus > Workflow > Default precedence (single-sourced in
198
- * model-resolution.resolveLocalModelRefPrecedence): the focus's per-pass
199
- * localModelOverride entry wins; absent that, the stage directive's localModel
200
- * is inherited; absent both, null (the pass runs on the engine's default model).
201
- * A claude engine or a blank ref returns null so the spawn keeps its
202
- * per-product / engine-default behavior unchanged.
203
- */
204
- export function selectLocalModelRef(engineId, pass, localModelOverride, stageDirectiveLocalModel) {
205
- if (engineId !== 'codex')
206
- return null;
207
- return resolveLocalModelRefPrecedence(localModelOverride?.[pass], stageDirectiveLocalModel);
208
- }
209
- export function decideLocalModelRouting(ref, coords, apiKey) {
210
- if (!ref)
211
- return { kind: 'none' };
212
- if (!coords)
213
- return { kind: 'fallback' };
214
- return { kind: 'route', coords, apiKey: apiKey?.trim() ? apiKey : null };
215
- }
216
- /**
217
- * Pure gate: should the daemon perform a host-control model swap before this
218
- * spawn? (Local model host control, D2.) True ONLY when routing resolved a local
219
- * model for this pass AND the host advertises the optional control capability.
220
- * A non-controllable host (today's behavior: the operator pre-loads the model in
221
- * the studio) yields false -> NO swap, spawn proceeds unchanged. A pass with no
222
- * routed local model (claude, no override) also yields false. Exported so the
223
- * load-or-skip decision is unit-testable with NO HTTP.
224
- */
225
- export function shouldSwapForPass(input) {
226
- return input.routing.kind === 'route' && input.capability.controllable;
227
- }
228
- /**
229
- * Resolve the effective model id for a spawn, applying the single team-model
230
- * precedence (focus override -> stage directive -> CLI default) via
231
- * resolveTeamModel. This is the SAME helper used at stage respawn, so the two
232
- * paths can never diverge on ordering.
233
- *
234
- * The focus-override slot folds in the selected local-Codex model: when this
235
- * pass routes to a controllable Codex home, `codexModel` IS the focus's
236
- * explicit pick and must win exactly like a focus pipeline_config.model would.
237
- * `directiveModel` has already been filtered through
238
- * selectDirectiveModelForSpawn() (nulled when the backend cannot run it, or
239
- * when a focus override is present), so it only ever reaches here as a
240
- * legitimate stage fallback.
241
- *
242
- * Pure + testable. Used by spawnFocusTeam to produce effectivePipelineConfig.model.
243
- */
244
- export function resolveEffectiveModel(directiveModel, spawnCodexHome, codexModel, pipelineModel) {
245
- const focusOverrideModel = (spawnCodexHome && codexModel ? codexModel : null) ?? pipelineModel ?? null;
246
- return resolveTeamModel(focusOverrideModel, directiveModel);
247
- }
248
- /**
249
- * A cloud model id the Claude backend can run: the full claude-* ids
250
- * (claude-opus-4-8, claude-sonnet-4-6, ...) AND the bare aliases Claude Code
251
- * accepts (opus, sonnet, haiku, fable -- used across the daemon, e.g.
252
- * audit-phase's AUDIT_DEFAULT_MODEL='opus'). Anything else (gpt-5-codex,
253
- * gemma-4-12b, ...) is treated as not-Claude so it cannot reach the claude CLI.
254
- */
255
- const CLAUDE_MODEL_PREFIXES = ['claude', 'opus', 'sonnet', 'haiku', 'fable'];
256
- export function isClaudeModelId(model) {
257
- if (typeof model !== 'string')
258
- return false;
259
- const normalized = model.trim().toLowerCase();
260
- return CLAUDE_MODEL_PREFIXES.some((prefix) => normalized.startsWith(prefix));
261
- }
262
- /**
263
- * Select the pipeline model fallback to feed into resolveEffectiveModel().
264
- *
265
- * Hosted Codex runs through the user's Codex account. Some registry labels
266
- * exposed in the UI (for example gpt-5-codex) are not valid `codex exec
267
- * --model` values for every account, and passing them fails the run before the
268
- * agent starts. Local-model Codex still needs an explicit model, and Claude
269
- * keeps the existing pipeline model behavior.
270
- *
271
- * Engine-compat (the per-pass bleed guard): product_focuses.pipeline_config.model
272
- * is a SINGLE focus-wide value shared across passes, but each pass resolves its
273
- * own engine. A pass whose engine resolved to claude can only run a claude-family
274
- * model -- so a coding-pass codex model (e.g. gpt-5-codex) selected on the focus
275
- * must NOT bleed into a claude review pass. When it would, null it so the claude
276
- * backend picks its own default rather than receiving `--model gpt-5-codex` and
277
- * erroring out. (The symmetric codex case is handled above + by spawnCodexModel
278
- * routing; local-codex models reach the spawn via spawnCodexModel, not here.)
279
- */
280
- export function selectPipelineModelForSpawn(engineId, spawnCodexHome, pipelineModel) {
281
- if (engineId === 'codex' && !spawnCodexHome)
282
- return null;
283
- if (engineId === 'claude' && !isClaudeModelId(pipelineModel))
284
- return null;
285
- return pipelineModel ?? null;
286
- }
287
- /**
288
- * Select whether a workflow directive's model may reach the backend.
289
- *
290
- * The focus pass selection is the source of truth for team launches. Stage
291
- * directives contribute prompt/context; their model field is only a legacy
292
- * fallback when the focus/pass has no selected model and the backend can run
293
- * that model. In particular, never pass a Claude directive model to Codex.
294
- */
295
- export function selectDirectiveModelForSpawn(engineId, directiveModel, focusSelectedModel) {
296
- if (!directiveModel)
297
- return null;
298
- if (focusSelectedModel)
299
- return null;
300
- if (engineId === 'codex')
301
- return null;
302
- return directiveModel;
303
- }
304
- /**
305
- * Build CLI args for the team lead via the selected backend.
306
- *
307
- * Engine-specific flag construction lives in the backend; resume uses the
308
- * backend's dedicated resume-arg builder. Exported for the spawner barrel
309
- * surface (legacy ./team-spawner.js consumers).
310
- */
311
- export function buildAgentArgs(backend, config, worktreePath, pipelineConfig, resumeSessionId, allowedTools, readOnly) {
312
- // allowedTools comes from the focus role's agent_roles.allowed_tools. The
313
- // backend applies least-privilege scoping when non-empty; empty/absent =>
314
- // engine default tool set (documented default policy).
315
- const base = { config, worktreePath, model: pipelineConfig?.model ?? null, allowedTools, readOnly };
316
- return resumeSessionId
317
- ? backend.buildResumeArgs({ ...base, resumeSessionId })
318
- : backend.buildSpawnArgs(base);
319
- }
320
- /**
321
- * Phase 1: fetch all spawn context (deliveries / issues / product-context /
322
- * deployment-profile / loop-context) in one Promise.all, then resolve the PRD
323
- * milestone push-down. Returns null when the context fetch rejects -- the
324
- * caller treats null as "tear down the freshly-inserted team and return"
325
- * (activeTeams.delete). The PRD push-down deliberately propagates on throw,
326
- * matching the pre-extraction behavior (it sits after the try/catch).
327
- *
328
- * Behavior-preserving extraction of the original inline block; the log lines,
329
- * the deployment-profile .catch fallback, the loop-context try/catch fallback,
330
- * and the manifest accumulation order are byte-identical.
331
- */
332
- export async function fetchSpawnContext(config, focusId, focusName) {
333
- // Accumulates the assembly manifest (one entry per resolved source) across
334
- // every recipe composed for this spawn, persisted after the session exists.
335
- const assemblyManifest = [];
336
- // Fetch all deliveries, issues, product context, deployment profile, and loop context
337
- let deliveries;
338
- let issues;
339
- let productContextDocs;
340
- let deploymentProfileSnapshot;
341
- let loopContext;
342
- try {
343
- let loopContextResult;
344
- [deliveries, issues, productContextDocs, deploymentProfileSnapshot, loopContextResult] = await Promise.all([
345
- getFocusDeliveries(focusId),
346
- getFocusIssues(focusId),
347
- getProductContextForFocus(config.productId, focusId),
348
- getProductDeploymentProfileSnapshot(config.productId).catch((err) => {
349
- console.debug(`[focus-executor] Could not fetch deployment profile snapshot (non-fatal):`, err.message);
350
- return null;
351
- }),
352
- (async () => {
353
- try {
354
- const assemblyCtx = {
355
- focusId,
356
- deliveryIds: [],
357
- worktreePath: null,
358
- config,
359
- organizationId: config.organizationId,
360
- productId: config.productId,
361
- };
362
- return await resolveAssemblyRecipeWithManifest(DEFAULT_LOOP_CONTEXT_RECIPE, assemblyCtx);
363
- }
364
- catch (err) {
365
- console.debug(`[focus-executor] Could not fetch loop context (non-fatal):`, err.message);
366
- return { content: '', manifest: [] };
367
- }
368
- })(),
369
- ]);
370
- loopContext = loopContextResult.content;
371
- assemblyManifest.push(...loopContextResult.manifest);
372
- }
373
- catch (err) {
374
- console.error(`[focus-executor] Failed to fetch context for focus "${focusName}":`, err.message);
375
- return null;
376
- }
377
- // PRD push-down: if this focus was armed by a PRD, resolve the parent PRD's
378
- // prd.context (milestone goal/expansion/vision + frontier position) and carry
379
- // it into the team prompt so the focus inherits coherent milestone intent.
380
- // Context ONLY -- the steward persona is never pushed to a coder team. A focus
381
- // not under a PRD yields '' and is unaffected (best-effort, never blocks spawn).
382
- let prdMilestoneContext = '';
383
- {
384
- const milestoneCtx = {
385
- focusId,
386
- deliveryIds: [],
387
- worktreePath: null,
388
- config,
389
- organizationId: config.organizationId,
390
- productId: config.productId,
391
- };
392
- const milestoneResult = await resolveFocusMilestoneContext(focusId, milestoneCtx);
393
- prdMilestoneContext = milestoneResult.content;
394
- assemblyManifest.push(...milestoneResult.manifest);
395
- }
396
- return { deliveries, issues, productContextDocs, deploymentProfileSnapshot, loopContext, prdMilestoneContext, assemblyManifest };
397
- }
398
- /**
399
- * Phase 2: consume any pending spawn directive, enforce the rank-ordered
400
- * pre-spawn guard, and record known-delivery bookkeeping on the team state.
401
- *
402
- * Mutates `teamState` exactly as the original inline block did
403
- * (lastConsumedDirectiveHash, sessionType, planningPhase, knownDeliveryIds,
404
- * deliveryStageIds, mergedDeliveryIds) and emits the same log lines. Returns
405
- * 'skip' for the findActionableDeliveries skip case (the caller owns the
406
- * activeTeams.delete + return so the team-state map stays the orchestrator's
407
- * responsibility). Behavior byte-identical.
408
- */
409
- export function resolveActionableDeliveries(args) {
410
- const { focusId, focusName, deliveries, issues, forceSpawn, teamState } = args;
411
- // Check for pending spawn directive (e.g., review stage spawn).
412
- // Record its content hash on team state so the spawn-guard in
413
- // executeSpawnDirective can detect divergent directives within the 60s
414
- // skip window.
415
- const pendingDirective = consumePendingSpawnDirective(focusId);
416
- if (pendingDirective) {
417
- teamState.lastConsumedDirectiveHash = pendingDirective.contentHash;
418
- teamState.sessionType = pendingDirective.sessionType;
419
- }
420
- // Pre-spawn guard: enforce rank-ordered execution.
421
- // Skip when a pending spawn directive exists -- the directive has its own intent.
422
- let actionableDeliveries = [];
423
- let isPlanningSpawn = false;
424
- if (pendingDirective) {
425
- actionableDeliveries = deliveries.filter(d => isStatusAgentActionable(d.executionStatus ?? ''));
426
- }
427
- else if (forceSpawn) {
428
- // Escalation answer re-arm: spawn to consume a pending answer even when no
429
- // delivery is actionable. Carry whatever deliveries ARE actionable (usually
430
- // none -- the focus exited because everything is verify/done); the
431
- // escalation.answers resolver in the standard recipe injects the answer.
432
- actionableDeliveries = deliveries.filter(d => isStatusAgentActionable(d.executionStatus ?? ''));
433
- console.log(`[focus-executor] Focus "${focusName}" force-spawning to consume escalation answer ` +
434
- `(${actionableDeliveries.length} actionable deliver${actionableDeliveries.length === 1 ? 'y' : 'ies'})`);
435
- }
436
- else {
437
- const decision = findActionableDeliveries(deliveries, focusName);
438
- if (decision.kind === 'skip') {
439
- return { kind: 'skip' };
440
- }
441
- if (decision.kind === 'planning') {
442
- isPlanningSpawn = true;
443
- teamState.planningPhase = true;
444
- console.log(`[focus-executor] Focus "${focusName}" has no deliveries -- spawning planning team`);
445
- }
446
- else {
447
- actionableDeliveries = decision.deliveries;
448
- }
449
- }
450
- // Track known deliveries, pre-populate already-completed ones.
451
- // Empty deliveries (planning spawn) yields zero iterations: knownDeliveryIds
452
- // stays empty until the wake-cycle refresh in handleCompletionEvent picks
453
- // up MCP-written deliveries, at which point planningPhase is cleared.
454
- for (const d of deliveries) {
455
- teamState.knownDeliveryIds.add(d.id);
456
- teamState.deliveryStageIds.set(d.id, d.currentWorkflowStageId);
457
- if (!isStatusAgentActionable(d.executionStatus ?? '') && !isStatusBlocking(d.executionStatus ?? '')) {
458
- teamState.mergedDeliveryIds.add(d.id);
459
- }
460
- }
461
- if (isPlanningSpawn) {
462
- console.log(` Deliveries: 0 (planning required)`);
463
- }
464
- else {
465
- console.log(` Deliveries: ${deliveries.length} (${actionableDeliveries.length} queued)`);
466
- }
467
- console.log(` Issues: ${issues.length}`);
468
- return { kind: 'proceed', pendingDirective, actionableDeliveries, isPlanningSpawn };
469
- }
470
- /**
471
- * Phase A: resolve the team-lead directive body + spawn metadata (no prompt
472
- * yet). Priority: pending spawn directive > current-stage directive > legacy
473
- * full prompt. Pushes any assembled directive manifest entries onto the shared
474
- * `assemblyManifest` accumulator.
475
- *
476
- * Behavior-preserving extraction of the original inline Phase A block -- the
477
- * "Using pending spawn directive" / "Using stage directive" log lines, the
478
- * "LEGACY PROMPT FALLBACK" warning and every fallbackReason string, and the
479
- * directive-manifest accumulation are byte-identical.
480
- */
481
- export async function resolveDirectiveOrFallback(args) {
482
- const { config, focusId, focusName, worktreePath, pendingDirective, assemblyManifest } = args;
483
- let directiveBody = null; // appended after the role framework
484
- let useLegacyFullPrompt = false; // legacy full prompt (no directive)
485
- let directiveModel = null;
486
- // Stage directive engine + local-model (Workflow layer of the unified
487
- // precedence for those two axes); null when the stage pins neither.
488
- let directiveEngine = null;
489
- let directiveLocalModel = null;
490
- // Focus override snapshotted on the pending directive (respawn path only).
491
- let pendingFocusOverrideModel = null;
492
- // Workflow stage active at spawn -- stamped onto telemetry as the .stage lever.
493
- let spawnStageName = null;
494
- // Lineage + continuity declared by whatever is firing this spawn (pending
495
- // directive, current stage directive, or none). Resolved uniformly later
496
- // (INJ-B). For a pending directive these are already resolved upstream, so
497
- // resolveLineageSpec is idempotent.
498
- let spawnDirectiveDeclared = null;
499
- if (pendingDirective && pendingDirective.message.trim()) {
500
- // Spawn directive from directive-executor (e.g., review stage transition)
501
- directiveBody = pendingDirective.message;
502
- directiveModel = pendingDirective.model;
503
- directiveEngine = pendingDirective.engine ?? null;
504
- directiveLocalModel = pendingDirective.localModel ?? null;
505
- pendingFocusOverrideModel = pendingDirective.focusOverrideModel;
506
- spawnStageName = pendingDirective.sessionType === 'review' ? 'reviewing' : null;
507
- spawnDirectiveDeclared = { lineage: pendingDirective.lineage, continuity: pendingDirective.continuity };
508
- console.log(`[focus-executor] Using pending spawn directive for "${focusName}" ` +
509
- `(${pendingDirective.message.length} chars directive, model: ${directiveModel ?? 'default'})`);
510
- }
511
- else {
512
- // Sync the focus's execution phase from its delivery aggregate, then
513
- // assemble the matching stage directive. Every focus workflow stage is
514
- // expected to carry an agent_directive (populated by migration
515
- // 20260503140420_focus_phase_directives); the legacy hardcoded prompt
516
- // path below is a safety net that should not fire under normal
517
- // operation. When it does fire, the warning identifies which guard
518
- // tripped so the gap can be fixed.
519
- let stageDirectiveContent = null;
520
- let stageDirectiveModel = null;
521
- let fallbackReason = null;
522
- try {
523
- const activeFocuses = await getActiveFocuses(config.organizationId, config.productId);
524
- const focusRecord = activeFocuses.find(s => s.focus_id === focusId);
525
- const syncedStageId = focusRecord
526
- ? await syncFocusPhase(focusRecord)
527
- : null;
528
- const workflow = await fetchFocusWorkflow(focusId);
529
- const currentStage = syncedStageId
530
- ? workflow.stages.find(s => s.id === syncedStageId)
531
- : workflow.stages.find(s => s.is_initial);
532
- if (!currentStage) {
533
- fallbackReason = syncedStageId
534
- ? `no stage matching synced id ${syncedStageId.slice(0, 8)} in workflow`
535
- : 'no synced stage and no is_initial stage in workflow';
536
- }
537
- else if (!currentStage.agentDirective || isAgentlessDirective(currentStage.agentDirective)) {
538
- // NULL directive OR an explicit agentless hold (execution: 'none') --
539
- // treat identically: no stage directive drives the spawn, fall back to
540
- // the legacy focus-team prompt. Parity keeps a deliberately-agentless
541
- // stage from being handed to directive assembly.
542
- spawnStageName = currentStage.name ?? null;
543
- fallbackReason = currentStage.agentDirective
544
- ? `stage "${currentStage.name}" is an explicit agentless hold (execution=none)`
545
- : `stage "${currentStage.name}" has no agent_directive populated`;
546
- }
547
- else {
548
- spawnStageName = currentStage.name ?? null;
549
- // The current stage's directive declares its own lineage + continuity
550
- // (INJ-B); resolved uniformly later.
551
- spawnDirectiveDeclared = currentStage.agentDirective;
552
- const { content: directiveContent, manifest: directiveManifest } = await assembleDirectiveContentWithManifest(config, focusId, currentStage.agentDirective, worktreePath);
553
- if (directiveContent.trim()) {
554
- stageDirectiveContent = directiveContent;
555
- stageDirectiveModel = currentStage.agentDirective.model;
556
- // The stage is the Workflow layer for engine + local model too.
557
- directiveEngine = currentStage.agentDirective.engine ?? null;
558
- directiveLocalModel = currentStage.agentDirective.localModel ?? null;
559
- assemblyManifest.push(...directiveManifest);
560
- }
561
- else {
562
- fallbackReason = `assembly returned empty content for stage "${currentStage.name}"`;
563
- }
564
- }
565
- }
566
- catch (err) {
567
- fallbackReason = `directive lookup threw: ${err.message}`;
568
- }
569
- if (stageDirectiveContent) {
570
- directiveBody = stageDirectiveContent;
571
- directiveModel = stageDirectiveModel;
572
- console.log(`[focus-executor] Using stage directive for "${focusName}" (${stageDirectiveContent.length} chars)`);
573
- }
574
- else {
575
- console.warn(`[focus-executor] LEGACY PROMPT FALLBACK for "${focusName}": ${fallbackReason ?? 'unknown reason'}`);
576
- useLegacyFullPrompt = true;
577
- }
578
- }
579
- return { directiveBody, directiveModel, directiveEngine, directiveLocalModel, pendingFocusOverrideModel, spawnStageName, spawnDirectiveDeclared, useLegacyFullPrompt };
580
- }
581
- /**
582
- * Phase C input: assemble the FocusTeamPromptContext threaded into both the
583
- * directive and legacy prompt builders. Pure factory -- byte-identical to the
584
- * original inline object literal; extracted so the orchestrator reads as ordered
585
- * phase calls rather than inlining the context shape.
586
- */
587
- export function buildFocusPromptContext(args) {
588
- const { config, focusId, focusName, focusDescription, deliveries, issues, executionConfig, pipelineConfig, productContextDocs, deploymentProfileSnapshot, readOnly, loopContext, reviewFiledOpenCount, engineId, } = args;
589
- return {
590
- focusId,
591
- focusName,
592
- focusDescription,
593
- organizationId: config.organizationId,
594
- productId: config.productId,
595
- deliveries,
596
- issues,
597
- executionConfig,
598
- pipelineConfig,
599
- productContextDocs,
600
- deploymentProfileSnapshot,
601
- readOnly,
602
- loopContext,
603
- reviewFiledOpenCount,
604
- engineId,
605
- };
606
- }
607
- /**
608
- * Phase C: compose the final team-lead message -- role framework + remediation
609
- * framing + directive body (or the legacy full focus prompt), then the PRD
610
- * milestone push-down appended last so it reaches BOTH prompt paths. Pure +
611
- * testable. Byte-identical to the original inline composition.
612
- */
613
- export function buildSpawnPrompt(args) {
614
- const { role, promptContext, useLegacyFullPrompt, directiveBody, prdMilestoneContext, isReviewPass, agentSessionId } = args;
615
- let prompt;
616
- if (!useLegacyFullPrompt && directiveBody !== null) {
617
- const framing = buildRemediationFramingSection(directiveBody).join('\n');
618
- prompt = buildRoleFrameworkPrompt(role, promptContext)
619
- + '\n\n'
620
- + (framing ? framing + '\n' : '')
621
- + directiveBody;
622
- }
623
- else {
624
- prompt = buildFocusTeamPrompt(role, promptContext);
625
- }
626
- // Review passes carry two review-only obligations. The sensor-audit charge
627
- // (weak-sensor audit injection #8) makes the independent review frame
628
- // interrogate the tests, not just the code; the outcome-stamping obligation
629
- // is the terminal step, so it is appended LAST. Both reach the directive and
630
- // legacy prompt paths and are emitted for review spawns only.
631
- if (isReviewPass) {
632
- prompt += '\n\n' + buildReviewSensorAuditSection().join('\n');
633
- prompt += '\n\n' + buildReviewOutcomeStampingSection(agentSessionId).join('\n');
634
- }
635
- // Append PRD milestone context (push-down) so a PRD-armed focus team inherits
636
- // the milestone's goal/expansion/vision + frontier position. Appended to the
637
- // final prompt so it reaches BOTH the directive and legacy prompt paths; empty
638
- // for any focus not armed by a PRD (no change to ordinary focus spawns).
639
- if (prdMilestoneContext.trim()) {
640
- prompt += '\n\n' + prdMilestoneContext;
641
- }
642
- return prompt;
643
- }
644
- /**
645
- * Phase: resolve the per-pass local-model routing for a codex spawn.
646
- *
647
- * Wraps the fetch (getLocalModel / getLocalModelSecret) + the pure decision
648
- * (selectLocalModelRef -> decideLocalModelRouting) and, on 'route', provisions
649
- * the per-model CODEX_HOME (writeLocalModelCodexHome) -- returning the codexHome
650
- * the caller points the spawn at. The key VALUE comes from Telora (D8), never a
651
- * host env var. Behavior byte-identical to the original inline block; only the
652
- * apply-side (session-terminal write on skip, env injection on route) stays in
653
- * the caller.
654
- */
655
- export async function resolveLocalModelConfig(args, deps = {}) {
656
- const getModel = deps.getLocalModel ?? getLocalModel;
657
- const getSecret = deps.getLocalModelSecret ?? getLocalModelSecret;
658
- const writeCodexHome = deps.writeLocalModelCodexHome ?? writeLocalModelCodexHome;
659
- const localModelRef = selectLocalModelRef(args.engineId, args.enginePass, args.localModelOverride, args.stageDirectiveLocalModel);
660
- // getLocalModel + getLocalModelSecret (the DB calls) run only when a ref
661
- // exists; decideLocalModelRouting is the pure decision over their results.
662
- const coords = localModelRef ? await getModel(localModelRef) : null;
663
- const apiKey = localModelRef ? await getSecret(localModelRef) : null;
664
- const routing = decideLocalModelRouting(localModelRef, coords, apiKey);
665
- switch (routing.kind) {
666
- case 'none':
667
- return { kind: 'none' };
668
- case 'fallback':
669
- return { kind: 'fallback', ref: localModelRef };
670
- case 'route': {
671
- const { codexHome } = writeCodexHome(routing.coords.id, {
672
- baseUrl: routing.coords.baseUrl,
673
- model: routing.coords.model,
674
- wireApi: routing.coords.wireApi ?? undefined,
675
- requiresApiKey: routing.apiKey !== null,
676
- });
677
- return {
678
- kind: 'route',
679
- codexHome,
680
- codexModel: codexSafeModelAlias(routing.coords.model),
681
- apiKey: routing.apiKey,
682
- coords: routing.coords,
683
- };
684
- }
685
- }
686
- }
687
- /**
688
- * Surface (do NOT silently swap) two DIFFERENT local models bound to the coding
689
- * vs review pass of one focus that share a single GPU host (#9). On a single
690
- * card the two bindings force an evict+load swap on every pass alternation and
691
- * fight over the GPU (the live incident: 0/56 routing attempts succeeded). The
692
- * daemon must make the conflict VISIBLE rather than thrash silently.
693
- *
694
- * Cheap early-out: only when the focus pins a local model for BOTH passes and
695
- * the two refs differ do we resolve coordinates (two getLocalModel reads) and run
696
- * the pure detector. A conflict (same host root, different served models) is
697
- * raised ONCE per focus per daemon run (shouldSurfaceHostConflictOnce) as a log
698
- * line + a single escalation -- never re-raised every poll. Returns whether a new
699
- * surfacing was raised this call. Best-effort: an escalation-create failure is
700
- * logged, never fatal to the spawn.
701
- */
702
- export async function surfaceSingleHostModelConflict(args, deps = {}) {
703
- const getModel = deps.getLocalModel ?? getLocalModel;
704
- const makeEscalation = deps.createEscalation ?? createEscalation;
705
- const codingRef = args.localModelOverride?.coding?.trim() || null;
706
- const reviewRef = args.localModelOverride?.review?.trim() || null;
707
- // The conflict needs a distinct local model pinned on BOTH passes.
708
- if (!codingRef || !reviewRef || codingRef === reviewRef)
709
- return { surfaced: false };
710
- const [codingCoords, reviewCoords] = await Promise.all([getModel(codingRef), getModel(reviewRef)]);
711
- if (!codingCoords || !reviewCoords)
712
- return { surfaced: false };
713
- const bindings = [
714
- { pass: 'coding', modelId: codingCoords.model, baseUrl: codingCoords.baseUrl },
715
- { pass: 'review', modelId: reviewCoords.model, baseUrl: reviewCoords.baseUrl },
716
- ];
717
- const result = detectSingleHostModelConflict(bindings);
718
- if (!result.conflict)
719
- return { surfaced: false };
720
- if (!shouldSurfaceHostConflictOnce(args.focusId))
721
- return { surfaced: false };
722
- const detail = `Focus "${args.focusName}" pins two different local models to one GPU host ` +
723
- `(${result.root}): coding="${codingCoords.name}" (${codingCoords.model}) vs ` +
724
- `review="${reviewCoords.name}" (${reviewCoords.model}). On a single GPU these force an ` +
725
- `evict+load swap on every pass alternation and fight over the card. Pin both passes to the ` +
726
- `same local model, or move one pass to a different host / a cloud engine.`;
727
- console.warn(`[focus-executor] Host-control: ${detail}`);
728
- try {
729
- await makeEscalation({
730
- organizationId: args.organizationId,
731
- issueId: null,
732
- reasonType: ESCALATION_REASONS.BLOCKED_BY_EXTERNAL,
733
- description: detail,
734
- whatWasTried: 'Detected at spawn that the coding and review passes resolve to different local models on the same GPU host.',
735
- helpNeeded: 'Pin both passes to one local model, or route one pass to a separate host or a cloud engine, so the single GPU is not swapped on every pass.',
736
- escalationKind: 'single_host_model_conflict',
737
- ...(args.productId ? { productId: args.productId } : {}),
738
- focusId: args.focusId,
739
- metadata: {
740
- host_root: result.root,
741
- coding_model: codingCoords.model,
742
- review_model: reviewCoords.model,
743
- },
744
- });
745
- }
746
- catch (err) {
747
- console.warn(`[focus-executor] Failed to file single_host_model_conflict escalation for "${args.focusName}":`, err.message);
748
- }
749
- return { surfaced: true };
750
- }
751
- /**
752
- * The terminal session-update payload written when the resource governor denies a
753
- * slot for this spawn. Pure -- the caller adds `ended_at` and calls updateSession.
754
- * Exported so the governor-denied exit_category contract is unit-pinned (the
755
- * apply-side -- updateSession + governor.releaseSlot + activeTeams.delete -- stays
756
- * in spawnTeamProcess, exercised by spawn-path-unified.test.ts).
757
- */
758
- export function governorDeniedSessionUpdate() {
759
- return {
760
- status: 'failed',
761
- exit_reason: 'Governor denied slot',
762
- exit_category: 'governor_denied',
763
- };
764
- }
765
- /**
766
- * The terminal session-update payload written when a required OS sandbox cannot
767
- * initialize and the spawn is refused (fail-closed). Pure -- the caller adds
768
- * `ended_at` and calls updateSession. Exported so the sandbox-unavailable
769
- * exit_category contract is unit-pinned.
770
- */
771
- export function sandboxUnavailableSessionUpdate(message) {
772
- return {
773
- status: 'failed',
774
- exit_reason: message,
775
- exit_category: 'sandbox_unavailable',
776
- };
777
- }
778
102
  /**
779
103
  * Log the spawn header (focus / role / model / workers / branch, plus the
780
104
  * product line in multi-product mode). Side-effect-only; byte-identical to the
@@ -794,863 +118,6 @@ export function logSpawnHeader(args) {
794
118
  console.log(` Repo: ${config.repoPath}`);
795
119
  }
796
120
  }
797
- /**
798
- * Build the initial FocusTeamState for a spawn. Pure factory -- no side effects;
799
- * the caller inserts it into the activeTeams map.
800
- */
801
- export function createInitialTeamState(args) {
802
- const { focusId, focusName, role, config, executionConfig, pipelineConfig, branchName, readOnly } = args;
803
- return {
804
- focusId,
805
- focusName,
806
- roleId: role.id,
807
- roleName: role.name,
808
- organizationId: config.organizationId,
809
- productId: config.productId,
810
- executionConfig,
811
- pipelineConfig,
812
- startedAt: new Date(),
813
- phase: 'initializing',
814
- knownDeliveryIds: new Set(),
815
- mergedDeliveryIds: new Set(),
816
- planningPhase: false,
817
- shutdownReason: null,
818
- deliveryStageIds: new Map(),
819
- leadSessionId: null,
820
- leadPid: null,
821
- leadStdin: null,
822
- branchName,
823
- worktreePath: null,
824
- integrationBaseSha: null,
825
- resolvingMergeConflict: false,
826
- readOnly,
827
- completionDetector: null,
828
- claudeSessionId: null,
829
- lastProgressSnapshot: null,
830
- noProgressCycles: 0,
831
- commitNudgeAttempts: 0,
832
- lastConsumedDirectiveHash: null,
833
- sessionType: 'coding',
834
- lineage: 'coding',
835
- };
836
- }
837
- /**
838
- * Phase: resolve the focus worktree to spawn into. Reuses the persistent
839
- * worktree (rebasing onto integration + re-asserting read-only guards) when one
840
- * exists, else creates one inline. Returns null when the spawn cannot proceed
841
- * (repo has no commits, or inline creation failed) -- owning its own teardown
842
- * (activeTeams.delete + recordFocusTeardown) so the caller just returns.
843
- * Behavior byte-identical to the original inline block.
844
- */
845
- export async function resolveSpawnWorktree(args) {
846
- const { config, focusId, focusName, branchName, readOnly } = args;
847
- const activeTeams = getActiveTeams();
848
- // Reuse persistent focus worktree (created by ensureFocusWorktrees in poll loop)
849
- const existingWorktree = getFocusWorktree(focusId);
850
- if (existingWorktree) {
851
- const worktreePath = existingWorktree.worktreePath;
852
- // Rebase onto integration to pick up latest changes from other focuses
853
- const rebaseResult = runGitSync(['rebase', config.integrationBranch], worktreePath);
854
- if (!rebaseResult.success) {
855
- runGitSync(['rebase', '--abort'], worktreePath);
856
- console.warn(`[focus-executor] Rebase failed for "${focusName}", continuing with existing state`);
857
- }
858
- console.log(` Worktree (reused): ${worktreePath}`);
859
- // Re-assert read-only audit guards on the reused worktree. ensureFocusWorktrees
860
- // installs them at provisioning time, but a read-only focus may reuse a
861
- // persistent worktree across daemon restarts -- re-applying here (idempotent)
862
- // guarantees the deny overlay + pre-commit hook are present before spawn.
863
- if (readOnly) {
864
- installReadOnlyAuditGuards(worktreePath);
865
- console.log(` Re-asserted read-only audit guards (reused worktree)`);
866
- }
867
- return worktreePath;
868
- }
869
- // Guard: can't create worktrees in a repo with no commits
870
- if (!repoHasCommits(config.repoPath)) {
871
- console.warn(`[focus-executor] Repository has no commits -- cannot create worktree for "${focusName}"`);
872
- activeTeams.delete(focusId);
873
- recordFocusTeardown(focusId);
874
- return null;
875
- }
876
- // Fallback: worktree doesn't exist yet (race condition or first poll)
877
- console.warn(`[focus-executor] No persistent worktree for "${focusName}", creating inline`);
878
- try {
879
- const worktreePath = await createWorktree(config, branchName);
880
- setFocusWorktree(focusId, {
881
- focusId,
882
- focusName,
883
- worktreePath,
884
- branchName,
885
- createdAt: new Date(),
886
- });
887
- console.log(` Worktree (created inline): ${worktreePath}`);
888
- // Install read-only audit guards (pre-commit hook + write-deny overlay)
889
- if (readOnly) {
890
- installReadOnlyAuditGuards(worktreePath);
891
- console.log(` Installed read-only audit guards (read-only mode)`);
892
- }
893
- return worktreePath;
894
- }
895
- catch (err) {
896
- console.error(`[focus-executor] Failed to create worktree for focus "${focusName}":`, err instanceof Error ? err.message : String(err));
897
- activeTeams.delete(focusId);
898
- recordFocusTeardown(focusId);
899
- return null;
900
- }
901
- }
902
- /**
903
- * Phase: create the team-lead session row + register it with the telemetry
904
- * persistence gate, stamping its id onto the team state. Returns null when
905
- * createSession fails (owning its own activeTeams.delete teardown). The worktree
906
- * is focus-owned and persists even when session creation fails.
907
- *
908
- * `sessionType` is the AUTHORITATIVE lineage (resolved by resolveEngineForPass,
909
- * matching teamState.sessionType), so the (focus, lineage) pass this segment is
910
- * opened against is on the SAME lineage the segment carries -- never a stale
911
- * 'coding' default while the lineage actually resolved to 'review'.
912
- */
913
- export async function createTeamSession(args) {
914
- const { config, role, focusId, focusName, branchName, sessionType, teamState } = args;
915
- try {
916
- const session = await createSession({
917
- organizationId: config.organizationId,
918
- roleId: role.id,
919
- issueId: null,
920
- focusId,
921
- branchName,
922
- sessionType,
923
- });
924
- teamState.leadSessionId = session.id;
925
- // Confirm the session is durably persisted so its telemetry (tagged with
926
- // telora.session_id = session.id at spawn) passes the writer's emission
927
- // gate. Emission paths carrying an unpersisted id (e.g. the audit assessor's
928
- // raw Claude session id) have that id nulled instead of FK-violating.
929
- markSessionPersisted(session.id);
930
- return session;
931
- }
932
- catch (err) {
933
- console.error(`[focus-executor] Failed to create session for focus "${focusName}":`, err.message);
934
- // Worktree is focus-owned and persists even if session creation fails
935
- getActiveTeams().delete(focusId);
936
- return null;
937
- }
938
- }
939
- /**
940
- * Phase: best-effort review context. When the focus has a recorded
941
- * review_requested_at, count the open review-filed issues (the remediation
942
- * directive's working set) and carry the prior findings; also resolve the focus
943
- * description. A fetch failure leaves the count at zero (normal execution team).
944
- */
945
- export async function resolveReviewContext(args) {
946
- const { config, focusId, focusName, issues } = args;
947
- let reviewFiledOpenCount = 0;
948
- let priorFindings = [];
949
- let focusDescription = null;
950
- try {
951
- const activeFocuses = await fetchActiveFocusesForReviewState(config.organizationId, config.productId);
952
- const current = activeFocuses.find(f => f.focus_id === focusId);
953
- const reviewRequestedAt = current?.review_requested_at ?? null;
954
- if (reviewRequestedAt) {
955
- priorFindings = filterReviewFiledIssues(issues, reviewRequestedAt);
956
- reviewFiledOpenCount = priorFindings.length;
957
- }
958
- focusDescription = current?.focus_description ?? null;
959
- }
960
- catch (err) {
961
- console.debug(`[focus-executor] Could not compute review-filed open count for "${focusName}":`, err.message);
962
- }
963
- return { reviewFiledOpenCount, priorFindings, focusDescription };
964
- }
965
- /**
966
- * Phase B: resolve the session lineage (pass) and the engine backend for it,
967
- * stamping lineage + sessionType onto the team state. Returns the lineage spec,
968
- * the derived engine pass, the resolved engine, and the backend object.
969
- */
970
- export function resolveEngineForPass(args) {
971
- const { spawnDirectiveDeclared, spawnStageName, engineOverride, stageDirectiveEngine, config, orgDefaultEngine, focusName, teamState } = args;
972
- const spawnSpec = resolveLineageSpec({ declared: spawnDirectiveDeclared, stageName: spawnStageName });
973
- teamState.lineage = spawnSpec.lineage;
974
- teamState.sessionType = spawnSpec.lineage === 'review' ? 'review' : 'coding';
975
- const enginePass = spawnSpec.lineage === 'review' ? 'review' : 'coding';
976
- const resolvedEngine = resolveBackend({
977
- focusOverride: engineOverride,
978
- pass: enginePass,
979
- stageOverride: stageDirectiveEngine,
980
- productDefault: config.defaultCodingEngine,
981
- orgDefault: orgDefaultEngine,
982
- });
983
- const backend = getBackend(resolvedEngine.engineId);
984
- console.log(`[focus-executor] Engine for "${focusName}" pass=${enginePass}: ` +
985
- `${resolvedEngine.engineId} (source: ${resolvedEngine.source})`);
986
- return { spawnSpec, enginePass, resolvedEngine, backend };
987
- }
988
- /**
989
- * Phase: resolve the per-product CODEX_HOME default, apply per-pass local-model
990
- * routing (fallback / route), select the effective model, and build the CLI
991
- * args. Returns 'ready' with the args + the model/env bits the spawn needs.
992
- */
993
- export async function resolveSpawnModel(args) {
994
- const { resolvedEngine, enginePass, localModelOverride, stageDirectiveLocalModel, config, directiveModel, pendingFocusOverrideModel, pipelineConfig, backend, worktreePath, resumeId, role, readOnly, focusName, focusId, } = args;
995
- // Single source of truth for the focus override at this spawn: a respawn's
996
- // snapshot wins over the live pipeline_config.model; otherwise the live config
997
- // carries it. Both initial spawn and respawn funnel through resolveTeamModel
998
- // below, so the precedence (focus override -> stage -> CLI) is identical.
999
- const focusOverrideModel = pendingFocusOverrideModel ?? pipelineConfig?.model ?? null;
1000
- // Resolve the per-product isolated CODEX_HOME for this spawn (undefined for
1001
- // claude engines and cloud products with no provisioned codexHome -- those
1002
- // spawns are byte-for-byte unaffected).
1003
- let spawnCodexHome = codexHomeForSpawn(resolvedEngine.engineId, config);
1004
- let spawnCodexModel = config.codexModel;
1005
- let isLocalModelPass = false;
1006
- // Per-pass local model routing: if this codex pass selected a registered local
1007
- // model, resolve it to coordinates, provision a per-model CODEX_HOME, and route
1008
- // the spawn there -- overriding the per-product codexHome/codexModel for this
1009
- // pass only. Absent ref => the per-product default above is kept.
1010
- const localModelConfig = await resolveLocalModelConfig({
1011
- engineId: resolvedEngine.engineId,
1012
- enginePass,
1013
- localModelOverride,
1014
- stageDirectiveLocalModel,
1015
- });
1016
- // The Telora-resolved key VALUE injected into the spawn env (under the fixed
1017
- // TELORA_LOCAL_API_KEY name) for a routed local-model pass; null otherwise.
1018
- let spawnLocalModelKey = null;
1019
- if (localModelConfig.kind === 'fallback') {
1020
- console.warn(`[focus-executor] Focus "${focusName}" ${enginePass} pass selected local model ` +
1021
- `${(localModelConfig.ref ?? '').slice(0, 8)} but it is not in the registry; falling back to the ` +
1022
- `per-product default model.`);
1023
- }
1024
- else if (localModelConfig.kind === 'route') {
1025
- const { codexHome, codexModel, apiKey, coords } = localModelConfig;
1026
- spawnCodexHome = codexHome;
1027
- spawnCodexModel = codexModel;
1028
- spawnLocalModelKey = apiKey;
1029
- isLocalModelPass = true;
1030
- console.log(`[focus-executor] Routing "${focusName}" ${enginePass} pass to local model ` +
1031
- `"${coords.name}" (${coords.model} @ ${coords.baseUrl}); CODEX_HOME=${codexHome}, ` +
1032
- (apiKey
1033
- ? `key from Telora (env ${TELORA_LOCAL_API_KEY_ENV}).`
1034
- : 'unauthenticated endpoint.'));
1035
- // Host-control swap (D2): on a CONTROLLABLE host, load/unload the selected
1036
- // per-pass model around this pass -- a sequential single-GPU swap (one model
1037
- // resident at a time) so the coding-vs-review model runs end-to-end with NO
1038
- // manual studio swap. On a non-controllable host this is a no-op (today's
1039
- // behavior: the operator pre-loads the model). A swap FAILURE must NOT spawn
1040
- // against a not-yet-loaded model: skip this tick (the 30s poll re-attempts).
1041
- const capability = await detectHostCapability(coords.baseUrl, apiKey);
1042
- if (shouldSwapForPass({ routing: { kind: 'route', coords, apiKey }, capability })) {
1043
- const root = hostRootFromBaseUrl(coords.baseUrl);
1044
- // Fast inline path (#1): if the target is ALREADY resident + ready, spawn
1045
- // THIS tick -- no detour through the detached swap. A single bounded
1046
- // /v1/status read; org-prefix-tolerant via decideSwap/modelIdsMatch.
1047
- let alreadyResident = false;
1048
- try {
1049
- const status = await getStatus(coords.baseUrl, apiKey);
1050
- alreadyResident =
1051
- decideSwap({
1052
- current: { modelId: status.loadedModelId, ready: status.ready },
1053
- target: coords.model,
1054
- }).action === 'noop';
1055
- }
1056
- catch {
1057
- // Transient status-read failure -> fall through to the managed async
1058
- // swap, which retries the load + readiness off the poll-critical path.
1059
- }
1060
- if (alreadyResident) {
1061
- // Claim the per-host residency lock (#4) so a second focus on this host
1062
- // cannot start a swap that evicts our resident model mid-pass.
1063
- const claim = claimResidentHost({
1064
- root,
1065
- targetModel: coords.model,
1066
- focusId,
1067
- isFocusActive: (id) => hasActiveTeam(id),
1068
- });
1069
- if (claim.status === 'busy_other') {
1070
- console.log(`[focus-executor] Host-control: host for "${coords.name}" is held by another ` +
1071
- `focus's running pass; deferring "${focusName}" ${enginePass} pass to avoid eviction.`);
1072
- return { kind: 'still_loading', coords, reason: 'host_busy_other_focus' };
1073
- }
1074
- console.log(`[focus-executor] Host-control: model "${coords.name}" (${coords.model}) already ` +
1075
- `resident + ready for "${focusName}" ${enginePass} pass.`);
1076
- }
1077
- else {
1078
- // A load is needed. Run it OFF the serial poll loop via the swap manager
1079
- // (#3) so a cold load (up to the 180s budget) does not stall every other
1080
- // focus/product. The manager also enforces the per-host residency lock
1081
- // (#4): a second focus on this host defers rather than evicting an
1082
- // in-flight model. The load is detached -- never awaited here.
1083
- const loadTimeoutMs = loadEnvConfig().TELORA_HOST_CONTROL_LOAD_TIMEOUT_MS;
1084
- const observation = observeHostSwap({
1085
- root,
1086
- targetModel: coords.model,
1087
- focusId,
1088
- isFocusActive: (id) => hasActiveTeam(id),
1089
- runLoad: async () => {
1090
- // D3 sizing seam: hardware-aware safe context that RESERVES the
1091
- // candidate model's weights before sizing the KV cache. The weights
1092
- // size + native context come from the registry coordinates (the
1093
- // local_models sizing-metadata columns); when unset the sizer
1094
- // degrades safely (no reservation / ceiling clamp). FAIL-SAFE to the
1095
- // fixed DEFAULT_MAX_SEQ_LENGTH when host hardware data is unavailable.
1096
- const sizing = await resolveSafeLoadOptions(coords.baseUrl, coords.model, { modelSizeMb: coords.modelSizeMb, nativeContext: coords.nativeContext }, apiKey);
1097
- if (sizing.source === 'fallback') {
1098
- console.warn(`[focus-executor] Host-control sizing unavailable for "${coords.name}" ` +
1099
- `(${coords.model}); falling back to the fixed default context ` +
1100
- `${sizing.maxSeqLength}. Check the host's /api/system/hardware endpoint.`);
1101
- }
1102
- else {
1103
- console.log(`[focus-executor] Host-control sizing for "${coords.name}" (${coords.model}): ` +
1104
- `max_seq_length=${sizing.maxSeqLength}` +
1105
- (sizing.cacheTypeKv ? `, cache_type_kv=${sizing.cacheTypeKv}` : '') +
1106
- (sizing.quantRequired ? ' (quant-required, marginal headroom)' : '') +
1107
- (sizing.marginal && !sizing.quantRequired ? ' (marginal headroom)' : '') + '.');
1108
- }
1109
- // Load-scoped POST timeout: a host that blocks /v1/load until the cold
1110
- // load completes must not be cut by the generic 5s fetch timeout.
1111
- await ensureLoaded(coords.baseUrl, coords.model, {
1112
- maxSeqLength: sizing.maxSeqLength,
1113
- loadTimeoutMs,
1114
- ...(sizing.cacheTypeKv !== undefined ? { cacheTypeKv: sizing.cacheTypeKv } : {}),
1115
- }, apiKey);
1116
- console.log(`[focus-executor] Host-control: local model "${coords.name}" (${coords.model}) ` +
1117
- `loaded + ready (off-loop swap complete).`);
1118
- },
1119
- });
1120
- if (observation.status === 'loading') {
1121
- console.log(`[focus-executor] Host-control: model "${coords.name}" (${coords.model}) is loading ` +
1122
- `for "${focusName}" ${enginePass} pass; deferring spawn (load runs off the poll loop).`);
1123
- return { kind: 'still_loading', coords, reason: 'model_loading' };
1124
- }
1125
- if (observation.status === 'busy_other') {
1126
- console.log(`[focus-executor] Host-control: host for "${coords.name}" is held by another ` +
1127
- `focus's running pass; deferring "${focusName}" ${enginePass} pass to avoid eviction.`);
1128
- return { kind: 'still_loading', coords, reason: 'host_busy_other_focus' };
1129
- }
1130
- if (observation.status === 'failed') {
1131
- console.error(`[focus-executor] Host-control swap to load "${coords.name}" (${coords.model}) for ` +
1132
- `"${focusName}" ${enginePass} pass FAILED: ${observation.error}. Not spawning against ` +
1133
- `an unloaded model; will re-attempt next poll.`);
1134
- return { kind: 'skip_load', coords, dbMessage: observation.error };
1135
- }
1136
- // observation.status === 'ready' -- the detached load completed on a
1137
- // prior tick; the model is resident and residency is held. Proceed.
1138
- console.log(`[focus-executor] Host-control: model "${coords.name}" (${coords.model}) ready for ` +
1139
- `"${focusName}" ${enginePass} pass (off-loop swap complete).`);
1140
- }
1141
- }
1142
- }
1143
- // Model selection: ONE precedence (focus override -> stage directive -> CLI),
1144
- // applied identically at initial spawn and at stage respawn. The engine-compat
1145
- // prefilters below null out an input the resolved backend cannot run; the
1146
- // surviving inputs funnel through resolveEffectiveModel -> resolveTeamModel.
1147
- const focusSelectedModel = selectPipelineModelForSpawn(resolvedEngine.engineId, spawnCodexHome, focusOverrideModel);
1148
- const effectiveDirectiveModel = selectDirectiveModelForSpawn(resolvedEngine.engineId, directiveModel, spawnCodexModel ?? focusSelectedModel);
1149
- const effectiveModel = resolveEffectiveModel(effectiveDirectiveModel, spawnCodexHome, spawnCodexModel, focusSelectedModel);
1150
- const effectivePipelineConfig = effectiveModel !== (pipelineConfig?.model ?? null)
1151
- ? { ...pipelineConfig, model: effectiveModel }
1152
- : pipelineConfig;
1153
- const args2 = buildAgentArgs(backend, config, worktreePath, effectivePipelineConfig, resumeId, role.allowed_tools, readOnly);
1154
- // Capability-driven (no operator flag): for a registered local-model route
1155
- // whose endpoint drops Codex's namespace tools (backendDropsNamespaceTools),
1156
- // insert a localhost translating proxy between Codex and the inference server
1157
- // so the OSS server still receives the Telora tool schemas. Started LAST --
1158
- // after every host-control early return -- so a deferred spawn never leaks a
1159
- // proxy. Points the provisioned CODEX_HOME's base_url at the proxy; the proxy
1160
- // forwards to the model's real coords.baseUrl. Non-local / native-Responses
1161
- // passes never route => mcpProxy stays null and the spawn is byte-identical.
1162
- let mcpProxy = null;
1163
- if (localModelConfig.kind === 'route' &&
1164
- shouldRouteLocalModelThroughProxy(isLocalModelPass, localModelConfig.coords)) {
1165
- const { coords, apiKey } = localModelConfig;
1166
- try {
1167
- mcpProxy = await startCodexMcpProxy({ upstreamBaseUrl: coords.baseUrl });
1168
- writeLocalModelCodexHome(coords.id, {
1169
- baseUrl: mcpProxy.url,
1170
- model: coords.model,
1171
- wireApi: coords.wireApi ?? undefined,
1172
- requiresApiKey: apiKey !== null,
1173
- });
1174
- console.log(`[focus-executor] Local-model MCP proxy engaged for "${coords.name}": ` +
1175
- `Codex base_url -> ${mcpProxy.url} -> upstream ${coords.baseUrl}.`);
1176
- }
1177
- catch (err) {
1178
- // Best-effort: a proxy that cannot start must not block the spawn; fall
1179
- // back to the direct (namespace-dropping) path. The fail-loud reception
1180
- // check only applies when a proxy is present.
1181
- console.warn(`[focus-executor] Local-model MCP proxy failed to start; spawning direct: ${err.message}`);
1182
- mcpProxy = null;
1183
- }
1184
- }
1185
- return { kind: 'ready', args: args2, effectiveModel, spawnCodexHome, spawnLocalModelKey, isLocalModelPass, mcpProxy };
1186
- }
1187
- /**
1188
- * Phase: choose the stdin message. A fresh spawn sends the full composed prompt;
1189
- * a resume sends an incremental delta (git diff + prior findings to verify) since
1190
- * the resumed session already holds its role + accumulated context (INJ-D).
1191
- *
1192
- * The scoping case is the one exception that overrides the fresh-vs-resume split:
1193
- * when a NON-read_only (executable) focus has queued deliveries carrying only
1194
- * Context Group issues and zero Task/Bug issues, this returns the role framework
1195
- * PLUS `buildQueuedDeliveryScopingMessage` REGARDLESS of resumeId. The caller pairs
1196
- * this with `resolveScopingSpawnResume`, which forces the spawn fresh (resumeId
1197
- * null) so the directive lands on a new session instead of no-opping a concluded
1198
- * one. Because the fresh session holds no accumulated context, the role framework
1199
- * (identity + Scoping Mandate + status-update rules) is prepended here; only the
1200
- * audit/kickoff DIRECTIVE body carried in `prompt` is dropped, per AC. `readOnlyFocus`
1201
- * focuses are discovery-only (audit-playbook origin): their CG-only deliveries are
1202
- * INTENTIONALLY unscoped, so they are never driven into scoping here --
1203
- * delivery-lifecycle terminalizes them instead.
1204
- */
1205
- export async function buildSpawnMessage(args) {
1206
- const { prompt, resumeId, readOnlyFocus, role, promptContext, spawnSpec, config, focusId, focusName, deliveries, issues, worktreePath, priorFindings } = args;
1207
- // Scoping directive takes priority over the fresh/resume split, but ONLY for
1208
- // executable (non-discovery) focuses. It is returned even when resumeId is
1209
- // null because the caller forces this spawn fresh for exactly this case.
1210
- if (!readOnlyFocus) {
1211
- const unscopedDeliveries = findQueuedDeliveriesWithoutTaskIssues(deliveries, issues);
1212
- if (unscopedDeliveries.length > 0) {
1213
- // Prepend the role framework (identity, Scoping Mandate, MANDATORY
1214
- // status-update rules, worker-management, escalation) so the FORCED-FRESH
1215
- // scoping session receives its operating contract -- the audit/kickoff
1216
- // DIRECTIVE body in `prompt` is the only part intentionally dropped, not the
1217
- // role framework. On a fresh session stdin is the sole first-turn message,
1218
- // so omitting the framework would leave the team lead with no identity.
1219
- const roleFramework = buildRoleFrameworkPrompt(role, promptContext);
1220
- const scopingDirective = buildQueuedDeliveryScopingMessage({
1221
- lineage: spawnSpec.lineage,
1222
- focusName,
1223
- deliveries: unscopedDeliveries,
1224
- });
1225
- const messageToSend = `${roleFramework}\n\n${scopingDirective}`;
1226
- console.log(`[focus-executor] Spawning fresh scoping session for "${focusName}" ` +
1227
- `-- role framework + explicit scoping directive for ${unscopedDeliveries.length} queued ` +
1228
- `zero-issue deliver${unscopedDeliveries.length === 1 ? 'y' : 'ies'} ` +
1229
- `(lineage ${spawnSpec.lineage}, ${messageToSend.length} chars)`);
1230
- return messageToSend;
1231
- }
1232
- }
1233
- if (!resumeId)
1234
- return prompt;
1235
- let gitDiff = '';
1236
- try {
1237
- const diffCtx = {
1238
- focusId,
1239
- deliveryIds: deliveries.map(d => d.id),
1240
- worktreePath,
1241
- config,
1242
- organizationId: config.organizationId,
1243
- productId: config.productId,
1244
- };
1245
- const { content } = await resolveAssemblyRecipeWithManifest(['git.diff_against_base'], diffCtx);
1246
- gitDiff = content;
1247
- }
1248
- catch (err) {
1249
- console.debug(`[focus-executor] Resume-delta diff assembly failed (non-fatal):`, err.message);
1250
- }
1251
- const messageToSend = buildResumeDeltaMessage({
1252
- lineage: spawnSpec.lineage,
1253
- focusName,
1254
- gitDiff,
1255
- priorFindings,
1256
- });
1257
- console.log(`[focus-executor] Resuming Claude session ${resumeId} for "${focusName}" ` +
1258
- `-- sending incremental delta (lineage ${spawnSpec.lineage}, ${messageToSend.length} chars, ` +
1259
- `${priorFindings.length} prior finding(s))`);
1260
- return messageToSend;
1261
- }
1262
- /**
1263
- * Decide whether this spawn must be forced fresh to deliver a scoping directive,
1264
- * and return the resume id the spawn should actually use.
1265
- *
1266
- * The wedge this closes: a queued delivery carrying only Context Group issues and
1267
- * zero Task/Bug issues can never cross the queued->coding gate. The daemon
1268
- * re-spawns the focus every poll, but the resume lands on a session that already
1269
- * considers the audit "done" and no-ops in one turn. The fix is to force the
1270
- * scoping spawn FRESH (resumeId null) so `buildQueuedDeliveryScopingMessage`
1271
- * primes a new session as its first instruction rather than a resume-delta a
1272
- * concluded session ignores.
1273
- *
1274
- * Only executable focuses are driven into scoping. A `readOnly` (discovery-only,
1275
- * audit-playbook) focus's CG-only deliveries are intentionally unscoped, so this
1276
- * never forces them fresh -- their resume id passes through unchanged and the
1277
- * delivery reaches a terminal state via the reconciler's discovery-only path.
1278
- *
1279
- * Pure and exported so the fresh-spawn contract is unit-testable without a live
1280
- * spawn.
1281
- */
1282
- export function resolveScopingSpawnResume(args) {
1283
- const { readOnly, deliveries, issues, resumeId } = args;
1284
- if (readOnly)
1285
- return { resumeId, isScopingSpawn: false };
1286
- const isScopingSpawn = findQueuedDeliveriesWithoutTaskIssues(deliveries, issues).length > 0;
1287
- return {
1288
- resumeId: isScopingSpawn ? null : resumeId,
1289
- isScopingSpawn,
1290
- };
1291
- }
1292
- export function findQueuedDeliveriesWithoutTaskIssues(deliveries, issues) {
1293
- const taskIssueCounts = new Map();
1294
- for (const issue of issues) {
1295
- if (issue.issueType !== 'Task' && issue.issueType !== 'Bug')
1296
- continue;
1297
- taskIssueCounts.set(issue.deliveryId, (taskIssueCounts.get(issue.deliveryId) ?? 0) + 1);
1298
- }
1299
- return deliveries.filter(delivery => (delivery.executionStatus ?? '') === 'queued' &&
1300
- (taskIssueCounts.get(delivery.id) ?? 0) === 0);
1301
- }
1302
- export function buildQueuedDeliveryScopingMessage(args) {
1303
- const deliverySections = args.deliveries
1304
- .slice()
1305
- .sort((a, b) => a.priorityRank - b.priorityRank)
1306
- .map((delivery, index) => [
1307
- `### ${index + 1}. ${delivery.name}`,
1308
- `- Delivery ID: ${delivery.id}`,
1309
- `- Priority rank: ${delivery.priorityRank}`,
1310
- '',
1311
- '**Description**',
1312
- delivery.description?.trim() || '(none)',
1313
- '',
1314
- '**Acceptance Criteria**',
1315
- delivery.acceptanceCriteria?.trim() || '(none)',
1316
- '',
1317
- '**Technical Context**',
1318
- delivery.techContext?.trim() || '(none)',
1319
- ].join('\n'))
1320
- .join('\n\n');
1321
- return [
1322
- `# Scoping Required for Newly Queued Deliveries`,
1323
- '',
1324
- `You are the team lead for lineage "${args.lineage}" on focus "${args.focusName}". This is a FRESH scoping turn (not an incremental coding-resume): the role framework above is your operating contract for it.`,
1325
- '',
1326
- `The focus has queued deliveries with zero Task/Bug issues. Decompose each delivery below into Task/Bug issues before doing code work. For each delivery, create agent-satisfiable issues from its acceptance criteria, then move at least one Task/Bug issue to In Progress. The daemon advances the delivery queued -> coding (single-writer reconciler) the moment a work issue is In Progress.`,
1327
- '',
1328
- `Do not acknowledge and stop after reading this message. The required output of this turn is scoped issues on every listed delivery, with issue statuses kept current via Telora.`,
1329
- '',
1330
- deliverySections,
1331
- ].join('\n');
1332
- }
1333
- /**
1334
- * Local-model passes depend on a host CLI, so refuse the process launch when
1335
- * the backend's existing availability probe says it cannot run. Cloud passes
1336
- * retain their existing behavior because their authentication can be supplied
1337
- * by the per-spawn environment after the host-level probe runs.
1338
- */
1339
- export async function gateLocalModelBackendSpawn(backend, config, isLocalModelPass, host = hostname()) {
1340
- if (!isLocalModelPass)
1341
- return { allowed: true };
1342
- const availability = await backend.probeAvailability(config);
1343
- if (availability.available)
1344
- return { allowed: true };
1345
- // A routed local model supplies its own provider key in the child env. The
1346
- // host probe cannot see that key yet, so only binary absence is terminal for
1347
- // this Codex path; host-auth absence is intentionally superseded.
1348
- if (backend.id === 'codex' && availability.detail?.includes('but not authenticated')) {
1349
- return { allowed: true };
1350
- }
1351
- const binaryMissing = backend.id === 'codex' && availability.detail?.includes('not found');
1352
- const prefix = binaryMissing
1353
- ? `Codex CLI not found on ${host} / not on PATH`
1354
- : `${backend.displayName} unavailable on ${host}`;
1355
- return {
1356
- allowed: false,
1357
- reason: availability.detail ? `${prefix}. ${availability.detail}` : prefix,
1358
- };
1359
- }
1360
- export function backendUnavailableSessionUpdate(reason, category = 'backend_unavailable') {
1361
- return {
1362
- status: 'failed',
1363
- exit_reason: reason,
1364
- exit_category: category,
1365
- };
1366
- }
1367
- /** Attach the error listener in the same synchronous stack frame as spawn(). */
1368
- export function spawnCrashIsolated(command, args, options, onError, spawnFn = spawn) {
1369
- const proc = spawnFn(command, args, {
1370
- ...options,
1371
- // Own process group (setsid on POSIX): the team lead becomes its own
1372
- // group leader (pgid == lead pid). This is what makes the reap's
1373
- // group-first signal (spawner/lifecycle.signalWorker: kill(-pid)) actually
1374
- // reap the lead AND any host children it sub-spawns as one group -- without
1375
- // detachment the lead shares the daemon's group and kill(-leadPid) ESRCHs,
1376
- // falling back to the bare pid (lead only). We deliberately do NOT unref():
1377
- // the daemon must still observe the lead's 'close'/'exit' to drive
1378
- // completion + delivery advancement. On a hard daemon kill the CLI reaps
1379
- // these groups from the persisted worker pidfile (see daemon-process reap).
1380
- detached: true,
1381
- stdio: ['pipe', 'pipe', 'pipe'],
1382
- });
1383
- proc.on('error', onError);
1384
- // A failed exec can also tear down stdin with EPIPE after the process-level
1385
- // error/close. Consume that duplicate stream error; forward other stdin
1386
- // failures through the pass-scoped handler.
1387
- proc.stdin.on('error', (error) => {
1388
- if (error.code !== 'EPIPE')
1389
- onError(error);
1390
- });
1391
- return proc;
1392
- }
1393
- /** Ensure a terminal spawn failure is persisted after any in-flight running transition. */
1394
- export async function persistSpawnFailureAfterRunning(runningTransition, persistFailure) {
1395
- if (runningTransition)
1396
- await runningTransition.catch(() => undefined);
1397
- await persistFailure();
1398
- }
1399
- /**
1400
- * Final phase: build the levered spawn environment, persist the assembly
1401
- * manifest, write the session lifecycle (starting -> running), acquire the
1402
- * governor slot, wrap the command in the sandbox (fail-closed), spawn the
1403
- * child, write its stdin, and attach the completion/close/error handlers.
1404
- *
1405
- * Owns ALL of its early-return cleanups so the team-state map and governor slot
1406
- * never leak: governor-denied (exit_category 'governor_denied') and
1407
- * sandbox-unavailable (exit_category 'sandbox_unavailable') both mark the
1408
- * session terminal, release the governor slot where one was held, remove the
1409
- * team from activeTeams, and return without spawning. Behavior byte-identical
1410
- * to the original inline tail of spawnFocusTeam.
1411
- */
1412
- /**
1413
- * Close a team's local-model translating proxy if one is engaged, and null it so
1414
- * the call is idempotent across the multiple teardown paths. No-op for every
1415
- * hosted/Claude/flag-off team (mcpProxy undefined/null).
1416
- */
1417
- export function closeTeamProxyQuietly(teamState) {
1418
- const proxy = teamState.mcpProxy;
1419
- if (!proxy)
1420
- return;
1421
- teamState.mcpProxy = null;
1422
- proxy.close().catch(() => undefined);
1423
- }
1424
- export async function spawnTeamProcess(spec) {
1425
- const { config, focusId, focusName, session, teamState, backend, args, messageToSend, logs, worktreePath, resumeId, assemblyManifest, spawnCodexHome, spawnLocalModelKey, isLocalModelPass, effectiveModel, pendingDirective, spawnStageName, params, } = spec;
1426
- const activeTeams = getActiveTeams();
1427
- // Two fail-closed gates for local-model passes, both refusing the spawn into a
1428
- // durable terminal session rather than launching a team that cannot work:
1429
- // 1. backend availability (host CLI present) -- 'backend_unavailable'
1430
- // 2. read-only MCP preflight (required Telora tools callable) -- 'mcp_preflight_failed'
1431
- // The preflight runs only after availability passes (no point probing MCP when
1432
- // the binary is missing). Cloud/claude passes short-circuit both gates.
1433
- const availabilityGate = await gateLocalModelBackendSpawn(backend, config, isLocalModelPass);
1434
- const spawnGate = !availabilityGate.allowed
1435
- ? { ...availabilityGate, category: 'backend_unavailable' }
1436
- : await preflightLocalModelMcp(config, isLocalModelPass);
1437
- if (!spawnGate.allowed) {
1438
- const reason = spawnGate.reason ?? `${backend.displayName} unavailable`;
1439
- const category = spawnGate.category ?? 'backend_unavailable';
1440
- console.error(`[focus-executor] Refusing spawn for "${focusName}" (${category}): ${reason}`);
1441
- await updateSession(session.id, {
1442
- ...backendUnavailableSessionUpdate(reason, category),
1443
- ended_at: new Date().toISOString(),
1444
- }).catch(updateErr => {
1445
- console.warn(`[focus-executor] Failed to update refused-spawn session:`, updateErr.message);
1446
- });
1447
- logs.stdoutLogStream.end();
1448
- logs.stderrStream.end();
1449
- logs.jsonlStream.end();
1450
- closeTeamProxyQuietly(teamState);
1451
- teamState.phase = 'terminated';
1452
- activeTeams.delete(focusId);
1453
- return;
1454
- }
1455
- // Tag spend with the lever: the team-lead spawn runs the focus's execute
1456
- // phase (review directives run the gate); the workflow stage is carried
1457
- // separately. The audit phase tags its own spawn elsewhere.
1458
- const spawnPhase = (pendingDirective?.sessionType ?? 'coding') === 'review' ? 'gate' : 'execute';
1459
- // Stage: prefer the resolved workflow-stage name; when the stage could not be
1460
- // resolved (legacy-prompt fallback, or a non-review directive that carried no
1461
- // stage), fall back to the phase-appropriate default so the lever is never
1462
- // dropped. 'reviewing' for the gate phase, 'coding' for execute.
1463
- const spawnStage = spawnStageName ?? (spawnPhase === 'gate' ? 'reviewing' : 'coding');
1464
- // Levered spawn: buildLeveredSpawnEnvironment requires phase + stage, so this
1465
- // path cannot regress to dropping the telemetry lever.
1466
- const spawnEnv = buildLeveredSpawnEnvironment(config, {
1467
- orgId: config.organizationId,
1468
- focusId,
1469
- sessionId: session.id,
1470
- phase: spawnPhase,
1471
- stage: spawnStage,
1472
- });
1473
- // Inject backend-specific org secrets (e.g. CODEX_API_KEY for the codex backend).
1474
- // Non-fatal: fetchBackendEnvExtras returns {} on any error, falling back to host creds.
1475
- Object.assign(spawnEnv, await fetchBackendEnvExtras(backend.id));
1476
- // Per-product local-model isolation: point this codex spawn at the product's
1477
- // provisioned CODEX_HOME (no-invisible-cascading -- the override is logged).
1478
- // Cloud/claude spawns leave CODEX_HOME unset (spawnCodexHome === undefined).
1479
- if (spawnCodexHome) {
1480
- spawnEnv.CODEX_HOME = spawnCodexHome;
1481
- console.log(`[focus-executor] CODEX_HOME set for "${focusName}" (product ${config.productId.slice(0, 8)}): ` +
1482
- `${spawnCodexHome}` +
1483
- (effectiveModel ? ` (model: ${effectiveModel})` : ''));
1484
- }
1485
- // Per-pass local-model bearer (D8): inject the Telora-resolved key VALUE into
1486
- // the spawn env under the fixed name codex's config.toml references (env_key).
1487
- // The value lives only in this child process env -- never on disk, never on the
1488
- // local_models row. Set only for a routed local-model pass; cloud/claude and
1489
- // non-local codex spawns leave it untouched.
1490
- if (spawnLocalModelKey) {
1491
- spawnEnv[TELORA_LOCAL_API_KEY_ENV] = spawnLocalModelKey;
1492
- }
1493
- // Persist the assembly manifest for this spawn (best-effort -- never blocks
1494
- // the spawn). One row per resolved source, attributed to org + session +
1495
- // focus, so injected context becomes a queryable lever on token spend.
1496
- void insertAssemblyManifest({
1497
- sessionId: session.id,
1498
- focusId,
1499
- deliveryId: null,
1500
- manifest: assemblyManifest,
1501
- });
1502
- // Update session to starting
1503
- await updateSession(session.id, {
1504
- status: 'starting',
1505
- started_at: new Date().toISOString(),
1506
- stdout_path: logs.stdoutPath,
1507
- stderr_path: logs.stderrPath,
1508
- });
1509
- // Acquire governor slot (if governor is configured)
1510
- if (governor) {
1511
- try {
1512
- await governor.acquireSlot('focus');
1513
- }
1514
- catch (err) {
1515
- console.warn(`[focus-executor] Governor denied slot for "${focusName}":`, err.message);
1516
- // Mark session as failed so it doesn't linger in `starting` status
1517
- await updateSession(session.id, {
1518
- ...governorDeniedSessionUpdate(),
1519
- ended_at: new Date().toISOString(),
1520
- }).catch(updateErr => {
1521
- console.warn(`[focus-executor] Failed to update session after governor denial:`, updateErr.message);
1522
- });
1523
- closeTeamProxyQuietly(teamState);
1524
- activeTeams.delete(focusId);
1525
- return;
1526
- }
1527
- }
1528
- // Wrap the spawn in the OS sandbox (D3). Default mode is 'off' (no change);
1529
- // an explicit sandbox.mode="bwrap" confines the agent's filesystem to its
1530
- // worktree + an explicit read-only allowlist. FAIL-CLOSED: if a required
1531
- // sandbox cannot initialize, refuse the spawn rather than run unconfined.
1532
- let spawnCommand;
1533
- let spawnArgs;
1534
- try {
1535
- const wrapped = wrapSpawnCommand(config, {
1536
- command: backend.resolveCommand(config),
1537
- args,
1538
- worktreePath,
1539
- });
1540
- spawnCommand = wrapped.command;
1541
- spawnArgs = wrapped.args;
1542
- }
1543
- catch (err) {
1544
- if (err instanceof SandboxUnavailableError) {
1545
- console.error(`[focus-executor] Refusing spawn for "${focusName}": ${err.message}`);
1546
- await updateSession(session.id, {
1547
- ...sandboxUnavailableSessionUpdate(err.message),
1548
- ended_at: new Date().toISOString(),
1549
- }).catch(updateErr => {
1550
- console.warn(`[focus-executor] Failed to update session after sandbox refusal:`, updateErr.message);
1551
- });
1552
- if (governor)
1553
- governor.releaseSlot('focus');
1554
- closeTeamProxyQuietly(teamState);
1555
- activeTeams.delete(focusId);
1556
- return;
1557
- }
1558
- throw err;
1559
- }
1560
- // Spawn the team lead process
1561
- let processErrorHandled = false;
1562
- let runningTransition = null;
1563
- let completionDetectorForError = null;
1564
- const handleProcessError = (err) => {
1565
- if (processErrorHandled)
1566
- return;
1567
- processErrorHandled = true;
1568
- console.error(`[focus-executor] Team lead for "${focusName}" error:`, err.message);
1569
- governor?.releaseSlot('focus');
1570
- const missingCliReason = err.code === 'ENOENT'
1571
- ? `${backend.displayName} not found on ${hostname()} / not on PATH: ${err.message}`
1572
- : `Process error: ${err.message}`;
1573
- void persistSpawnFailureAfterRunning(runningTransition, () => withRetry(() => updateSession(session.id, {
1574
- status: 'failed',
1575
- exit_reason: missingCliReason,
1576
- exit_category: err.code === 'ENOENT' ? 'backend_unavailable' : 'error',
1577
- ended_at: new Date().toISOString(),
1578
- }), { maxAttempts: 3, baseDelayMs: 1000, label: 'session-update-error' })).catch(updateErr => {
1579
- console.warn(`[focus-executor] Failed to update session after retries:`, updateErr.message);
1580
- });
1581
- completionDetectorForError?.destroy();
1582
- logs.stdoutLogStream.end();
1583
- logs.stderrStream.end();
1584
- logs.jsonlStream.end();
1585
- closeTeamProxyQuietly(teamState);
1586
- teamState.phase = 'terminated';
1587
- activeTeams.delete(focusId);
1588
- };
1589
- const proc = spawnCrashIsolated(spawnCommand, spawnArgs, {
1590
- cwd: worktreePath,
1591
- env: spawnEnv,
1592
- }, handleProcessError);
1593
- teamState.leadPid = proc.pid ?? null;
1594
- teamState.leadStdin = proc.stdin;
1595
- teamState.phase = 'executing';
1596
- // Persist the detached lead pid so a hard daemon kill (stop --force / timeout
1597
- // / external kill -9) can group-reap it from the pidfile -- the in-memory
1598
- // activeTeams map dies with the daemon. Auto-removed on the lead's exit.
1599
- if (typeof proc.pid === 'number') {
1600
- const leadPid = proc.pid;
1601
- recordWorkerPid(leadPid, focusName);
1602
- proc.once('close', () => removeWorkerPid(leadPid));
1603
- proc.once('error', () => removeWorkerPid(leadPid));
1604
- }
1605
- // Attach stream parser, completion detector, activity tracker, event handlers
1606
- const { completionDetector, activityTracker } = attachStreamHandlers({
1607
- proc, backend, teamState, focusId, focusName, sessionId: session.id, logs,
1608
- });
1609
- completionDetectorForError = completionDetector;
1610
- // Send the message via stdin: the full composed prompt on a fresh spawn, or
1611
- // the incremental delta when resuming a lineage (INJ-D). Engine-specific input
1612
- // encoding lives in the backend.
1613
- proc.stdin.write(backend.encodeInput(messageToSend));
1614
- if (backend.closeStdinAfterInput) {
1615
- proc.stdin.end();
1616
- teamState.leadStdin = null;
1617
- }
1618
- // Update session to running
1619
- runningTransition = updateSession(session.id, {
1620
- status: 'running',
1621
- pid: proc.pid,
1622
- });
1623
- await runningTransition;
1624
- console.log(`[focus-executor] Team lead spawned for "${focusName}" (PID: ${proc.pid}, session: ${session.id})`);
1625
- // Handle completion -- check for more work or terminate.
1626
- // No idle phase: if actionable work remains, send it immediately (mid-focus
1627
- // handoff). If all work is done, terminate. A fresh team is spawned if new
1628
- // deliveries arrive later (~$0.50-1.00 exploration cost, but clean context).
1629
- completionDetector.on('complete', (info) => {
1630
- console.log(`[focus-executor] Team lead for "${focusName}" completed work cycle: ` +
1631
- `${info.turnCount} turns, $${info.totalCostUsd.toFixed(4)}`);
1632
- handleCompletionEvent({ config, focusId, focusName, teamState, completionDetector, proc, completionInfo: info }).catch(err => {
1633
- console.warn(`[focus-executor] Failed to check delivery state for "${focusName}":`, err.message);
1634
- // Fallback: terminate to avoid idle money burn.
1635
- terminateTeam(focusId, 'handoff');
1636
- });
1637
- });
1638
- // Handle process exit
1639
- const spawnedAt = Date.now();
1640
- proc.on('close', (code, signal) => {
1641
- // Node emits close after a spawn error. The error path already persisted
1642
- // the terminal reason; do not let normal completion overwrite it.
1643
- if (processErrorHandled)
1644
- return;
1645
- handleTeamProcessClose({
1646
- code, signal, focusName, focusId, resumeId, spawnedAt,
1647
- teamState, sessionId: session.id, config, completionDetector,
1648
- activityTracker, logs, params, governor, spawnFocusTeam,
1649
- }).catch(err => {
1650
- console.error(`[focus-executor] handleTeamProcessClose failed for "${focusName}":`, err.message);
1651
- });
1652
- });
1653
- }
1654
121
  /**
1655
122
  * Spawn a focus team to execute all deliveries in a focus.
1656
123
  *
@@ -1685,7 +152,7 @@ export async function spawnFocusTeam(params) {
1685
152
  activeTeams.delete(focusId);
1686
153
  return;
1687
154
  }
1688
- const { deliveries, issues, productContextDocs, deploymentProfileSnapshot, loopContext, prdMilestoneContext, assemblyManifest, } = spawnContext;
155
+ const { deliveries, issues, deploymentProfileSnapshot, answeredEscalations, prdMilestoneContext, prdMilestoneManifest, assemblyManifest, } = spawnContext;
1689
156
  // --- Phase 2: consume any pending directive, run the rank-ordered pre-spawn
1690
157
  // guard, and record known-delivery bookkeeping on the team state. 'skip' =>
1691
158
  // no actionable work; tear down and return (the map stays orchestrator-owned).
@@ -1726,6 +193,7 @@ export async function spawnFocusTeam(params) {
1726
193
  // engine that will actually run this pass.
1727
194
  const { directiveBody, directiveModel, directiveEngine, directiveLocalModel, pendingFocusOverrideModel, spawnStageName, spawnDirectiveDeclared, useLegacyFullPrompt, } = await resolveDirectiveOrFallback({
1728
195
  config, focusId, focusName, worktreePath, pendingDirective, assemblyManifest,
196
+ claudeSessionIds, deliveries, issues, readOnly,
1729
197
  });
1730
198
  // --- Phase B: resolve the session lineage (pass) and the engine backend
1731
199
  // (stamps lineage + sessionType onto the team state). The stage directive's
@@ -1734,6 +202,14 @@ export async function spawnFocusTeam(params) {
1734
202
  spawnDirectiveDeclared, spawnStageName, engineOverride, stageDirectiveEngine: directiveEngine,
1735
203
  config, orgDefaultEngine, focusName, teamState,
1736
204
  });
205
+ // Pin the focus-branch tip THIS review pass is covering (spawn-tip), captured
206
+ // now (post-rebase, before any mid-review commits). Carried to completion so
207
+ // the coverage watermark is stamped from what the review actually saw, and so
208
+ // the auto-approve gate can detect mid-review drift (spawn-tip != exit-tip).
209
+ // Review lineage only; coding teams leave it null.
210
+ if (teamState.sessionType === 'review') {
211
+ teamState.reviewSpawnTip = resolveRef(worktreePath, branchName);
212
+ }
1737
213
  // --- Phase: create the team-lead session. Opened AFTER Phase B so the
1738
214
  // (focus, lineage) pass is resolved on the authoritative lineage
1739
215
  // (teamState.sessionType), not a stale 'coding' default -- a 'review'-lineage
@@ -1747,14 +223,24 @@ export async function spawnFocusTeam(params) {
1747
223
  });
1748
224
  if (!session)
1749
225
  return;
226
+ // Legacy loop-context: resolved ONLY when the legacy full prompt will render it
227
+ // (the directive path drops it, so resolving there was a dead fetch + a
228
+ // duplicate focus.injections resolution). Empty string on the directive path.
229
+ let loopContext = '';
230
+ if (useLegacyFullPrompt) {
231
+ const loopContextResult = await resolveLegacyLoopContext(config, focusId);
232
+ loopContext = loopContextResult.content;
233
+ assemblyManifest.push(...loopContextResult.manifest);
234
+ }
1750
235
  // --- Phase C: build the prompt with the resolved engine's vocabulary ---
1751
236
  const promptContext = buildFocusPromptContext({
1752
237
  config, focusId, focusName, focusDescription, deliveries, issues, executionConfig,
1753
- pipelineConfig, productContextDocs, deploymentProfileSnapshot, readOnly, loopContext,
238
+ pipelineConfig, deploymentProfileSnapshot, readOnly, loopContext,
1754
239
  reviewFiledOpenCount, engineId: backend.id,
1755
240
  });
1756
241
  const prompt = buildSpawnPrompt({
1757
242
  role, promptContext, useLegacyFullPrompt, directiveBody, prdMilestoneContext,
243
+ answeredEscalationsSection: answeredEscalations.section,
1758
244
  isReviewPass: teamState.sessionType === 'review',
1759
245
  // Hand the review agent its own Telora agent_sessions.id so it stamps the
1760
246
  // outcome with the exact id product-api validates against (review.ts).
@@ -1856,9 +342,31 @@ export async function spawnFocusTeam(params) {
1856
342
  // reception guard. Null for hosted/Claude/flag-off spawns.
1857
343
  teamState.mcpProxy = modelResolution.mcpProxy ?? null;
1858
344
  // On resume, send an incremental delta instead of the full prompt (INJ-D);
1859
- // a fresh spawn sends the composed prompt unchanged.
345
+ // a fresh spawn sends the composed prompt unchanged. The git.diff manifest the
346
+ // resume delta delivers is captured here for the delivered-manifest record.
347
+ const gitDiffManifest = [];
1860
348
  const messageToSend = await buildSpawnMessage({
1861
349
  prompt, resumeId, readOnlyFocus: readOnly, role, promptContext, spawnSpec, config, focusId, focusName, deliveries, issues, worktreePath, priorFindings,
350
+ answeredEscalationsSection: answeredEscalations.section,
351
+ gitDiffManifestSink: gitDiffManifest,
352
+ });
353
+ // Assemble the DELIVERED manifest: the sources actually composed into the
354
+ // message just built, per its shape (agent_context_assembly truthfulness).
355
+ // assemblyManifest already carries only the directive recipe (fresh directive)
356
+ // or the loop-context recipe (fresh legacy) -- empty on resume-delta / scoping.
357
+ const spawnShape = scopingResume.isScopingSpawn
358
+ ? 'scoping'
359
+ : resumeId != null
360
+ ? 'resume-delta'
361
+ : useLegacyFullPrompt
362
+ ? 'fresh-legacy'
363
+ : 'fresh-directive';
364
+ const deliveredManifest = buildDeliveredManifest({
365
+ shape: spawnShape,
366
+ promptRecipeManifest: assemblyManifest,
367
+ answeredEscalationsSection: answeredEscalations.section,
368
+ prdMilestoneManifest,
369
+ gitDiffManifest,
1862
370
  });
1863
371
  // --- Final phase: build the spawn env, persist the manifest, write the
1864
372
  // session lifecycle, acquire the governor slot, sandbox-wrap, spawn the child,
@@ -1866,8 +374,9 @@ export async function spawnFocusTeam(params) {
1866
374
  // unavailable cleanups (session failed-write + slot release + activeTeams.delete).
1867
375
  await spawnTeamProcess({
1868
376
  config, focusId, focusName, session, teamState, backend, args, messageToSend,
1869
- logs, worktreePath, resumeId, assemblyManifest, spawnCodexHome, spawnLocalModelKey, isLocalModelPass,
1870
- effectiveModel, pendingDirective, spawnStageName, params,
377
+ logs, worktreePath, resumeId, assemblyManifest: deliveredManifest, spawnCodexHome, spawnLocalModelKey, isLocalModelPass,
378
+ effectiveModel, pendingDirective, spawnStageName, params, governor, spawnFocusTeam,
379
+ answeredEscalationIds: answeredEscalations.ids,
1871
380
  });
1872
381
  }
1873
382
  //# sourceMappingURL=focus-executor.js.map