@telora/daemon 0.19.43 → 0.19.74

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