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