smol-symphony 0.2.0 → 0.3.0
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/AGENTS.md +41 -22
- package/DESIGN.md +494 -273
- package/README.md +109 -57
- package/SPEC.md +33 -24
- package/WORKFLOW.minimal.yaml +34 -0
- package/{WORKFLOW.template.md → WORKFLOW.template.yaml} +409 -256
- package/WORKFLOW.yaml +487 -0
- package/assets/skills/symphony-issues/SKILL.md +136 -0
- package/assets/symphony-mise.system.toml +68 -0
- package/dist/src/bin/symphony.js +30 -0
- package/dist/src/bin/symphony.js.map +1 -0
- package/dist/src/core/actions/context.js +109 -0
- package/dist/src/core/actions/context.js.map +1 -0
- package/dist/{actions/parsing.js → src/core/actions/parse.js} +33 -114
- package/dist/src/core/actions/parse.js.map +1 -0
- package/dist/src/core/actions/plan.js +197 -0
- package/dist/src/core/actions/plan.js.map +1 -0
- package/dist/src/core/actions/predicates.js +111 -0
- package/dist/src/core/actions/predicates.js.map +1 -0
- package/dist/src/core/actions/run-fold.js +248 -0
- package/dist/src/core/actions/run-fold.js.map +1 -0
- package/dist/src/core/actions/template.js +118 -0
- package/dist/src/core/actions/template.js.map +1 -0
- package/dist/src/core/cli/args.js +116 -0
- package/dist/src/core/cli/args.js.map +1 -0
- package/dist/src/core/coerce.js +75 -0
- package/dist/src/core/coerce.js.map +1 -0
- package/dist/src/core/credential/account-id.js +20 -0
- package/dist/src/core/credential/account-id.js.map +1 -0
- package/dist/src/core/credential/adapter-config.js +136 -0
- package/dist/src/core/credential/adapter-config.js.map +1 -0
- package/dist/src/core/credential/availability.js +98 -0
- package/dist/src/core/credential/availability.js.map +1 -0
- package/dist/src/core/credential/extract.js +228 -0
- package/dist/src/core/credential/extract.js.map +1 -0
- package/dist/src/core/credential/fake-creds.js +171 -0
- package/dist/src/core/credential/fake-creds.js.map +1 -0
- package/dist/src/core/credential/identity.js +125 -0
- package/dist/src/core/credential/identity.js.map +1 -0
- package/dist/src/core/credential/shape.js +230 -0
- package/dist/src/core/credential/shape.js.map +1 -0
- package/dist/src/core/credential/strings.js +15 -0
- package/dist/src/core/credential/strings.js.map +1 -0
- package/dist/src/core/doctor/checks.js +303 -0
- package/dist/src/core/doctor/checks.js.map +1 -0
- package/dist/src/core/git/result.js +107 -0
- package/dist/src/core/git/result.js.map +1 -0
- package/dist/src/core/http/decisions.js +225 -0
- package/dist/src/core/http/decisions.js.map +1 -0
- package/dist/{http.js → src/core/http/render.js} +472 -738
- package/dist/src/core/http/render.js.map +1 -0
- package/dist/{http-handlers.js → src/core/http/routes.js} +52 -87
- package/dist/src/core/http/routes.js.map +1 -0
- package/dist/src/core/http/views.js +181 -0
- package/dist/src/core/http/views.js.map +1 -0
- package/dist/src/core/image/managed-image.js +95 -0
- package/dist/src/core/image/managed-image.js.map +1 -0
- package/dist/src/core/issue/file.js +149 -0
- package/dist/src/core/issue/file.js.map +1 -0
- package/dist/src/core/issue/parse.js +210 -0
- package/dist/src/core/issue/parse.js.map +1 -0
- package/dist/src/core/mcp/dispatch.js +239 -0
- package/dist/src/core/mcp/dispatch.js.map +1 -0
- package/dist/src/core/mcp/post-move.js +92 -0
- package/dist/src/core/mcp/post-move.js.map +1 -0
- package/dist/src/core/mcp/protocol.js +293 -0
- package/dist/src/core/mcp/protocol.js.map +1 -0
- package/dist/src/core/mcp/url.js +162 -0
- package/dist/src/core/mcp/url.js.map +1 -0
- package/dist/src/core/path.js +63 -0
- package/dist/src/core/path.js.map +1 -0
- package/dist/src/core/reconcile/image-decide.js +48 -0
- package/dist/src/core/reconcile/image-decide.js.map +1 -0
- package/dist/src/core/reconcile/ledger.js +142 -0
- package/dist/src/core/reconcile/ledger.js.map +1 -0
- package/dist/src/core/reconcile/pr-classify.js +62 -0
- package/dist/src/core/reconcile/pr-classify.js.map +1 -0
- package/dist/{reconciler → src/core/reconcile}/pr-decide.js +25 -12
- package/dist/src/core/reconcile/pr-decide.js.map +1 -0
- package/dist/src/core/reconcile/pr-loop.js +161 -0
- package/dist/src/core/reconcile/pr-loop.js.map +1 -0
- package/dist/src/core/reconcile/pr-notes.js +35 -0
- package/dist/src/core/reconcile/pr-notes.js.map +1 -0
- package/dist/src/core/reconcile/vm-decide.js +70 -0
- package/dist/src/core/reconcile/vm-decide.js.map +1 -0
- package/dist/src/core/reconcile/vm-reap.js +207 -0
- package/dist/src/core/reconcile/vm-reap.js.map +1 -0
- package/dist/src/core/reconcile/workspace-decide.js +162 -0
- package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/core/runlog/summary.js +231 -0
- package/dist/src/core/runlog/summary.js.map +1 -0
- package/dist/src/core/runner/dispatch-config.js +95 -0
- package/dist/src/core/runner/dispatch-config.js.map +1 -0
- package/dist/src/core/runner/injection.js +61 -0
- package/dist/src/core/runner/injection.js.map +1 -0
- package/dist/src/core/runner/mise.js +210 -0
- package/dist/src/core/runner/mise.js.map +1 -0
- package/dist/src/core/runner/prompt.js +720 -0
- package/dist/src/core/runner/prompt.js.map +1 -0
- package/dist/src/core/runner/turn.js +242 -0
- package/dist/src/core/runner/turn.js.map +1 -0
- package/dist/src/core/runner/vm-plan.js +390 -0
- package/dist/src/core/runner/vm-plan.js.map +1 -0
- package/dist/src/core/schedule/admission.js +123 -0
- package/dist/src/core/schedule/admission.js.map +1 -0
- package/dist/src/core/schedule/circuit-breaker.js +111 -0
- package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/core/schedule/eligibility.js +83 -0
- package/dist/src/core/schedule/eligibility.js.map +1 -0
- package/dist/src/core/schedule/reconcile-issue.js +82 -0
- package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
- package/dist/src/core/schedule/retry.js +96 -0
- package/dist/src/core/schedule/retry.js.map +1 -0
- package/dist/src/core/schedule/sleep-cycle.js +133 -0
- package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/core/schedule/slots.js +124 -0
- package/dist/src/core/schedule/slots.js.map +1 -0
- package/dist/src/core/schedule/tick.js +553 -0
- package/dist/src/core/schedule/tick.js.map +1 -0
- package/dist/src/core/schedule/token-fold.js +181 -0
- package/dist/src/core/schedule/token-fold.js.map +1 -0
- package/dist/src/core/state-resolve.js +86 -0
- package/dist/src/core/state-resolve.js.map +1 -0
- package/dist/src/core/vm-guards.js +278 -0
- package/dist/src/core/vm-guards.js.map +1 -0
- package/dist/src/core/workflow/derive.js +107 -0
- package/dist/src/core/workflow/derive.js.map +1 -0
- package/dist/src/core/workflow/parse.js +687 -0
- package/dist/src/core/workflow/parse.js.map +1 -0
- package/dist/src/core/workflow/prompt-probe.js +78 -0
- package/dist/src/core/workflow/prompt-probe.js.map +1 -0
- package/dist/src/core/workflow/validate.js +189 -0
- package/dist/src/core/workflow/validate.js.map +1 -0
- package/dist/src/core/workspace-key.js +19 -0
- package/dist/src/core/workspace-key.js.map +1 -0
- package/dist/src/shell/actions-runner.js +356 -0
- package/dist/src/shell/actions-runner.js.map +1 -0
- package/dist/src/shell/adapter/adapter-registry.js +45 -0
- package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
- package/dist/src/shell/adapter/clock-random.js +96 -0
- package/dist/src/shell/adapter/clock-random.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/shell/adapter/launcher-asset.js +57 -0
- package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
- package/dist/src/shell/adapter/mise-config-asset.js +65 -0
- package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
- package/dist/src/shell/adapter/workflow-loader.js +304 -0
- package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
- package/dist/src/shell/cli/doctor.js +268 -0
- package/dist/src/shell/cli/doctor.js.map +1 -0
- package/dist/src/shell/effect-interpreter-families.js +314 -0
- package/dist/src/shell/effect-interpreter-families.js.map +1 -0
- package/dist/src/shell/effect-interpreter.js +29 -0
- package/dist/src/shell/effect-interpreter.js.map +1 -0
- package/dist/src/shell/interp/acp-frame.js +137 -0
- package/dist/src/shell/interp/acp-frame.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-conn.js +320 -0
- package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-frames.js +159 -0
- package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
- package/dist/src/shell/interp/acp-ws.js +197 -0
- package/dist/src/shell/interp/acp-ws.js.map +1 -0
- package/dist/src/shell/interp/acp.js +319 -0
- package/dist/src/shell/interp/acp.js.map +1 -0
- package/dist/src/shell/interp/credential-defaults.js +128 -0
- package/dist/src/shell/interp/credential-defaults.js.map +1 -0
- package/dist/src/shell/interp/credential-hooks.js +149 -0
- package/dist/src/shell/interp/credential-hooks.js.map +1 -0
- package/dist/src/shell/interp/credential-registry.js +226 -0
- package/dist/src/shell/interp/credential-registry.js.map +1 -0
- package/dist/src/shell/interp/credential.js +103 -0
- package/dist/src/shell/interp/credential.js.map +1 -0
- package/dist/src/shell/interp/gh.js +163 -0
- package/dist/src/shell/interp/gh.js.map +1 -0
- package/dist/src/shell/interp/git.js +28 -0
- package/dist/src/shell/interp/git.js.map +1 -0
- package/dist/src/shell/interp/log.js +213 -0
- package/dist/src/shell/interp/log.js.map +1 -0
- package/dist/src/shell/interp/process.js +178 -0
- package/dist/src/shell/interp/process.js.map +1 -0
- package/dist/src/shell/interp/runlog.js +193 -0
- package/dist/src/shell/interp/runlog.js.map +1 -0
- package/dist/src/shell/interp/timer.js +64 -0
- package/dist/src/shell/interp/timer.js.map +1 -0
- package/dist/src/shell/interp/tracker-disk.js +99 -0
- package/dist/src/shell/interp/tracker-disk.js.map +1 -0
- package/dist/src/shell/interp/tracker-parse.js +71 -0
- package/dist/src/shell/interp/tracker-parse.js.map +1 -0
- package/dist/src/shell/interp/tracker-scan.js +238 -0
- package/dist/src/shell/interp/tracker-scan.js.map +1 -0
- package/dist/src/shell/interp/tracker-write.js +91 -0
- package/dist/src/shell/interp/tracker-write.js.map +1 -0
- package/dist/src/shell/interp/tracker.js +41 -0
- package/dist/src/shell/interp/tracker.js.map +1 -0
- package/dist/src/shell/interp/tty.js +48 -0
- package/dist/src/shell/interp/tty.js.map +1 -0
- package/dist/src/shell/interp/vm.js +199 -0
- package/dist/src/shell/interp/vm.js.map +1 -0
- package/dist/src/shell/interp/workspace.js +310 -0
- package/dist/src/shell/interp/workspace.js.map +1 -0
- package/dist/src/shell/main-acp.js +78 -0
- package/dist/src/shell/main-acp.js.map +1 -0
- package/dist/src/shell/main-adapters.js +222 -0
- package/dist/src/shell/main-adapters.js.map +1 -0
- package/dist/src/shell/main-credential.js +122 -0
- package/dist/src/shell/main-credential.js.map +1 -0
- package/dist/src/shell/main-doctor.js +22 -0
- package/dist/src/shell/main-doctor.js.map +1 -0
- package/dist/src/shell/main-entry.js +46 -0
- package/dist/src/shell/main-entry.js.map +1 -0
- package/dist/src/shell/main-http-csrf.js +45 -0
- package/dist/src/shell/main-http-csrf.js.map +1 -0
- package/dist/src/shell/main-http-handler.js +389 -0
- package/dist/src/shell/main-http-handler.js.map +1 -0
- package/dist/src/shell/main-http-mcp.js +122 -0
- package/dist/src/shell/main-http-mcp.js.map +1 -0
- package/dist/src/shell/main-http-views.js +253 -0
- package/dist/src/shell/main-http-views.js.map +1 -0
- package/dist/src/shell/main-http.js +76 -0
- package/dist/src/shell/main-http.js.map +1 -0
- package/dist/src/shell/main-loops.js +130 -0
- package/dist/src/shell/main-loops.js.map +1 -0
- package/dist/src/shell/main-mcp.js +129 -0
- package/dist/src/shell/main-mcp.js.map +1 -0
- package/dist/src/shell/main-orchestrator.js +120 -0
- package/dist/src/shell/main-orchestrator.js.map +1 -0
- package/dist/src/shell/main-preflight.js +43 -0
- package/dist/src/shell/main-preflight.js.map +1 -0
- package/dist/src/shell/main-reconcilers-helpers.js +244 -0
- package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
- package/dist/src/shell/main-reconcilers-pr.js +148 -0
- package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
- package/dist/src/shell/main-reconcilers.js +225 -0
- package/dist/src/shell/main-reconcilers.js.map +1 -0
- package/dist/src/shell/main-runner.js +355 -0
- package/dist/src/shell/main-runner.js.map +1 -0
- package/dist/src/shell/main-scaffold.js +116 -0
- package/dist/src/shell/main-scaffold.js.map +1 -0
- package/dist/src/shell/main-shutdown.js +115 -0
- package/dist/src/shell/main-shutdown.js.map +1 -0
- package/dist/src/shell/main-startup.js +48 -0
- package/dist/src/shell/main-startup.js.map +1 -0
- package/dist/src/shell/main-substrates.js +43 -0
- package/dist/src/shell/main-substrates.js.map +1 -0
- package/dist/src/shell/main.js +385 -0
- package/dist/src/shell/main.js.map +1 -0
- package/dist/src/shell/orchestrator-feedback.js +69 -0
- package/dist/src/shell/orchestrator-feedback.js.map +1 -0
- package/dist/src/shell/orchestrator-image.js +167 -0
- package/dist/src/shell/orchestrator-image.js.map +1 -0
- package/dist/src/shell/orchestrator-loop.js +468 -0
- package/dist/src/shell/orchestrator-loop.js.map +1 -0
- package/dist/src/shell/orchestrator-reconcile.js +36 -0
- package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
- package/dist/src/shell/reconciler-loop.js +228 -0
- package/dist/src/shell/reconciler-loop.js.map +1 -0
- package/dist/src/shell/runner-loop-turn.js +301 -0
- package/dist/src/shell/runner-loop-turn.js.map +1 -0
- package/dist/src/shell/runner-loop.js +338 -0
- package/dist/src/shell/runner-loop.js.map +1 -0
- package/dist/src/shell/server/http.js +208 -0
- package/dist/src/shell/server/http.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
- package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime.js +99 -0
- package/dist/src/shell/server/mcp-runtime.js.map +1 -0
- package/dist/src/shell/workspace-key.js +14 -0
- package/dist/src/shell/workspace-key.js.map +1 -0
- package/dist/src/types/acp.js +8 -0
- package/dist/src/types/acp.js.map +1 -0
- package/dist/src/types/actions/plan.js +6 -0
- package/dist/src/types/actions/plan.js.map +1 -0
- package/dist/src/types/actions/predicates.js +6 -0
- package/dist/src/types/actions/predicates.js.map +1 -0
- package/dist/src/types/actions/run-fold.js +8 -0
- package/dist/src/types/actions/run-fold.js.map +1 -0
- package/dist/src/types/actions.js +7 -0
- package/dist/src/types/actions.js.map +1 -0
- package/dist/src/types/adapter/clock-random.js +4 -0
- package/dist/src/types/adapter/clock-random.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
- package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/types/adapter/workflow-loader.js +4 -0
- package/dist/src/types/adapter/workflow-loader.js.map +1 -0
- package/dist/src/types/cli/args.js +8 -0
- package/dist/src/types/cli/args.js.map +1 -0
- package/dist/src/types/config.js +8 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/credential-interp.js +6 -0
- package/dist/src/types/credential-interp.js.map +1 -0
- package/dist/src/types/credentials.js +10 -0
- package/dist/src/types/credentials.js.map +1 -0
- package/dist/src/types/doctor.js +7 -0
- package/dist/src/types/doctor.js.map +1 -0
- package/dist/src/types/domain.js +7 -0
- package/dist/src/types/domain.js.map +1 -0
- package/dist/src/types/effect.js +15 -0
- package/dist/src/types/effect.js.map +1 -0
- package/dist/src/types/errors.js +39 -0
- package/dist/src/types/errors.js.map +1 -0
- package/dist/src/types/http/decisions.js +6 -0
- package/dist/src/types/http/decisions.js.map +1 -0
- package/dist/src/types/http/render.js +10 -0
- package/dist/src/types/http/render.js.map +1 -0
- package/dist/src/types/http/views.js +6 -0
- package/dist/src/types/http/views.js.map +1 -0
- package/dist/src/types/http.js +9 -0
- package/dist/src/types/http.js.map +1 -0
- package/dist/src/types/image/managed-image.js +7 -0
- package/dist/src/types/image/managed-image.js.map +1 -0
- package/dist/src/types/interp/effect-interpreter.js +8 -0
- package/dist/src/types/interp/effect-interpreter.js.map +1 -0
- package/dist/src/types/interp/tracker.js +7 -0
- package/dist/src/types/interp/tracker.js.map +1 -0
- package/dist/src/types/issue/file.js +6 -0
- package/dist/src/types/issue/file.js.map +1 -0
- package/dist/src/types/issue/parse.js +8 -0
- package/dist/src/types/issue/parse.js.map +1 -0
- package/dist/src/types/main-acp.js +13 -0
- package/dist/src/types/main-acp.js.map +1 -0
- package/dist/src/types/main-adapters.js +5 -0
- package/dist/src/types/main-adapters.js.map +1 -0
- package/dist/src/types/main-credential.js +21 -0
- package/dist/src/types/main-credential.js.map +1 -0
- package/dist/src/types/main-doctor.js +6 -0
- package/dist/src/types/main-doctor.js.map +1 -0
- package/dist/src/types/main-http-handler.js +12 -0
- package/dist/src/types/main-http-handler.js.map +1 -0
- package/dist/src/types/main-http.js +5 -0
- package/dist/src/types/main-http.js.map +1 -0
- package/dist/src/types/main-loops.js +5 -0
- package/dist/src/types/main-loops.js.map +1 -0
- package/dist/src/types/main-mcp.js +12 -0
- package/dist/src/types/main-mcp.js.map +1 -0
- package/dist/src/types/main-orchestrator.js +5 -0
- package/dist/src/types/main-orchestrator.js.map +1 -0
- package/dist/src/types/main-reconcilers.js +11 -0
- package/dist/src/types/main-reconcilers.js.map +1 -0
- package/dist/src/types/main-runner.js +13 -0
- package/dist/src/types/main-runner.js.map +1 -0
- package/dist/src/types/main-startup.js +5 -0
- package/dist/src/types/main-startup.js.map +1 -0
- package/dist/src/types/main-substrates.js +5 -0
- package/dist/src/types/main-substrates.js.map +1 -0
- package/dist/src/types/mcp/dispatch.js +4 -0
- package/dist/src/types/mcp/dispatch.js.map +1 -0
- package/dist/src/types/mcp/post-move.js +7 -0
- package/dist/src/types/mcp/post-move.js.map +1 -0
- package/dist/src/types/mcp.js +9 -0
- package/dist/src/types/mcp.js.map +1 -0
- package/dist/src/types/ports.js +12 -0
- package/dist/src/types/ports.js.map +1 -0
- package/dist/src/types/reconcile/image-decide.js +5 -0
- package/dist/src/types/reconcile/image-decide.js.map +1 -0
- package/dist/src/types/reconcile/ledger.js +7 -0
- package/dist/src/types/reconcile/ledger.js.map +1 -0
- package/dist/src/types/reconcile/pr-loop.js +8 -0
- package/dist/src/types/reconcile/pr-loop.js.map +1 -0
- package/dist/src/types/reconcile/vm-reap.js +8 -0
- package/dist/src/types/reconcile/vm-reap.js.map +1 -0
- package/dist/src/types/reconcile/workspace-decide.js +7 -0
- package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/types/reconcile.js +9 -0
- package/dist/src/types/reconcile.js.map +1 -0
- package/dist/src/types/runlog.js +7 -0
- package/dist/src/types/runlog.js.map +1 -0
- package/dist/src/types/runner/actions-runner.js +12 -0
- package/dist/src/types/runner/actions-runner.js.map +1 -0
- package/dist/src/types/runner/gondolin-dispatch.js +5 -0
- package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
- package/dist/src/types/runner/injection.js +6 -0
- package/dist/src/types/runner/injection.js.map +1 -0
- package/dist/src/types/runner/runner-loop.js +5 -0
- package/dist/src/types/runner/runner-loop.js.map +1 -0
- package/dist/src/types/runner/turn.js +4 -0
- package/dist/src/types/runner/turn.js.map +1 -0
- package/dist/src/types/runner/vm-plan.js +4 -0
- package/dist/src/types/runner/vm-plan.js.map +1 -0
- package/dist/src/types/runtime.js +9 -0
- package/dist/src/types/runtime.js.map +1 -0
- package/dist/src/types/schedule/admission.js +7 -0
- package/dist/src/types/schedule/admission.js.map +1 -0
- package/dist/src/types/schedule/circuit-breaker.js +2 -0
- package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/types/schedule/eligibility.js +9 -0
- package/dist/src/types/schedule/eligibility.js.map +1 -0
- package/dist/src/types/schedule/orchestrator-loop.js +10 -0
- package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
- package/dist/src/types/schedule/sleep-cycle.js +4 -0
- package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/types/schedule/slots.js +8 -0
- package/dist/src/types/schedule/slots.js.map +1 -0
- package/dist/src/types/schedule/tick.js +9 -0
- package/dist/src/types/schedule/tick.js.map +1 -0
- package/dist/src/types/server/mcp-runtime.js +8 -0
- package/dist/src/types/server/mcp-runtime.js.map +1 -0
- package/dist/src/types/workflow/parse.js +4 -0
- package/dist/src/types/workflow/parse.js.map +1 -0
- package/dist/tests/core/account-id.test.js +35 -0
- package/dist/tests/core/account-id.test.js.map +1 -0
- package/dist/tests/core/actions-parse.test.js +176 -0
- package/dist/tests/core/actions-parse.test.js.map +1 -0
- package/dist/tests/core/adapter-config.test.js +133 -0
- package/dist/tests/core/adapter-config.test.js.map +1 -0
- package/dist/tests/core/admission.test.js +215 -0
- package/dist/tests/core/admission.test.js.map +1 -0
- package/dist/tests/core/args.test.js +132 -0
- package/dist/tests/core/args.test.js.map +1 -0
- package/dist/tests/core/availability.test.js +62 -0
- package/dist/tests/core/availability.test.js.map +1 -0
- package/dist/tests/core/checks.test.js +395 -0
- package/dist/tests/core/checks.test.js.map +1 -0
- package/dist/tests/core/circuit-breaker.test.js +172 -0
- package/dist/tests/core/circuit-breaker.test.js.map +1 -0
- package/dist/tests/core/coerce.test.js +87 -0
- package/dist/tests/core/coerce.test.js.map +1 -0
- package/dist/tests/core/context.test.js +228 -0
- package/dist/tests/core/context.test.js.map +1 -0
- package/dist/tests/core/decisions.test.js +310 -0
- package/dist/tests/core/decisions.test.js.map +1 -0
- package/dist/tests/core/derive.test.js +205 -0
- package/dist/tests/core/derive.test.js.map +1 -0
- package/dist/tests/core/dispatch-config.test.js +164 -0
- package/dist/tests/core/dispatch-config.test.js.map +1 -0
- package/dist/tests/core/dispatch.test.js +302 -0
- package/dist/tests/core/dispatch.test.js.map +1 -0
- package/dist/tests/core/eligibility.test.js +163 -0
- package/dist/tests/core/eligibility.test.js.map +1 -0
- package/dist/tests/core/extract.test.js +139 -0
- package/dist/tests/core/extract.test.js.map +1 -0
- package/dist/tests/core/fake-creds.test.js +134 -0
- package/dist/tests/core/fake-creds.test.js.map +1 -0
- package/dist/tests/core/file.test.js +197 -0
- package/dist/tests/core/file.test.js.map +1 -0
- package/dist/tests/core/git-result.test.js +113 -0
- package/dist/tests/core/git-result.test.js.map +1 -0
- package/dist/tests/core/identity.test.js +180 -0
- package/dist/tests/core/identity.test.js.map +1 -0
- package/dist/tests/core/image-decide.test.js +59 -0
- package/dist/tests/core/image-decide.test.js.map +1 -0
- package/dist/tests/core/injection.test.js +163 -0
- package/dist/tests/core/injection.test.js.map +1 -0
- package/dist/tests/core/ledger.test.js +218 -0
- package/dist/tests/core/ledger.test.js.map +1 -0
- package/dist/tests/core/managed-image.test.js +68 -0
- package/dist/tests/core/managed-image.test.js.map +1 -0
- package/dist/tests/core/mise.test.js +138 -0
- package/dist/tests/core/mise.test.js.map +1 -0
- package/dist/tests/core/parse.test.js +174 -0
- package/dist/tests/core/parse.test.js.map +1 -0
- package/dist/tests/core/path.test.js +50 -0
- package/dist/tests/core/path.test.js.map +1 -0
- package/dist/tests/core/plan.test.js +218 -0
- package/dist/tests/core/plan.test.js.map +1 -0
- package/dist/tests/core/post-move.test.js +162 -0
- package/dist/tests/core/post-move.test.js.map +1 -0
- package/dist/tests/core/pr-classify.test.js +117 -0
- package/dist/tests/core/pr-classify.test.js.map +1 -0
- package/dist/tests/core/pr-decide.test.js +298 -0
- package/dist/tests/core/pr-decide.test.js.map +1 -0
- package/dist/tests/core/pr-loop.test.js +301 -0
- package/dist/tests/core/pr-loop.test.js.map +1 -0
- package/dist/tests/core/pr-notes.test.js +165 -0
- package/dist/tests/core/pr-notes.test.js.map +1 -0
- package/dist/tests/core/predicates.test.js +154 -0
- package/dist/tests/core/predicates.test.js.map +1 -0
- package/dist/tests/core/prompt.test.js +189 -0
- package/dist/tests/core/prompt.test.js.map +1 -0
- package/dist/tests/core/protocol.test.js +195 -0
- package/dist/tests/core/protocol.test.js.map +1 -0
- package/dist/tests/core/reconcile-issue.test.js +116 -0
- package/dist/tests/core/reconcile-issue.test.js.map +1 -0
- package/dist/tests/core/render.test.js +549 -0
- package/dist/tests/core/render.test.js.map +1 -0
- package/dist/tests/core/retry.test.js +186 -0
- package/dist/tests/core/retry.test.js.map +1 -0
- package/dist/tests/core/routes.test.js +247 -0
- package/dist/tests/core/routes.test.js.map +1 -0
- package/dist/tests/core/run-fold.test.js +299 -0
- package/dist/tests/core/run-fold.test.js.map +1 -0
- package/dist/tests/core/shape.test.js +185 -0
- package/dist/tests/core/shape.test.js.map +1 -0
- package/dist/tests/core/sleep-cycle.test.js +150 -0
- package/dist/tests/core/sleep-cycle.test.js.map +1 -0
- package/dist/tests/core/slots.test.js +201 -0
- package/dist/tests/core/slots.test.js.map +1 -0
- package/dist/tests/core/state-resolve.test.js +80 -0
- package/dist/tests/core/state-resolve.test.js.map +1 -0
- package/dist/tests/core/summary.test.js +200 -0
- package/dist/tests/core/summary.test.js.map +1 -0
- package/dist/tests/core/template.test.js +116 -0
- package/dist/tests/core/template.test.js.map +1 -0
- package/dist/tests/core/tick.test.js +558 -0
- package/dist/tests/core/tick.test.js.map +1 -0
- package/dist/tests/core/token-fold.test.js +176 -0
- package/dist/tests/core/token-fold.test.js.map +1 -0
- package/dist/tests/core/turn.test.js +388 -0
- package/dist/tests/core/turn.test.js.map +1 -0
- package/dist/tests/core/url.test.js +118 -0
- package/dist/tests/core/url.test.js.map +1 -0
- package/dist/tests/core/validate.test.js +247 -0
- package/dist/tests/core/validate.test.js.map +1 -0
- package/dist/tests/core/views.test.js +252 -0
- package/dist/tests/core/views.test.js.map +1 -0
- package/dist/tests/core/vm-decide.test.js +110 -0
- package/dist/tests/core/vm-decide.test.js.map +1 -0
- package/dist/tests/core/vm-guards.test.js +153 -0
- package/dist/tests/core/vm-guards.test.js.map +1 -0
- package/dist/tests/core/vm-plan.test.js +332 -0
- package/dist/tests/core/vm-plan.test.js.map +1 -0
- package/dist/tests/core/vm-reap.test.js +196 -0
- package/dist/tests/core/vm-reap.test.js.map +1 -0
- package/dist/tests/core/workflow-parse.test.js +493 -0
- package/dist/tests/core/workflow-parse.test.js.map +1 -0
- package/dist/tests/core/workspace-decide.test.js +236 -0
- package/dist/tests/core/workspace-decide.test.js.map +1 -0
- package/dist/tests/helpers/fixtures.js +167 -0
- package/dist/tests/helpers/fixtures.js.map +1 -0
- package/dist/tests/shell/acp-substrate.test.js +101 -0
- package/dist/tests/shell/acp-substrate.test.js.map +1 -0
- package/dist/tests/shell/actions-runner-push.test.js +203 -0
- package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
- package/dist/tests/shell/credential-hooks.test.js +36 -0
- package/dist/tests/shell/credential-hooks.test.js.map +1 -0
- package/dist/tests/shell/credential-registry.test.js +165 -0
- package/dist/tests/shell/credential-registry.test.js.map +1 -0
- package/dist/tests/shell/credential-substrate.test.js +179 -0
- package/dist/tests/shell/credential-substrate.test.js.map +1 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
- package/dist/tests/shell/doctor.test.js +101 -0
- package/dist/tests/shell/doctor.test.js.map +1 -0
- package/dist/tests/shell/effect-vm-create.test.js +52 -0
- package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
- package/dist/tests/shell/gh-port.test.js +63 -0
- package/dist/tests/shell/gh-port.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
- package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
- package/dist/tests/shell/http-handler.test.js +608 -0
- package/dist/tests/shell/http-handler.test.js.map +1 -0
- package/dist/tests/shell/http-server.test.js +53 -0
- package/dist/tests/shell/http-server.test.js.map +1 -0
- package/dist/tests/shell/mcp-runtime.test.js +366 -0
- package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
- package/dist/tests/shell/mise-config-asset.test.js +87 -0
- package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
- package/dist/tests/shell/orchestrator-loop.test.js +583 -0
- package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
- package/dist/tests/shell/reconciler-passes.test.js +314 -0
- package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
- package/dist/tests/shell/runner-loop-turn.test.js +97 -0
- package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
- package/dist/tests/shell/runner-slice.test.js +536 -0
- package/dist/tests/shell/runner-slice.test.js.map +1 -0
- package/dist/tests/shell/scaffold.test.js +65 -0
- package/dist/tests/shell/scaffold.test.js.map +1 -0
- package/dist/tests/shell/tick-config.test.js +83 -0
- package/dist/tests/shell/tick-config.test.js.map +1 -0
- package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
- package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
- package/dist/tests/shell/tracker-write-issue.test.js +154 -0
- package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
- package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
- package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
- package/dist/tests/shell/workspace-live-config.test.js +140 -0
- package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
- package/package.json +21 -9
- package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
- package/prompts/Reflect.md +91 -0
- package/prompts/Review.md +97 -0
- package/prompts/Todo.md +96 -0
- package/prompts/_footer.md +41 -0
- package/prompts/_preamble.md +42 -0
- package/prompts-minimal/Todo.md +26 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/vm-agent.mjs +312 -90
- package/WORKFLOW.md +0 -744
- package/dist/acp-bridge.js +0 -324
- package/dist/acp-bridge.js.map +0 -1
- package/dist/actions/cache.js +0 -191
- package/dist/actions/cache.js.map +0 -1
- package/dist/actions/effects.js +0 -41
- package/dist/actions/effects.js.map +0 -1
- package/dist/actions/executor.js +0 -570
- package/dist/actions/executor.js.map +0 -1
- package/dist/actions/index.js +0 -13
- package/dist/actions/index.js.map +0 -1
- package/dist/actions/parsing.js.map +0 -1
- package/dist/actions/predicate-env.js +0 -27
- package/dist/actions/predicate-env.js.map +0 -1
- package/dist/actions/predicates.js +0 -49
- package/dist/actions/predicates.js.map +0 -1
- package/dist/actions/templating.js +0 -66
- package/dist/actions/templating.js.map +0 -1
- package/dist/actions/types.js +0 -15
- package/dist/actions/types.js.map +0 -1
- package/dist/agent/acp.js +0 -473
- package/dist/agent/acp.js.map +0 -1
- package/dist/agent/adapter-names.js +0 -159
- package/dist/agent/adapter-names.js.map +0 -1
- package/dist/agent/adapters.js +0 -511
- package/dist/agent/adapters.js.map +0 -1
- package/dist/agent/credential-extractors.js +0 -342
- package/dist/agent/credential-extractors.js.map +0 -1
- package/dist/agent/credential-secrets.js +0 -628
- package/dist/agent/credential-secrets.js.map +0 -1
- package/dist/agent/credential-ticker.js +0 -57
- package/dist/agent/credential-ticker.js.map +0 -1
- package/dist/agent/gondolin-creds-staging.js +0 -356
- package/dist/agent/gondolin-creds-staging.js.map +0 -1
- package/dist/agent/gondolin-dispatch.js +0 -375
- package/dist/agent/gondolin-dispatch.js.map +0 -1
- package/dist/agent/gondolin.js +0 -124
- package/dist/agent/gondolin.js.map +0 -1
- package/dist/agent/runner-decisions.js +0 -134
- package/dist/agent/runner-decisions.js.map +0 -1
- package/dist/agent/runner.js +0 -1456
- package/dist/agent/runner.js.map +0 -1
- package/dist/agent/tool-call-summary.js +0 -102
- package/dist/agent/tool-call-summary.js.map +0 -1
- package/dist/agent/vm-acp-mapping.js +0 -73
- package/dist/agent/vm-acp-mapping.js.map +0 -1
- package/dist/agent/vm-guards.js +0 -262
- package/dist/agent/vm-guards.js.map +0 -1
- package/dist/agent/vm-port.js +0 -22
- package/dist/agent/vm-port.js.map +0 -1
- package/dist/agent/vm-process-registry.js +0 -79
- package/dist/agent/vm-process-registry.js.map +0 -1
- package/dist/bin/cli-args.js +0 -105
- package/dist/bin/cli-args.js.map +0 -1
- package/dist/bin/symphony.js +0 -794
- package/dist/bin/symphony.js.map +0 -1
- package/dist/errors.js +0 -15
- package/dist/errors.js.map +0 -1
- package/dist/http-disk.js +0 -135
- package/dist/http-disk.js.map +0 -1
- package/dist/http-handlers.js.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/issues.js +0 -178
- package/dist/issues.js.map +0 -1
- package/dist/logging.js +0 -203
- package/dist/logging.js.map +0 -1
- package/dist/mcp.js +0 -706
- package/dist/mcp.js.map +0 -1
- package/dist/memory.js +0 -85
- package/dist/memory.js.map +0 -1
- package/dist/orchestrator-decisions.js +0 -331
- package/dist/orchestrator-decisions.js.map +0 -1
- package/dist/orchestrator.js +0 -1569
- package/dist/orchestrator.js.map +0 -1
- package/dist/prompt.js +0 -65
- package/dist/prompt.js.map +0 -1
- package/dist/reconciler/cache.js +0 -65
- package/dist/reconciler/cache.js.map +0 -1
- package/dist/reconciler/index.js +0 -448
- package/dist/reconciler/index.js.map +0 -1
- package/dist/reconciler/ledger.js +0 -131
- package/dist/reconciler/ledger.js.map +0 -1
- package/dist/reconciler/pr-adapters.js +0 -174
- package/dist/reconciler/pr-adapters.js.map +0 -1
- package/dist/reconciler/pr-decide.js.map +0 -1
- package/dist/reconciler/pr.js +0 -422
- package/dist/reconciler/pr.js.map +0 -1
- package/dist/reconciler/types.js +0 -12
- package/dist/reconciler/types.js.map +0 -1
- package/dist/reconciler/vm.js +0 -243
- package/dist/reconciler/vm.js.map +0 -1
- package/dist/reconciler/workspace-defaults.js +0 -83
- package/dist/reconciler/workspace-defaults.js.map +0 -1
- package/dist/reconciler/workspace.js +0 -272
- package/dist/reconciler/workspace.js.map +0 -1
- package/dist/runlog.js +0 -403
- package/dist/runlog.js.map +0 -1
- package/dist/scaffold.js +0 -165
- package/dist/scaffold.js.map +0 -1
- package/dist/trackers/local.js +0 -445
- package/dist/trackers/local.js.map +0 -1
- package/dist/trackers/types.js +0 -10
- package/dist/trackers/types.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/util/clock.js +0 -12
- package/dist/util/clock.js.map +0 -1
- package/dist/util/crypto.js +0 -25
- package/dist/util/crypto.js.map +0 -1
- package/dist/util/frontmatter.js +0 -70
- package/dist/util/frontmatter.js.map +0 -1
- package/dist/util/fs-issues.js +0 -22
- package/dist/util/fs-issues.js.map +0 -1
- package/dist/util/process.js +0 -152
- package/dist/util/process.js.map +0 -1
- package/dist/util/workspace-key.js +0 -10
- package/dist/util/workspace-key.js.map +0 -1
- package/dist/workflow-loader.js +0 -147
- package/dist/workflow-loader.js.map +0 -1
- package/dist/workflow.js +0 -822
- package/dist/workflow.js.map +0 -1
- package/dist/workspace-types.js +0 -8
- package/dist/workspace-types.js.map +0 -1
- package/dist/workspace.js +0 -443
- package/dist/workspace.js.map +0 -1
package/dist/agent/runner.js
DELETED
|
@@ -1,1456 +0,0 @@
|
|
|
1
|
-
// Agent Runner (SPEC §6.2): workspace + prompt + ACP session, with continuation turns up
|
|
2
|
-
// to agent.max_turns. The ACP adapter (claude-agent-acp / codex-acp / opencode acp) runs
|
|
3
|
-
// inside a per-issue Gondolin VM. The host workspace directory is volume-mounted into
|
|
4
|
-
// the VM at the same absolute path so cwd values are consistent.
|
|
5
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
-
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { WorkspaceManager, fetchBaseInWorkspace, resolveBaseBranch, resolveGithubRepo, sanitizeWorkspaceKey, } from '../workspace.js';
|
|
10
|
-
import { renderPrompt } from '../prompt.js';
|
|
11
|
-
import { SYMPHONY_VM_PREFIX } from './vm-port.js';
|
|
12
|
-
import { GondolinDispatcher, } from './gondolin-dispatch.js';
|
|
13
|
-
import { MCP_GUEST_BASE_URL } from './vm-acp-mapping.js';
|
|
14
|
-
import { stripCredentialEnv } from './vm-guards.js';
|
|
15
|
-
import { AcpClient } from './acp.js';
|
|
16
|
-
import { ADAPTERS, isKnownAdapter, } from './adapters.js';
|
|
17
|
-
import { activeStateNames } from '../issues.js';
|
|
18
|
-
import { withIssue } from '../logging.js';
|
|
19
|
-
import { resolveActionsForState } from '../workflow.js';
|
|
20
|
-
import { parseFrontMatterLenient } from '../util/frontmatter.js';
|
|
21
|
-
import { runActions, toActionsSnapshot, } from '../actions/index.js';
|
|
22
|
-
import { defaultPredicateEnv } from '../actions/predicate-env.js';
|
|
23
|
-
import { classifyTurnOutcome, computeForwardedEnv, decideAttemptOutcome, decideTurnContinuation, deriveActionContext, selectPromptKind, } from './runner-decisions.js';
|
|
24
|
-
const CONTINUATION_PROMPT_WITH_MCP = 'Continue working on the same issue. Pick up where the prior turn left off and proceed with the next concrete action. If the work is fully complete, summarize what changed and call the symphony.transition tool to hand off to the next state.';
|
|
25
|
-
const CONTINUATION_PROMPT_NO_MCP = 'Continue working on the same issue. Pick up where the prior turn left off and proceed with the next concrete action. If the work is fully complete, summarize what changed and stop.';
|
|
26
|
-
function continuationPrompt(mcpEnabled) {
|
|
27
|
-
return mcpEnabled ? CONTINUATION_PROMPT_WITH_MCP : CONTINUATION_PROMPT_NO_MCP;
|
|
28
|
-
}
|
|
29
|
-
// Source of truth for "which state's cleanup actions should fire."
|
|
30
|
-
// Prefers the running-entry's current issue state (the runner may have moved
|
|
31
|
-
// it during the attempt, e.g. via a typed-action reroute), otherwise falls
|
|
32
|
-
// back to the issue snapshot the attempt was launched with.
|
|
33
|
-
function resolveCleanupState(issue, runningEntry) {
|
|
34
|
-
return runningEntry?.issue.state ?? issue.state;
|
|
35
|
-
}
|
|
36
|
-
// Stage the SYMPHONY_* env vars + temp body file consumed by the Done-state
|
|
37
|
-
// `actions:` block (push_branch + create_pr_if_missing). Reads the current
|
|
38
|
-
// issue file from <tracker_root>/<state>/<identifier>.md so any transition
|
|
39
|
-
// notes appended by `symphony.transition` ride through into the PR body; falls
|
|
40
|
-
// back to the in-memory description if the file isn't reachable (no tracker
|
|
41
|
-
// root pinned, or read failure). Returns the env map and a cleanup closure the
|
|
42
|
-
// caller MUST run after the actions complete, so the temp file is removed
|
|
43
|
-
// promptly.
|
|
44
|
-
async function stageActionContextEnv(entry) {
|
|
45
|
-
const issue = entry.issue;
|
|
46
|
-
const ident = entry.identifier;
|
|
47
|
-
const branch = `agent/${ident}`;
|
|
48
|
-
let body = issue.description ?? '';
|
|
49
|
-
if (entry.tracker_root_at_dispatch) {
|
|
50
|
-
const issuePath = path.join(entry.tracker_root_at_dispatch, issue.state, `${ident}.md`);
|
|
51
|
-
try {
|
|
52
|
-
const text = await readFile(issuePath, 'utf8');
|
|
53
|
-
body = parseFrontMatterLenient(text).body;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Fall back to the dispatch-time description; the action still fires, it
|
|
57
|
-
// just won't see notes the agent appended during the run.
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'symphony-pr-body-'));
|
|
61
|
-
const bodyFile = path.join(tmpDir, 'body.md');
|
|
62
|
-
await writeFile(bodyFile, body, 'utf8');
|
|
63
|
-
const cleanup = async () => {
|
|
64
|
-
try {
|
|
65
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// Best-effort cleanup; tmp dir is in $TMPDIR and the OS will reclaim it eventually.
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const title = issue.title.trim();
|
|
72
|
-
// Use the base branch pinned at dispatch time (`base_branch_at_dispatch`), the
|
|
73
|
-
// same value the workspace was cloned/fetched against, so a WORKFLOW.md reload
|
|
74
|
-
// mid-run can't open a PR against a base the agent never rebased onto.
|
|
75
|
-
const env = {
|
|
76
|
-
SYMPHONY_ISSUE_ID: issue.id,
|
|
77
|
-
SYMPHONY_BRANCH: branch,
|
|
78
|
-
SYMPHONY_BASE_BRANCH: entry.base_branch_at_dispatch,
|
|
79
|
-
SYMPHONY_PR_TITLE: title.length > 0 ? `${issue.id}: ${title}` : issue.id,
|
|
80
|
-
SYMPHONY_PR_BODY_FILE: bodyFile,
|
|
81
|
-
};
|
|
82
|
-
return { env, cleanup };
|
|
83
|
-
}
|
|
84
|
-
function buildSteeringReplyPrompt(question, context, reply) {
|
|
85
|
-
const ctxBlock = context && context.length > 0 ? `\n\nContext you provided:\n${context}` : '';
|
|
86
|
-
return [
|
|
87
|
-
'The human operator has responded to your steering request.',
|
|
88
|
-
'',
|
|
89
|
-
'Your question was:',
|
|
90
|
-
question,
|
|
91
|
-
ctxBlock,
|
|
92
|
-
'',
|
|
93
|
-
'The human responded:',
|
|
94
|
-
reply,
|
|
95
|
-
'',
|
|
96
|
-
'Continue work on the issue, taking the human response into account. If the work is fully complete, call symphony.transition to hand off to the next state. If you need to ask another question, call symphony.request_human_steering again.',
|
|
97
|
-
]
|
|
98
|
-
.join('\n')
|
|
99
|
-
.replace(/\n{3,}/g, '\n\n');
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Fixed guest paths for the eval/debug read-only mounts. Hardcoded (not
|
|
103
|
-
* configurable) so the prompt body can reference them by literal path. Kept
|
|
104
|
-
* as exports for tests and any future operator-facing surface that needs to
|
|
105
|
-
* mention them.
|
|
106
|
-
*/
|
|
107
|
-
export const EVAL_MODE_ISSUES_GUEST_PATH = '/symphony/issues';
|
|
108
|
-
export const EVAL_MODE_LOGS_GUEST_PATH = '/symphony/logs';
|
|
109
|
-
/**
|
|
110
|
-
* Resolve effective adapter/model/max_turns for an issue's current state. Per-state
|
|
111
|
-
* overrides declared under `states.<name>` win; otherwise the workflow-level
|
|
112
|
-
* `acp.adapter` / `acp.model` / `agent.max_turns` defaults apply.
|
|
113
|
-
*
|
|
114
|
-
* Throws when `state` is not declared in `cfg.states`. The orchestrator should never
|
|
115
|
-
* dispatch an issue whose state is not declared (validateDispatch + reconciliation both
|
|
116
|
-
* gate on that), but defense in depth: returning a silent fallback here would mask a
|
|
117
|
-
* tracker/workflow drift bug as a confusing default-adapter run.
|
|
118
|
-
*/
|
|
119
|
-
export function resolveDispatchConfig(cfg, state) {
|
|
120
|
-
const states = cfg.states;
|
|
121
|
-
// Case-insensitive lookup matches the rest of symphony (eligibility, reconciliation,
|
|
122
|
-
// local-tracker state directories all compare lowercase). A workflow that declares
|
|
123
|
-
// `Todo` and a tracker file living under `todo/` still resolves correctly.
|
|
124
|
-
let key = null;
|
|
125
|
-
if (Object.prototype.hasOwnProperty.call(states, state)) {
|
|
126
|
-
key = state;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
const lower = state.toLowerCase();
|
|
130
|
-
for (const name of Object.keys(states)) {
|
|
131
|
-
if (name.toLowerCase() === lower) {
|
|
132
|
-
key = name;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (key === null) {
|
|
138
|
-
const declared = Object.keys(states).join(', ');
|
|
139
|
-
throw new Error(`resolveDispatchConfig: state "${state}" is not declared in workflow states (declared: ${declared.length > 0 ? declared : '<none>'})`);
|
|
140
|
-
}
|
|
141
|
-
const s = states[key];
|
|
142
|
-
const adapter = (s.adapter ?? cfg.acp.adapter);
|
|
143
|
-
// Distinguish "not overridden" (undefined) from "explicitly null" (means: use adapter
|
|
144
|
-
// default). Only fall back to workflow-level acp.model when the state did not declare
|
|
145
|
-
// a model key at all; an explicit null in the state config means the operator wants
|
|
146
|
-
// the adapter's own default for this state.
|
|
147
|
-
const model = s.model === undefined ? cfg.acp.model : s.model;
|
|
148
|
-
const effort = s.effort === undefined ? cfg.acp.effort : s.effort;
|
|
149
|
-
const max_turns = s.max_turns ?? cfg.agent.max_turns;
|
|
150
|
-
const eval_mode = s.eval_mode === true;
|
|
151
|
-
return { adapter, model, effort, max_turns, eval_mode };
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Derive the extra read-only bind mounts the eval/debug mode contributes for
|
|
155
|
-
* a single dispatch. Returns an empty list when the state did not opt in or
|
|
156
|
-
* when neither symphony state root is configured (defense in depth — the
|
|
157
|
-
* local tracker always sets `tracker.root` and the loader always sets
|
|
158
|
-
* `logs.root`, but a hand-built ServiceConfig in tests might not).
|
|
159
|
-
*
|
|
160
|
-
* Pure so tests can assert the mount shape without spinning up the runner.
|
|
161
|
-
* The host paths are absolute (the loader normalizes both roots), and the
|
|
162
|
-
* guest paths are the fixed `EVAL_MODE_*` constants so the prompt body can
|
|
163
|
-
* reference them by literal path.
|
|
164
|
-
*/
|
|
165
|
-
export function buildEvalModeMounts(cfg, resolved) {
|
|
166
|
-
if (!resolved.eval_mode)
|
|
167
|
-
return [];
|
|
168
|
-
const mounts = [];
|
|
169
|
-
const trackerRoot = cfg.tracker.root;
|
|
170
|
-
if (trackerRoot && trackerRoot.length > 0) {
|
|
171
|
-
mounts.push({ host: trackerRoot, guest: EVAL_MODE_ISSUES_GUEST_PATH, readonly: true });
|
|
172
|
-
}
|
|
173
|
-
const logsRoot = cfg.logs.root;
|
|
174
|
-
if (logsRoot && logsRoot.length > 0) {
|
|
175
|
-
mounts.push({ host: logsRoot, guest: EVAL_MODE_LOGS_GUEST_PATH, readonly: true });
|
|
176
|
-
}
|
|
177
|
-
return mounts;
|
|
178
|
-
}
|
|
179
|
-
export class AgentRunner {
|
|
180
|
-
cfg;
|
|
181
|
-
workflow;
|
|
182
|
-
workspaces;
|
|
183
|
-
tracker;
|
|
184
|
-
vmClient;
|
|
185
|
-
events;
|
|
186
|
-
mcp;
|
|
187
|
-
acpBridge;
|
|
188
|
-
followupSink;
|
|
189
|
-
actionSnapshotSink;
|
|
190
|
-
credentialRegistry;
|
|
191
|
-
adapterHooks;
|
|
192
|
-
gondolinVmConfig;
|
|
193
|
-
constructor(cfg, workflow, workspaces, tracker,
|
|
194
|
-
/**
|
|
195
|
-
* Gondolin VM-substrate client (the in-process VM backend). Each
|
|
196
|
-
* dispatch creates a per-issue VM through a per-attempt `GondolinDispatcher`
|
|
197
|
-
* built over this client + the shared credential registry.
|
|
198
|
-
*/
|
|
199
|
-
vmClient, events, mcp = null,
|
|
200
|
-
/**
|
|
201
|
-
* Host-side TCP bridge the in-VM agent dials back to for ACP traffic. The
|
|
202
|
-
* exec channel is now just the process tether + stderr tap; required at
|
|
203
|
-
* runtime — runAttempt fails fast if absent.
|
|
204
|
-
*/
|
|
205
|
-
acpBridge = null,
|
|
206
|
-
/**
|
|
207
|
-
* Sink for `propose_followup` actions (issue 36). Wired to the
|
|
208
|
-
* orchestrator's tracker in production; nullable for tests that don't
|
|
209
|
-
* exercise the action. Same shape as the MCP `propose_issue` tool's
|
|
210
|
-
* tracker-side write.
|
|
211
|
-
*/
|
|
212
|
-
followupSink = null,
|
|
213
|
-
/**
|
|
214
|
-
* Sink for per-attempt action ledgers surfaced on Snapshot (issue 36 AC5).
|
|
215
|
-
* Nullable so tests that don't exercise the snapshot surface can pass
|
|
216
|
-
* undefined.
|
|
217
|
-
*/
|
|
218
|
-
actionSnapshotSink = null,
|
|
219
|
-
/**
|
|
220
|
-
* Shared host credential registry (Gondolin secret-substitution model,
|
|
221
|
-
* replaces the credential proxy). Owns every live per-VM `secretManager`;
|
|
222
|
-
* the dispatcher registers a manager per dispatch, the ticker fans fresh
|
|
223
|
-
* tokens out to all of them on rotation. Required — runAttempt fails fast
|
|
224
|
-
* if absent.
|
|
225
|
-
*/
|
|
226
|
-
credentialRegistry = null,
|
|
227
|
-
/**
|
|
228
|
-
* Per-adapter Gondolin hooks config (allowlist + token-shaped placeholder
|
|
229
|
-
* secret + request/response hooks). Built once at composition time from
|
|
230
|
-
* `buildAdapterCredentialSpecs`; the dispatcher passes the selected adapter's
|
|
231
|
-
* entry straight into `createHttpHooks`. Required — runAttempt fails fast
|
|
232
|
-
* if the dispatched adapter is missing.
|
|
233
|
-
*/
|
|
234
|
-
adapterHooks = null,
|
|
235
|
-
/**
|
|
236
|
-
* Static Gondolin VM shape (image ref + cpus/mem). Resolved from config at
|
|
237
|
-
* composition time. Required.
|
|
238
|
-
*/
|
|
239
|
-
gondolinVmConfig = null) {
|
|
240
|
-
this.cfg = cfg;
|
|
241
|
-
this.workflow = workflow;
|
|
242
|
-
this.workspaces = workspaces;
|
|
243
|
-
this.tracker = tracker;
|
|
244
|
-
this.vmClient = vmClient;
|
|
245
|
-
this.events = events;
|
|
246
|
-
this.mcp = mcp;
|
|
247
|
-
this.acpBridge = acpBridge;
|
|
248
|
-
this.followupSink = followupSink;
|
|
249
|
-
this.actionSnapshotSink = actionSnapshotSink;
|
|
250
|
-
this.credentialRegistry = credentialRegistry;
|
|
251
|
-
this.adapterHooks = adapterHooks;
|
|
252
|
-
this.gondolinVmConfig = gondolinVmConfig;
|
|
253
|
-
}
|
|
254
|
-
setAcpBridge(bridge) {
|
|
255
|
-
this.acpBridge = bridge;
|
|
256
|
-
}
|
|
257
|
-
updateConfig(cfg, workflow) {
|
|
258
|
-
this.cfg = cfg;
|
|
259
|
-
this.workflow = workflow;
|
|
260
|
-
}
|
|
261
|
-
setMcpRegistry(mcp) {
|
|
262
|
-
this.mcp = mcp;
|
|
263
|
-
}
|
|
264
|
-
vmNameFor(issue) {
|
|
265
|
-
return `${SYMPHONY_VM_PREFIX}${sanitizeWorkspaceKey(issue.identifier)}`.toLowerCase();
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Resolve the action templating context from the staged `extraEnv` map and
|
|
269
|
-
* the running entry. Pass-through to the pure `deriveActionContext` helper
|
|
270
|
-
* so the env fallback chain stays out of the imperative-shell complexity
|
|
271
|
-
* budget; the shell only adapts `RunningEntry` to the helper's input shape.
|
|
272
|
-
* The repo comes from the entry's dispatch-time pin (`github_repo_at_dispatch`),
|
|
273
|
-
* NOT live config, so a WORKFLOW.md reload mid-run can't retarget `$repo` away
|
|
274
|
-
* from the repo the workspace `origin` was set up against.
|
|
275
|
-
*/
|
|
276
|
-
buildActionContext(entry, workspacePath, extraEnv) {
|
|
277
|
-
return deriveActionContext({
|
|
278
|
-
identifier: entry.identifier,
|
|
279
|
-
workspacePath,
|
|
280
|
-
issueId: entry.issue.id,
|
|
281
|
-
issueTitle: entry.issue.title ?? '',
|
|
282
|
-
issueDescription: entry.issue.description,
|
|
283
|
-
repoEnv: entry.github_repo_at_dispatch ?? undefined,
|
|
284
|
-
extraEnv,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Construct a `RunInVmExecutor` bound to a specific per-issue VM. Each
|
|
289
|
-
* invocation execs a fresh one-shot process against the dispatch's live
|
|
290
|
-
* `VmHandle`, with stdin closed, stdout/stderr drained into the per-issue
|
|
291
|
-
* run log, and a timeout that aborts the exec on overrun. The workspace is
|
|
292
|
-
* VFS-mounted at the same host path inside the VM (the dispatcher declares
|
|
293
|
-
* that mount at bring-up), so `workdir` is identical on both sides — no path
|
|
294
|
-
* translation needed.
|
|
295
|
-
*
|
|
296
|
-
* Wraps the `VmHandle.exec` contract rather than reaching into the VmClient
|
|
297
|
-
* from the actions module so the actions package stays free of
|
|
298
|
-
* `node:child_process`/VM imports; the dependency inversion lets tests pass
|
|
299
|
-
* `hostRunInVm` without touching the VM substrate.
|
|
300
|
-
*/
|
|
301
|
-
buildVmRunInVm(vm, runLog) {
|
|
302
|
-
return ({ name, cmd, env, workdir, timeoutMs, onStdout, onStderr }) => new Promise((resolve) => {
|
|
303
|
-
const stream = vm.exec({
|
|
304
|
-
command: cmd,
|
|
305
|
-
workdir,
|
|
306
|
-
env,
|
|
307
|
-
// The local timer below kills the exec at the deadline; passing
|
|
308
|
-
// timeoutMs to the VM exec lets the substrate enforce the same bound.
|
|
309
|
-
timeoutMs,
|
|
310
|
-
});
|
|
311
|
-
// No stdin: run_in_vm is one-shot exec; the action's `cmd` is the
|
|
312
|
-
// full command line. Closing stdin tells the in-VM process its
|
|
313
|
-
// input stream is at EOF so it never blocks waiting for input.
|
|
314
|
-
try {
|
|
315
|
-
stream.stdin.end();
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
/* idempotent on already-ended stream */
|
|
319
|
-
}
|
|
320
|
-
let stdout = '';
|
|
321
|
-
let stderr = '';
|
|
322
|
-
let timedOut = false;
|
|
323
|
-
const limit = 65_536;
|
|
324
|
-
stream.stdout.setEncoding('utf8');
|
|
325
|
-
stream.stderr.setEncoding('utf8');
|
|
326
|
-
stream.stdout.on('data', (chunk) => {
|
|
327
|
-
stdout += chunk;
|
|
328
|
-
if (stdout.length > limit)
|
|
329
|
-
stdout = stdout.slice(0, limit);
|
|
330
|
-
onStdout?.(chunk);
|
|
331
|
-
runLog?.record({ channel: 'hook', hook: `run_in_vm:${name}`, stream: 'stdout', text: chunk });
|
|
332
|
-
});
|
|
333
|
-
stream.stderr.on('data', (chunk) => {
|
|
334
|
-
stderr += chunk;
|
|
335
|
-
if (stderr.length > limit)
|
|
336
|
-
stderr = stderr.slice(0, limit);
|
|
337
|
-
onStderr?.(chunk);
|
|
338
|
-
runLog?.record({ channel: 'hook', hook: `run_in_vm:${name}`, stream: 'stderr', text: chunk });
|
|
339
|
-
});
|
|
340
|
-
const timer = setTimeout(() => {
|
|
341
|
-
timedOut = true;
|
|
342
|
-
try {
|
|
343
|
-
stream.kill();
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
/* idempotent */
|
|
347
|
-
}
|
|
348
|
-
}, timeoutMs);
|
|
349
|
-
stream.exit
|
|
350
|
-
.then(({ code }) => {
|
|
351
|
-
clearTimeout(timer);
|
|
352
|
-
resolve({
|
|
353
|
-
// Gondolin reports the abort/exit as a numeric signal; the
|
|
354
|
-
// run-log surface only cares about exit_code + timed_out, so the
|
|
355
|
-
// numeric signal is folded into `null` rather than mistyped.
|
|
356
|
-
exit_code: code,
|
|
357
|
-
signal: null,
|
|
358
|
-
timed_out: timedOut,
|
|
359
|
-
stdout,
|
|
360
|
-
stderr,
|
|
361
|
-
});
|
|
362
|
-
})
|
|
363
|
-
.catch((err) => {
|
|
364
|
-
clearTimeout(timer);
|
|
365
|
-
resolve({
|
|
366
|
-
exit_code: null,
|
|
367
|
-
signal: null,
|
|
368
|
-
timed_out: timedOut,
|
|
369
|
-
stdout,
|
|
370
|
-
stderr: stderr + `\n${err.message}`,
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Drive the typed action executor for a state's `actions:` block. Reroutes
|
|
377
|
-
* the issue when an action returns `route_to` (today: `merge` on conflict);
|
|
378
|
-
* the workspace and `agent/<id>` branch are preserved so the operator who
|
|
379
|
-
* picks up the issue in the routed state can resolve it.
|
|
380
|
-
*
|
|
381
|
-
* Returns the underlying `ActionExecResult` so the caller can distinguish
|
|
382
|
-
* "rerouted (treat as success — the agent's work is done; the issue lives
|
|
383
|
-
* on in the conflict state)" from "non-routed failure (the cleanup pass
|
|
384
|
-
* itself failed — the attempt must report `ok: false` so the orchestrator
|
|
385
|
-
* retries or surfaces the error)." A void return here is what let a failed
|
|
386
|
-
* `push_branch` / `create_pr_if_missing` look like a successful attempt
|
|
387
|
-
* in the prior implementation.
|
|
388
|
-
*/
|
|
389
|
-
async runStateActions(stateName, actions, entry, workspacePath, extraEnv, capture, runInVm) {
|
|
390
|
-
const ctx = this.buildActionContext(entry, workspacePath, extraEnv);
|
|
391
|
-
const snapshotId = `actions:${stateName}`;
|
|
392
|
-
const logger = withIssue({ issue_id: entry.issue_id, issue_identifier: entry.identifier });
|
|
393
|
-
logger.info('running state actions', {
|
|
394
|
-
state: stateName,
|
|
395
|
-
action_count: actions.length,
|
|
396
|
-
});
|
|
397
|
-
const result = await runActions(actions, {
|
|
398
|
-
workspacePath,
|
|
399
|
-
ctx,
|
|
400
|
-
capture: capture ?? undefined,
|
|
401
|
-
followupSink: this.followupSink ?? undefined,
|
|
402
|
-
runInVm: runInVm ?? undefined,
|
|
403
|
-
predicateEnv: defaultPredicateEnv,
|
|
404
|
-
snapshotId,
|
|
405
|
-
now: () => Date.now(),
|
|
406
|
-
});
|
|
407
|
-
// Surface on snapshot regardless of outcome; the dashboard shows the
|
|
408
|
-
// full ledger including in-progress / error states.
|
|
409
|
-
this.actionSnapshotSink?.recordActionResult(snapshotId, {
|
|
410
|
-
id: snapshotId,
|
|
411
|
-
ready: result.ok,
|
|
412
|
-
desired_hash: null,
|
|
413
|
-
last_error: result.actions.find((a) => a.state === 'error')?.error ?? null,
|
|
414
|
-
actions: result.actions,
|
|
415
|
-
});
|
|
416
|
-
if (result.route_to) {
|
|
417
|
-
logger.warn('state action requested reroute', {
|
|
418
|
-
state: stateName,
|
|
419
|
-
target_state: result.route_to,
|
|
420
|
-
reason: result.reason,
|
|
421
|
-
});
|
|
422
|
-
await this.rerouteEntryAction(entry, stateName, result.route_to, result.reason);
|
|
423
|
-
}
|
|
424
|
-
else if (!result.ok) {
|
|
425
|
-
logger.warn('state actions failed', {
|
|
426
|
-
state: stateName,
|
|
427
|
-
reason: result.reason,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
return result;
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Move `entry`'s tracker file into `targetState` and append a diagnostic
|
|
434
|
-
* note. Used by `runStateActions` when an action returns a route_to (e.g.
|
|
435
|
-
* `merge`'s on_conflict).
|
|
436
|
-
*/
|
|
437
|
-
async rerouteEntryAction(entry, fromState, targetState, reason) {
|
|
438
|
-
if (!this.tracker.moveIssueToState) {
|
|
439
|
-
entry.cleanup_workspace_on_exit = false;
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const notes = [
|
|
443
|
-
`**Action rerouted** to \`${targetState}\` from \`${fromState}\`.`,
|
|
444
|
-
'',
|
|
445
|
-
`**Reason:** ${reason ?? 'unknown'}`,
|
|
446
|
-
'',
|
|
447
|
-
`**Workspace and \`agent/${entry.identifier}\` branch are preserved** for resolution.`,
|
|
448
|
-
].join('\n');
|
|
449
|
-
try {
|
|
450
|
-
await this.tracker.moveIssueToState(entry.issue_id, targetState, {
|
|
451
|
-
fromRoot: entry.tracker_root_at_dispatch ?? undefined,
|
|
452
|
-
fromState,
|
|
453
|
-
notes,
|
|
454
|
-
actor: entry.resolved_actor,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
catch {
|
|
458
|
-
entry.cleanup_workspace_on_exit = false;
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
entry.cleanup_workspace_on_exit = false;
|
|
462
|
-
entry.issue.state = targetState;
|
|
463
|
-
// Record the conflict reroute so the run-summary reducer can surface it as
|
|
464
|
-
// PR-autopilot rebase churn (issue 123). `rerouted` distinguishes it from a
|
|
465
|
-
// normal handoff so it never counts as a review rejection.
|
|
466
|
-
entry.last_transition = {
|
|
467
|
-
from_state: fromState,
|
|
468
|
-
to_state: targetState,
|
|
469
|
-
notes,
|
|
470
|
-
actor: entry.resolved_actor,
|
|
471
|
-
terminal: false,
|
|
472
|
-
rerouted: true,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Cleanup: run the per-state `actions:` block (push_branch +
|
|
477
|
-
* create_pr_if_missing on Done is the canonical example). Stages the
|
|
478
|
-
* SYMPHONY_* env vars + temp body file the action context consumes via
|
|
479
|
-
* `deriveActionContext` (so $pr_body_file points at the post-transition
|
|
480
|
-
* body), and tears them down via `finally`. Returns the non-routed action
|
|
481
|
-
* failure reason, or null when the cleanup succeeded / rerouted / was
|
|
482
|
-
* skipped (no actions declared or no running entry).
|
|
483
|
-
*
|
|
484
|
-
* The Done-state push + PR-create handoff is a typed `actions:` block
|
|
485
|
-
* (push_branch + create_pr_if_missing) — there is no shell-hook arm here.
|
|
486
|
-
*/
|
|
487
|
-
async runCleanupActions(issue, cleanupState, runningEntry, workspacePath, vm, hookCapture, runLog) {
|
|
488
|
-
const logger = withIssue({ issue_id: issue.id, issue_identifier: issue.identifier });
|
|
489
|
-
const cleanupActions = resolveActionsForState(this.cfg, cleanupState);
|
|
490
|
-
if (!runningEntry || !cleanupActions || cleanupActions.length === 0)
|
|
491
|
-
return null;
|
|
492
|
-
let staged;
|
|
493
|
-
try {
|
|
494
|
-
const built = await stageActionContextEnv(runningEntry);
|
|
495
|
-
staged = { extraEnv: built.env, cleanup: built.cleanup };
|
|
496
|
-
}
|
|
497
|
-
catch (err) {
|
|
498
|
-
logger.warn('action-context env staging failed; running actions without SYMPHONY_PR_* vars', {
|
|
499
|
-
error: err.message,
|
|
500
|
-
});
|
|
501
|
-
staged = { extraEnv: undefined, cleanup: async () => undefined };
|
|
502
|
-
}
|
|
503
|
-
try {
|
|
504
|
-
// run_in_vm goes through the per-issue VM's exec channel. The VM is still
|
|
505
|
-
// alive here — teardown of the dispatch happens after the cleanup actions
|
|
506
|
-
// run. A null handle (the dispatch never came up) skips run_in_vm.
|
|
507
|
-
const runInVm = vm
|
|
508
|
-
? this.buildVmRunInVm(vm, runLog)
|
|
509
|
-
: undefined;
|
|
510
|
-
const actionResult = await this.runStateActions(cleanupState, cleanupActions, runningEntry, workspacePath, staged.extraEnv, hookCapture('actions'), runInVm);
|
|
511
|
-
if (!actionResult.ok && !actionResult.route_to) {
|
|
512
|
-
return actionResult.reason ?? 'unknown';
|
|
513
|
-
}
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
finally {
|
|
517
|
-
await staged.cleanup();
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Per-attempt context assembled once the VM is up and the bridge is registered.
|
|
522
|
-
* Everything `tearDownSession` needs to unwind cleanly lives here so post-VM
|
|
523
|
-
* failure paths share one teardown contract. `acpSocket` is `null` until the
|
|
524
|
-
* in-VM agent dials back; teardown checks for null before destroying.
|
|
525
|
-
*/
|
|
526
|
-
static STDERR_RING_LIMIT = 240;
|
|
527
|
-
static STDERR_LOG_LIMIT = 500;
|
|
528
|
-
static STEERING_PREVIEW_LIMIT = 240;
|
|
529
|
-
async runAttempt(issue, attempt, cancelSignal, runningEntry, runLog) {
|
|
530
|
-
const logger = withIssue({ issue_id: issue.id, issue_identifier: issue.identifier });
|
|
531
|
-
try {
|
|
532
|
-
return await this.runAttemptCore(issue, attempt, cancelSignal, runningEntry, runLog, logger);
|
|
533
|
-
}
|
|
534
|
-
catch (err) {
|
|
535
|
-
if (err instanceof PhaseFailure)
|
|
536
|
-
return err.attemptResult;
|
|
537
|
-
throw err;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Phase pipeline that backs `runAttempt`. Each `unwrap(...)` either yields
|
|
542
|
-
* the phase's success value or throws `PhaseFailure` (caught by `runAttempt`
|
|
543
|
-
* and converted to a `RunAttemptResult`). This keeps the orchestrator under
|
|
544
|
-
* the imperative-shell budget while preserving the strict ordering of the
|
|
545
|
-
* original 535-line method.
|
|
546
|
-
*/
|
|
547
|
-
async runAttemptCore(issue, attempt, cancelSignal, runningEntry, runLog, logger) {
|
|
548
|
-
const resolved = this.unwrap(this.resolveAttemptDispatch(issue, logger));
|
|
549
|
-
const hookCapture = this.makeHookCapture(runLog);
|
|
550
|
-
// Use the repo/base pinned on the entry at dispatch time so workspace setup,
|
|
551
|
-
// the pre-dispatch fetch, and the Done action context all agree even if
|
|
552
|
-
// WORKFLOW.md is reloaded mid-run. Fall back to live config only when no
|
|
553
|
-
// entry was supplied (defensive — production always passes one).
|
|
554
|
-
const setupSnapshot = {
|
|
555
|
-
githubRepo: runningEntry?.github_repo_at_dispatch ?? resolveGithubRepo(this.cfg.workspace.github_repo),
|
|
556
|
-
baseBranch: runningEntry?.base_branch_at_dispatch ?? resolveBaseBranch(this.cfg.workspace.base_branch),
|
|
557
|
-
};
|
|
558
|
-
const ws = this.unwrap(await this.setupWorkspace(issue, setupSnapshot, runLog, logger));
|
|
559
|
-
const adapter = this.unwrap(this.prepareAdapterRuntime(resolved, logger));
|
|
560
|
-
const bridge = this.unwrap(this.validateAcpBridge(logger));
|
|
561
|
-
const vm = this.unwrap(await this.bringUpVmAndExec({
|
|
562
|
-
issue,
|
|
563
|
-
resolved,
|
|
564
|
-
workspacePath: ws.workspace.path,
|
|
565
|
-
adapter,
|
|
566
|
-
runLog,
|
|
567
|
-
logger,
|
|
568
|
-
}));
|
|
569
|
-
const ctx = {
|
|
570
|
-
issue,
|
|
571
|
-
runningEntry,
|
|
572
|
-
workspacePath: ws.workspace.path,
|
|
573
|
-
vm: vm.vm,
|
|
574
|
-
bridgeReg: vm.bridgeReg,
|
|
575
|
-
exec: vm.exec,
|
|
576
|
-
teardownDispatch: vm.teardownDispatch,
|
|
577
|
-
acpSocket: null,
|
|
578
|
-
hookCapture,
|
|
579
|
-
runLog,
|
|
580
|
-
logger,
|
|
581
|
-
};
|
|
582
|
-
// Once dispatch() has succeeded the VM + the per-VM secret registration are
|
|
583
|
-
// live and ONLY this ctx's teardown can release them. `unwrap` throws a
|
|
584
|
-
// `PhaseFailure` straight up the call stack, so a post-dispatch step that
|
|
585
|
-
// returns early/throws BEFORE `tearDownSession` runs would strand the VM +
|
|
586
|
-
// the secret manager. Guard the whole post-dispatch path: on any throw, tear
|
|
587
|
-
// the dispatch down before propagating (idempotent — the dispatcher's
|
|
588
|
-
// teardown() guards re-entry, so the normal success path's tearDownSession is
|
|
589
|
-
// never double-counted).
|
|
590
|
-
return this.runSessionOrTeardown(ctx, {
|
|
591
|
-
resolved,
|
|
592
|
-
cancelSignal,
|
|
593
|
-
attempt,
|
|
594
|
-
acpReachUrl: bridge.acpReachUrl,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Drive the post-dispatch path (bridge connect → session init → turn loop →
|
|
599
|
-
* teardown) with a teardown safety net: any throw before the normal
|
|
600
|
-
* `tearDownSession` runs still releases the Gondolin handle (close VM +
|
|
601
|
-
* deregister the secret manager) so a post-dispatch failure cannot leak the VM.
|
|
602
|
-
* The dispatch teardown is idempotent, so re-running it from the catch after an
|
|
603
|
-
* inner `tearDownSession` already fired is a no-op.
|
|
604
|
-
*/
|
|
605
|
-
async runSessionOrTeardown(ctx, args) {
|
|
606
|
-
try {
|
|
607
|
-
const session = this.unwrap(await this.connectBridgeAndInitSession({
|
|
608
|
-
ctx,
|
|
609
|
-
resolved: args.resolved,
|
|
610
|
-
cancelSignal: args.cancelSignal,
|
|
611
|
-
acpReachUrl: args.acpReachUrl,
|
|
612
|
-
}));
|
|
613
|
-
const activeStates = new Set(activeStateNames(this.cfg.states).map((s) => s.toLowerCase()));
|
|
614
|
-
const loopRes = await this.runTurnLoop({
|
|
615
|
-
ctx,
|
|
616
|
-
client: session.client,
|
|
617
|
-
resolved: args.resolved,
|
|
618
|
-
cancelSignal: args.cancelSignal,
|
|
619
|
-
attempt: args.attempt,
|
|
620
|
-
activeStates,
|
|
621
|
-
});
|
|
622
|
-
clearInterval(session.cancelCheckTimer);
|
|
623
|
-
const tearReason = loopRes.kind === 'mid_failure' ? loopRes.cleanupReason : loopRes.lastReason;
|
|
624
|
-
const nonRouted = await this.tearDownSession(ctx, tearReason);
|
|
625
|
-
return composeAttemptResult({ loopRes, sessionId: session.sessionId, nonRouted });
|
|
626
|
-
}
|
|
627
|
-
catch (err) {
|
|
628
|
-
// A post-dispatch step threw without unwinding the dispatch (e.g. an
|
|
629
|
-
// unexpected throw inside session init that bypassed its own teardown).
|
|
630
|
-
// Release the VM + secret registration before re-propagating.
|
|
631
|
-
await this.teardownDispatch(ctx, 'post_dispatch_failure');
|
|
632
|
-
throw err;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
/** Convert a PhaseResult into either the success value or a thrown PhaseFailure. */
|
|
636
|
-
unwrap(res) {
|
|
637
|
-
if (!res.ok)
|
|
638
|
-
throw new PhaseFailure(res.result);
|
|
639
|
-
return res.value;
|
|
640
|
-
}
|
|
641
|
-
// -------------------------------------------------------------------------
|
|
642
|
-
// Phase 1: dispatch resolution
|
|
643
|
-
// -------------------------------------------------------------------------
|
|
644
|
-
/**
|
|
645
|
-
* Pin adapter/model/max_turns once at attempt start. Every downstream read
|
|
646
|
-
* goes through `resolved`, not the live `this.cfg.acp.*` — that way a
|
|
647
|
-
* workflow reload mid-attempt cannot redirect the adapter or change the
|
|
648
|
-
* budget. Fails fast on unknown adapter (defense in depth — validateDispatch
|
|
649
|
-
* + per-state validation should have caught it earlier).
|
|
650
|
-
*/
|
|
651
|
-
resolveAttemptDispatch(issue, logger) {
|
|
652
|
-
let resolved;
|
|
653
|
-
try {
|
|
654
|
-
resolved = resolveDispatchConfig(this.cfg, issue.state);
|
|
655
|
-
}
|
|
656
|
-
catch (err) {
|
|
657
|
-
logger.error('dispatch resolution failed', {
|
|
658
|
-
error: err.message,
|
|
659
|
-
state: issue.state,
|
|
660
|
-
});
|
|
661
|
-
return failPhase('dispatch resolution error');
|
|
662
|
-
}
|
|
663
|
-
if (!isKnownAdapter(resolved.adapter)) {
|
|
664
|
-
logger.error('unknown acp adapter for state', {
|
|
665
|
-
adapter: resolved.adapter,
|
|
666
|
-
state: issue.state,
|
|
667
|
-
});
|
|
668
|
-
return failPhase('unknown acp adapter');
|
|
669
|
-
}
|
|
670
|
-
return { ok: true, value: resolved };
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Build the per-issue capture closure used by the terminal-state `actions:`
|
|
674
|
-
* block to mirror its stdout/stderr into the run log's `hook` channel
|
|
675
|
-
* (`hook: "actions"`). Returns `undefined` when no run log was provided
|
|
676
|
-
* (production always wires one in; tests may not).
|
|
677
|
-
*/
|
|
678
|
-
makeHookCapture(runLog) {
|
|
679
|
-
return (hook) => runLog
|
|
680
|
-
? {
|
|
681
|
-
onChunk: (stream, text) => runLog.record({ channel: 'hook', hook, stream, text }),
|
|
682
|
-
onResult: (r) => runLog.record({
|
|
683
|
-
channel: 'hook',
|
|
684
|
-
hook,
|
|
685
|
-
kind: 'result',
|
|
686
|
-
exit_code: r.exit_code,
|
|
687
|
-
signal: r.signal,
|
|
688
|
-
timed_out: r.timed_out,
|
|
689
|
-
}),
|
|
690
|
-
}
|
|
691
|
-
: undefined;
|
|
692
|
-
}
|
|
693
|
-
// -------------------------------------------------------------------------
|
|
694
|
-
// Phase 2: workspace setup (ensureFor + base fetch)
|
|
695
|
-
// -------------------------------------------------------------------------
|
|
696
|
-
async setupWorkspace(issue, setup, runLog, logger) {
|
|
697
|
-
let workspace;
|
|
698
|
-
try {
|
|
699
|
-
workspace = await this.workspaces.ensureFor(issue.identifier, setup);
|
|
700
|
-
}
|
|
701
|
-
catch (err) {
|
|
702
|
-
logger.error('workspace error', { error: err.message });
|
|
703
|
-
return failPhase('workspace error');
|
|
704
|
-
}
|
|
705
|
-
const fetchRes = await this.fetchBaseBranch(workspace.path, setup.baseBranch, runLog, logger);
|
|
706
|
-
if (!fetchRes.ok)
|
|
707
|
-
return fetchRes;
|
|
708
|
-
return { ok: true, value: { workspace } };
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Issue 101: a fresh `origin/<base>` is a dispatch precondition. The host
|
|
712
|
-
* fetches it before every dispatch (fresh OR re-dispatch) so the agent's
|
|
713
|
-
* first step — `git rebase origin/<base>` — runs against a current ref.
|
|
714
|
-
* Skipped cleanly in local-only mode (no `origin` configured) — the source
|
|
715
|
-
* repo's local `<base>` is the only truth there. `baseBranch` is the
|
|
716
|
-
* dispatch-time pinned value (NOT live config) so the fetch matches the base
|
|
717
|
-
* the Done action context renders.
|
|
718
|
-
*/
|
|
719
|
-
async fetchBaseBranch(workspacePath, baseBranch, runLog, logger) {
|
|
720
|
-
const fetchResult = await fetchBaseInWorkspace(workspacePath, baseBranch);
|
|
721
|
-
if (!fetchResult.ok) {
|
|
722
|
-
logger.error('pre-dispatch base fetch failed; aborting attempt', {
|
|
723
|
-
base_branch: baseBranch,
|
|
724
|
-
error: fetchResult.diagnostic,
|
|
725
|
-
});
|
|
726
|
-
runLog?.system('pre_dispatch_base_fetch_failed', {
|
|
727
|
-
base_branch: baseBranch,
|
|
728
|
-
error: fetchResult.diagnostic,
|
|
729
|
-
});
|
|
730
|
-
return failPhase('pre-dispatch base fetch failed');
|
|
731
|
-
}
|
|
732
|
-
if (!fetchResult.skipped) {
|
|
733
|
-
runLog?.system('pre_dispatch_base_fetch_ok', { base_branch: baseBranch });
|
|
734
|
-
}
|
|
735
|
-
return { ok: true, value: undefined };
|
|
736
|
-
}
|
|
737
|
-
// -------------------------------------------------------------------------
|
|
738
|
-
// Phase 3: adapter runtime preparation (credentials + injections)
|
|
739
|
-
// -------------------------------------------------------------------------
|
|
740
|
-
/**
|
|
741
|
-
* Apply model/effort runtime injections and seal the adapter runtime the
|
|
742
|
-
* Gondolin dispatcher launches. Pre-handshake failures unwind through the
|
|
743
|
-
* normal phase pipeline.
|
|
744
|
-
*
|
|
745
|
-
* Credentials never enter the VM: the dispatcher stages per-adapter FAKE
|
|
746
|
-
* native creds (placeholders only) and registers a per-VM `secretManager`
|
|
747
|
-
* that substitutes the real token at egress (Gondolin secret-substitution).
|
|
748
|
-
* There is no credential proxy and no base-URL injection — the in-VM client
|
|
749
|
-
* dials its REAL upstream in native mode with the placeholder bearer. So this
|
|
750
|
-
* phase produces only the model/effort knobs (env vars, extra argv, and the
|
|
751
|
-
* non-secret staged files like claude's `settings.json` effortLevel), which
|
|
752
|
-
* ride through the dispatch options.
|
|
753
|
-
*/
|
|
754
|
-
prepareAdapterRuntime(resolved, logger) {
|
|
755
|
-
const profile = ADAPTERS[resolved.adapter];
|
|
756
|
-
const injectedRes = this.applyRuntimeInjectionsOrFail(profile, resolved, logger);
|
|
757
|
-
if (!injectedRes.ok)
|
|
758
|
-
return injectedRes;
|
|
759
|
-
return {
|
|
760
|
-
ok: true,
|
|
761
|
-
value: {
|
|
762
|
-
profile,
|
|
763
|
-
adapterBin: profile.binary[0],
|
|
764
|
-
effectiveAdapterArgs: [...profile.binary.slice(1), ...injectedRes.value.runtimeArgs],
|
|
765
|
-
runtimeEnv: injectedRes.value.runtimeEnv,
|
|
766
|
-
// Model/effort staged files (claude effortLevel settings.json) carry
|
|
767
|
-
// their content in-memory, so they are passed straight to the dispatcher
|
|
768
|
-
// as guest writes — no workspace staging + in-VM `cp` preamble.
|
|
769
|
-
extraGuestFiles: injectedRes.value.runtimeExtraFiles,
|
|
770
|
-
},
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
applyRuntimeInjectionsOrFail(profile, resolved, logger) {
|
|
774
|
-
try {
|
|
775
|
-
const injected = this.applyRuntimeInjections(profile, resolved);
|
|
776
|
-
return { ok: true, value: injected };
|
|
777
|
-
}
|
|
778
|
-
catch (err) {
|
|
779
|
-
logger.error('runtime injection staging failed', { adapter: profile.id, error: err.message });
|
|
780
|
-
return failPhase('runtime injection staging error');
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Compose the model + effort injections through the three orthogonal
|
|
785
|
-
* channels the adapter profile declares: env vars (claude-agent-acp's
|
|
786
|
-
* ANTHROPIC_MODEL), extra argv (codex-acp's `-c model=...`), and staged
|
|
787
|
-
* files (claude-agent-acp's settings.json for `effortLevel`). Pure: the
|
|
788
|
-
* staged-file content is known in-memory and converted directly to guest
|
|
789
|
-
* writes (no FS staging).
|
|
790
|
-
*/
|
|
791
|
-
applyRuntimeInjections(profile, resolved) {
|
|
792
|
-
const acc = { runtimeEnv: {}, runtimeArgs: [], runtimeExtraFiles: [] };
|
|
793
|
-
if (resolved.model) {
|
|
794
|
-
this.applyModelInjection(profile.modelInjection(resolved.model), acc);
|
|
795
|
-
}
|
|
796
|
-
if (resolved.effort && profile.effortInjection) {
|
|
797
|
-
this.applyModelInjection(profile.effortInjection(resolved.effort), acc);
|
|
798
|
-
}
|
|
799
|
-
return acc;
|
|
800
|
-
}
|
|
801
|
-
/** Fold one injection into the accumulator (env / args / guest files). */
|
|
802
|
-
applyModelInjection(inj, acc) {
|
|
803
|
-
if (inj.env) {
|
|
804
|
-
for (const [k, v] of Object.entries(inj.env))
|
|
805
|
-
acc.runtimeEnv[k] = v;
|
|
806
|
-
}
|
|
807
|
-
if (inj.extraArgs)
|
|
808
|
-
acc.runtimeArgs.push(...inj.extraArgs);
|
|
809
|
-
if (inj.stagedFiles) {
|
|
810
|
-
for (const f of inj.stagedFiles) {
|
|
811
|
-
acc.runtimeExtraFiles.push({ guestPath: f.guestPath, content: f.content, mode: 0o600 });
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
// -------------------------------------------------------------------------
|
|
816
|
-
// Phase 4: bridge presence + reach URL
|
|
817
|
-
// -------------------------------------------------------------------------
|
|
818
|
-
/**
|
|
819
|
-
* The TCP bridge is mandatory — without it there is no transport for ACP
|
|
820
|
-
* frames. Returns the reach URL the in-VM agent will dial back to,
|
|
821
|
-
* preferring the explicit `reach_url` override over the host/port derived
|
|
822
|
-
* from the bridge's bound port.
|
|
823
|
-
*/
|
|
824
|
-
validateAcpBridge(logger) {
|
|
825
|
-
if (!this.acpBridge) {
|
|
826
|
-
logger.error('acp bridge is not configured', {});
|
|
827
|
-
return failPhase('acp bridge unavailable');
|
|
828
|
-
}
|
|
829
|
-
const port = this.acpBridge.port() ?? this.cfg.acp.bridge.bind_port;
|
|
830
|
-
const acpReachUrl = this.cfg.acp.bridge.reach_url ?? `tcp://${this.cfg.acp.bridge.reach_host}:${port}`;
|
|
831
|
-
return { ok: true, value: { acpReachUrl } };
|
|
832
|
-
}
|
|
833
|
-
// -------------------------------------------------------------------------
|
|
834
|
-
// Phase 5: VM bring-up + bridge register + exec stream
|
|
835
|
-
// -------------------------------------------------------------------------
|
|
836
|
-
/**
|
|
837
|
-
* Bring up the per-issue VM, register with the ACP bridge, and start the
|
|
838
|
-
* in-VM proxy via Gondolin exec. VM start happens BEFORE bridge register so a
|
|
839
|
-
* `register()` synchronous throw cannot leave us with a half-staged
|
|
840
|
-
* registration whose `accepted` promise has no `.catch` attached yet
|
|
841
|
-
* (Node ≥ 15 crashes on unhandled rejections).
|
|
842
|
-
*/
|
|
843
|
-
async bringUpVmAndExec(args) {
|
|
844
|
-
const dispatcherRes = this.buildDispatcherOrFail(args.resolved, args.logger);
|
|
845
|
-
if (!dispatcherRes.ok)
|
|
846
|
-
return dispatcherRes;
|
|
847
|
-
const bridgePort = this.acpBridge.port() ?? this.cfg.acp.bridge.bind_port;
|
|
848
|
-
const bridgeHost = this.cfg.acp.bridge.reach_host;
|
|
849
|
-
// Register the bridge FIRST so the dispatch launch env carries the bearer.
|
|
850
|
-
// `register()` attaches its own internal `.catch`, so a cancel before the
|
|
851
|
-
// caller's `await accepted` cannot escalate to an unhandled rejection.
|
|
852
|
-
const regRes = this.registerBridgeOrFail(args.issue, args.logger);
|
|
853
|
-
if (!regRes.ok)
|
|
854
|
-
return regRes;
|
|
855
|
-
const bridgeReg = regRes.value.bridgeReg;
|
|
856
|
-
try {
|
|
857
|
-
const handle = await dispatcherRes.value.dispatch(this.buildDispatchOptions({ ...args, bridgeHost, bridgePort, bridgeReg }));
|
|
858
|
-
return {
|
|
859
|
-
ok: true,
|
|
860
|
-
value: { vm: handle.vm, exec: handle.exec, bridgeReg, teardownDispatch: handle.teardown },
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
catch (err) {
|
|
864
|
-
// The dispatch failed mid-bring-up. Cancel the bridge registration (the VM,
|
|
865
|
-
// if it came up, is torn down by the dispatcher's own error path); the
|
|
866
|
-
// reconciler GC converges any session that leaked past close().
|
|
867
|
-
bridgeReg.cancel('gondolin_dispatch_failed');
|
|
868
|
-
args.logger.error('gondolin dispatch failed', { error: err.message });
|
|
869
|
-
return failPhase('gondolin dispatch error');
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* Build the per-dispatch `GondolinDispatcher` for the resolved adapter. The
|
|
874
|
-
* VM client, credential registry, and Gondolin VM shape are injected once at
|
|
875
|
-
* composition time; the per-adapter hooks config (allowlist + placeholder
|
|
876
|
-
* secret + request/response hooks) selects the credential routing. Fails fast
|
|
877
|
-
* if any collaborator is unwired (the composition root guarantees them).
|
|
878
|
-
*/
|
|
879
|
-
buildDispatcherOrFail(resolved, logger) {
|
|
880
|
-
if (!this.credentialRegistry || !this.adapterHooks || !this.gondolinVmConfig) {
|
|
881
|
-
logger.error('gondolin dispatch collaborators are not wired', {
|
|
882
|
-
registry: this.credentialRegistry !== null,
|
|
883
|
-
hooks: this.adapterHooks !== null,
|
|
884
|
-
vmConfig: this.gondolinVmConfig !== null,
|
|
885
|
-
});
|
|
886
|
-
return failPhase('gondolin dispatch unavailable');
|
|
887
|
-
}
|
|
888
|
-
const hooks = this.adapterHooks[resolved.adapter];
|
|
889
|
-
if (!hooks) {
|
|
890
|
-
logger.error('no gondolin hooks config for adapter', { adapter: resolved.adapter });
|
|
891
|
-
return failPhase('gondolin adapter hooks unavailable');
|
|
892
|
-
}
|
|
893
|
-
return {
|
|
894
|
-
ok: true,
|
|
895
|
-
value: new GondolinDispatcher(this.vmClient, this.credentialRegistry, hooks, this.gondolinVmConfig),
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Assemble the `GondolinDispatchOptions` for a dispatch. Mounts (workspace RW
|
|
900
|
-
* + configured volumes + eval-mode RO) are validated by the Phase 3 guard
|
|
901
|
-
* inside the dispatcher; the forwarded boot env is stripped of all credential
|
|
902
|
-
* vars there too. The adapter bin/args/runtime-env + the non-secret runtime
|
|
903
|
-
* files (effort settings.json) ride through; the bridge host/port + bearer
|
|
904
|
-
* become the dispatch's ACP wiring.
|
|
905
|
-
*/
|
|
906
|
-
buildDispatchOptions(args) {
|
|
907
|
-
return {
|
|
908
|
-
identifier: sanitizeWorkspaceKey(args.issue.identifier).toLowerCase(),
|
|
909
|
-
mounts: this.buildVmMounts(args.workspacePath, args.resolved),
|
|
910
|
-
env: this.buildForwardedEnv(),
|
|
911
|
-
workdir: args.workspacePath,
|
|
912
|
-
bridgeHost: args.bridgeHost,
|
|
913
|
-
bridgePort: args.bridgePort,
|
|
914
|
-
mcp: this.mcpDispatchTarget(),
|
|
915
|
-
acpToken: args.bridgeReg.token,
|
|
916
|
-
adapterBin: args.adapter.adapterBin,
|
|
917
|
-
adapterArgs: args.adapter.effectiveAdapterArgs,
|
|
918
|
-
runtimeEnv: args.adapter.runtimeEnv,
|
|
919
|
-
extraGuestFiles: args.adapter.extraGuestFiles,
|
|
920
|
-
opencodeModel: args.resolved.model,
|
|
921
|
-
onStderr: (chunk) => this.onAgentStderr(chunk, args.issue, args.runLog, args.logger),
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* The host MCP endpoint to tunnel into the guest via `tcp.hosts`, or undefined
|
|
926
|
-
* when MCP is disabled, the HTTP server hasn't bound a port, or an
|
|
927
|
-
* `explicit_host_url` already points the guest at a directly-reachable URL.
|
|
928
|
-
* MUST mirror `setupMcpForAttempt`'s synthetic-vs-explicit decision so the guest
|
|
929
|
-
* MCP URL and the tunnel that backs it stay consistent — under Gondolin the guest
|
|
930
|
-
* cannot reach the host loopback directly, so a missing tunnel means the agent
|
|
931
|
-
* can never reach `symphony.transition`.
|
|
932
|
-
*/
|
|
933
|
-
mcpDispatchTarget() {
|
|
934
|
-
if (!this.cfg.mcp.enabled || this.cfg.mcp.explicit_host_url || !this.mcp)
|
|
935
|
-
return undefined;
|
|
936
|
-
const port = this.mcp.getEffectivePort();
|
|
937
|
-
return port === null ? undefined : { host: this.cfg.mcp.host, port };
|
|
938
|
-
}
|
|
939
|
-
buildVmMounts(workspacePath, resolved) {
|
|
940
|
-
const mounts = [
|
|
941
|
-
{ host: workspacePath, guest: workspacePath, readonly: false },
|
|
942
|
-
];
|
|
943
|
-
for (const v of this.cfg.gondolin.volumes) {
|
|
944
|
-
mounts.push({ host: v.host, guest: v.guest, readonly: v.readonly });
|
|
945
|
-
}
|
|
946
|
-
for (const m of buildEvalModeMounts(this.cfg, resolved)) {
|
|
947
|
-
mounts.push(m);
|
|
948
|
-
}
|
|
949
|
-
return mounts;
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Forward the configured `gondolin.forward_env` vars into the VM boot env, then
|
|
953
|
-
* STRIP every credential-bearing var via `stripCredentialEnv` (defense in depth).
|
|
954
|
-
* `forward_env` can name a real cred var (e.g. `OPENAI_API_KEY`), so this strip
|
|
955
|
-
* makes "no real token reaches the guest boot env" obvious AT THE SOURCE rather
|
|
956
|
-
* than only inside the dispatcher. The dispatcher's `buildCreateVmOptions`
|
|
957
|
-
* re-applies the SAME `stripCredentialEnv` as the enforcement chokepoint — the
|
|
958
|
-
* double-strip is idempotent (`stripCredentialEnv` is a pure filter), so the two
|
|
959
|
-
* layers compose without surprise. The guest holds only the placeholder bearer
|
|
960
|
-
* Gondolin substitutes at egress (the host-only-refresh invariant). No per-adapter
|
|
961
|
-
* omit is needed here (the strip is uniform + strictly stronger).
|
|
962
|
-
*/
|
|
963
|
-
buildForwardedEnv() {
|
|
964
|
-
return stripCredentialEnv(computeForwardedEnv(this.cfg.gondolin.forward_env, undefined, (k) => process.env[k]));
|
|
965
|
-
}
|
|
966
|
-
registerBridgeOrFail(issue, logger) {
|
|
967
|
-
try {
|
|
968
|
-
const bridgeReg = this.acpBridge.register(issue.id, issue.identifier);
|
|
969
|
-
return { ok: true, value: { bridgeReg } };
|
|
970
|
-
}
|
|
971
|
-
catch (err) {
|
|
972
|
-
logger.error('acp bridge register failed', { error: err.message });
|
|
973
|
-
return failPhase('acp bridge register failed');
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* The diagnostic-stderr sink the dispatcher pipes the in-VM agent's stderr to.
|
|
978
|
-
* Mirrors the old `attachStderrTap`: records to the run log, surfaces a
|
|
979
|
-
* truncated `agent_stderr` runtime event, and logs at info. Wired before the
|
|
980
|
-
* bridge handshake (the dispatcher taps stderr at launch) so a pre-connect
|
|
981
|
-
* crash still surfaces.
|
|
982
|
-
*/
|
|
983
|
-
onAgentStderr(chunk, issue, runLog, logger) {
|
|
984
|
-
runLog?.record({ channel: 'stderr', text: chunk });
|
|
985
|
-
const text = chunk.trim();
|
|
986
|
-
if (text.length === 0)
|
|
987
|
-
return;
|
|
988
|
-
const truncated = text.length > AgentRunner.STDERR_RING_LIMIT
|
|
989
|
-
? text.slice(0, AgentRunner.STDERR_RING_LIMIT) + '…'
|
|
990
|
-
: text;
|
|
991
|
-
this.events.onRuntimeEvent(issue.id, {
|
|
992
|
-
at: new Date().toISOString(),
|
|
993
|
-
event: 'agent_stderr',
|
|
994
|
-
message: truncated,
|
|
995
|
-
});
|
|
996
|
-
logger.info('agent stderr', { text: text.slice(0, AgentRunner.STDERR_LOG_LIMIT) });
|
|
997
|
-
}
|
|
998
|
-
// -------------------------------------------------------------------------
|
|
999
|
-
// Phase 6: bridge connect + MCP setup + AcpClient + initSession
|
|
1000
|
-
// -------------------------------------------------------------------------
|
|
1001
|
-
async connectBridgeAndInitSession(args) {
|
|
1002
|
-
const connRes = await this.waitForBridgeAccept(args.ctx, args.acpReachUrl);
|
|
1003
|
-
if (!connRes.ok) {
|
|
1004
|
-
await this.tearDownSession(args.ctx, 'acp_bridge_connect_failed');
|
|
1005
|
-
return connRes;
|
|
1006
|
-
}
|
|
1007
|
-
args.ctx.acpSocket = connRes.value.acpSocket;
|
|
1008
|
-
const clientRef = { current: null };
|
|
1009
|
-
const cancelCheckTimer = this.startCancelTimer(args.ctx, args.cancelSignal, clientRef);
|
|
1010
|
-
const mcpRes = this.setupMcpForAttempt(args.ctx);
|
|
1011
|
-
if (!mcpRes.ok) {
|
|
1012
|
-
await this.cancelAndTearDown(args.ctx, cancelCheckTimer, mcpRes.cleanupReason);
|
|
1013
|
-
return { ok: false, result: mcpRes.result };
|
|
1014
|
-
}
|
|
1015
|
-
const client = this.buildAcpClient(args.ctx, mcpRes.value.mcpServers);
|
|
1016
|
-
clientRef.current = client;
|
|
1017
|
-
const sessRes = await this.initAcpSession(args.ctx, client, args.resolved);
|
|
1018
|
-
if (!sessRes.ok) {
|
|
1019
|
-
await this.cancelAndTearDown(args.ctx, cancelCheckTimer, 'init_failed');
|
|
1020
|
-
return sessRes;
|
|
1021
|
-
}
|
|
1022
|
-
this.emitSessionStarted(args.ctx, sessRes.value.sessionId);
|
|
1023
|
-
return { ok: true, value: { client, sessionId: sessRes.value.sessionId, cancelCheckTimer } };
|
|
1024
|
-
}
|
|
1025
|
-
async cancelAndTearDown(ctx, timer, reason) {
|
|
1026
|
-
clearInterval(timer);
|
|
1027
|
-
await this.tearDownSession(ctx, reason);
|
|
1028
|
-
}
|
|
1029
|
-
buildAcpClient(ctx, mcpServers) {
|
|
1030
|
-
return new AcpClient({
|
|
1031
|
-
stdin: ctx.acpSocket,
|
|
1032
|
-
stdout: ctx.acpSocket,
|
|
1033
|
-
stderr: ctx.exec.stderr,
|
|
1034
|
-
cwd: ctx.workspacePath,
|
|
1035
|
-
readTimeoutMs: this.cfg.acp.read_timeout_ms,
|
|
1036
|
-
promptTimeoutMs: this.cfg.acp.prompt_timeout_ms,
|
|
1037
|
-
onEvent: (event) => this.events.onRuntimeEvent(ctx.issue.id, event),
|
|
1038
|
-
onTokenUsage: (u) => this.events.onTokenUsage(ctx.issue.id, u),
|
|
1039
|
-
mcpServers,
|
|
1040
|
-
runLog: ctx.runLog,
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
emitSessionStarted(ctx, sessionId) {
|
|
1044
|
-
this.events.onSessionStarted?.({
|
|
1045
|
-
issueId: ctx.issue.id,
|
|
1046
|
-
sessionId,
|
|
1047
|
-
threadId: sessionId,
|
|
1048
|
-
pid: ctx.exec.pid ? String(ctx.exec.pid) : null,
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Race the bridge handshake against a configured connect timeout. A stuck
|
|
1053
|
-
* VM or misconfigured `reach_host` would otherwise hang the attempt until
|
|
1054
|
-
* the orchestrator's stall timer fires much later.
|
|
1055
|
-
*/
|
|
1056
|
-
async waitForBridgeAccept(ctx, acpReachUrl) {
|
|
1057
|
-
try {
|
|
1058
|
-
const acpSocket = await Promise.race([
|
|
1059
|
-
ctx.bridgeReg.accepted,
|
|
1060
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('acp bridge: in-VM agent did not connect in time')), this.cfg.acp.bridge.connect_timeout_ms)),
|
|
1061
|
-
]);
|
|
1062
|
-
ctx.runLog?.system('acp_bridge_connected', { reach_url: acpReachUrl });
|
|
1063
|
-
return { ok: true, value: { acpSocket } };
|
|
1064
|
-
}
|
|
1065
|
-
catch (err) {
|
|
1066
|
-
ctx.logger.error('acp bridge connect timeout', { error: err.message });
|
|
1067
|
-
ctx.runLog?.system('acp_bridge_failed', { error: err.message });
|
|
1068
|
-
return failPhase('acp bridge connect failed');
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Start the periodic cancel check. The polite path is `client.cancel()`
|
|
1073
|
-
* (session/cancel over ACP); the belt-and-braces path is `forceClose()` to
|
|
1074
|
-
* unwind a stuck `runPrompt()` plus `execStream.kill()` and socket destroy
|
|
1075
|
-
* to break the transport. `clientRef.current` may briefly be null between
|
|
1076
|
-
* timer start and AcpClient construction; the `?.` keeps that race safe.
|
|
1077
|
-
*/
|
|
1078
|
-
startCancelTimer(ctx, cancelSignal, clientRef) {
|
|
1079
|
-
const onCancel = () => {
|
|
1080
|
-
if (!cancelSignal.cancelled)
|
|
1081
|
-
return;
|
|
1082
|
-
const c = clientRef.current;
|
|
1083
|
-
c?.cancel().catch(() => undefined);
|
|
1084
|
-
c?.forceClose('cancel_requested');
|
|
1085
|
-
try {
|
|
1086
|
-
ctx.exec.kill();
|
|
1087
|
-
}
|
|
1088
|
-
catch { /* idempotent */ }
|
|
1089
|
-
if (ctx.acpSocket && !ctx.acpSocket.destroyed) {
|
|
1090
|
-
try {
|
|
1091
|
-
ctx.acpSocket.destroy();
|
|
1092
|
-
}
|
|
1093
|
-
catch { /* idempotent */ }
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1096
|
-
return setInterval(onCancel, 500);
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Wire the MCP registry servers list for AcpClient. MCP is mandatory for
|
|
1100
|
-
* symphony operations (`transition`, `request_human_steering`); fail fast
|
|
1101
|
-
* if the registry or the reachable URL is missing.
|
|
1102
|
-
*/
|
|
1103
|
-
setupMcpForAttempt(ctx) {
|
|
1104
|
-
const mcpServers = [];
|
|
1105
|
-
if (!this.cfg.mcp.enabled || !ctx.runningEntry) {
|
|
1106
|
-
return { ok: true, value: { mcpServers } };
|
|
1107
|
-
}
|
|
1108
|
-
if (!this.mcp) {
|
|
1109
|
-
ctx.logger.error('mcp is required but no registry is wired into the runner', {});
|
|
1110
|
-
return {
|
|
1111
|
-
ok: false,
|
|
1112
|
-
result: { ok: false, reason: 'mcp required but registry unavailable', threadId: null, turnsCompleted: 0 },
|
|
1113
|
-
cleanupReason: 'mcp_registry_unavailable',
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
// Under Gondolin the guest can't reach the host loopback directly, so when no
|
|
1117
|
-
// `explicit_host_url` is configured the agent dials the fixed synthetic MCP host
|
|
1118
|
-
// (`MCP_GUEST_BASE_URL`) that `mcpDispatchTarget()` tunnelled via `tcp.hosts`.
|
|
1119
|
-
// The condition MUST match `mcpDispatchTarget()` exactly (enabled, no explicit
|
|
1120
|
-
// URL, port bound) so the advertised URL is always backed by a live tunnel.
|
|
1121
|
-
const useSyntheticGuestHost = !this.cfg.mcp.explicit_host_url && this.mcp.getEffectivePort() !== null;
|
|
1122
|
-
const url = this.mcp.buildUrl(ctx.runningEntry.identifier, { host: this.cfg.mcp.host, explicit_host_url: this.cfg.mcp.explicit_host_url }, useSyntheticGuestHost ? MCP_GUEST_BASE_URL : undefined);
|
|
1123
|
-
if (!url) {
|
|
1124
|
-
ctx.logger.error('mcp is required but no reachable URL is configured', {
|
|
1125
|
-
host: this.cfg.mcp.host,
|
|
1126
|
-
explicit_host_url: this.cfg.mcp.explicit_host_url,
|
|
1127
|
-
});
|
|
1128
|
-
return {
|
|
1129
|
-
ok: false,
|
|
1130
|
-
result: {
|
|
1131
|
-
ok: false,
|
|
1132
|
-
reason: 'mcp required but URL unavailable (start the HTTP server or set mcp.host_url)',
|
|
1133
|
-
threadId: null,
|
|
1134
|
-
turnsCompleted: 0,
|
|
1135
|
-
},
|
|
1136
|
-
cleanupReason: 'mcp_url_unavailable',
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
const token = this.mcp.activate(ctx.runningEntry);
|
|
1140
|
-
mcpServers.push({
|
|
1141
|
-
type: 'http',
|
|
1142
|
-
name: 'symphony',
|
|
1143
|
-
url,
|
|
1144
|
-
headers: [{ name: 'Authorization', value: `Bearer ${token}` }],
|
|
1145
|
-
});
|
|
1146
|
-
ctx.logger.debug('mcp registered', { url });
|
|
1147
|
-
return { ok: true, value: { mcpServers } };
|
|
1148
|
-
}
|
|
1149
|
-
async initAcpSession(ctx, client, resolved) {
|
|
1150
|
-
try {
|
|
1151
|
-
const sess = await client.initSession();
|
|
1152
|
-
return { ok: true, value: { sessionId: sess.sessionId } };
|
|
1153
|
-
}
|
|
1154
|
-
catch (err) {
|
|
1155
|
-
ctx.logger.error('acp init failed', {
|
|
1156
|
-
error: err.message,
|
|
1157
|
-
adapter: resolved.adapter,
|
|
1158
|
-
});
|
|
1159
|
-
this.events.onRuntimeEvent(ctx.issue.id, {
|
|
1160
|
-
at: new Date().toISOString(),
|
|
1161
|
-
event: 'startup_failed',
|
|
1162
|
-
message: err.message,
|
|
1163
|
-
});
|
|
1164
|
-
return failPhase('agent session startup error');
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
// -------------------------------------------------------------------------
|
|
1168
|
-
// Phase 7: autonomous turn loop
|
|
1169
|
-
// -------------------------------------------------------------------------
|
|
1170
|
-
/**
|
|
1171
|
-
* Drive the ACP loop. Runs as long as the agent keeps engaging — only
|
|
1172
|
-
* autonomous turns count against max_turns; steering-reply turns are free
|
|
1173
|
-
* because the human is in the loop.
|
|
1174
|
-
*/
|
|
1175
|
-
async runTurnLoop(args) {
|
|
1176
|
-
const state = {
|
|
1177
|
-
turnsCompleted: 0,
|
|
1178
|
-
autonomousTurns: 0,
|
|
1179
|
-
lastReason: 'unknown',
|
|
1180
|
-
agentFailure: null,
|
|
1181
|
-
currentIssue: args.ctx.issue,
|
|
1182
|
-
pendingSteering: null,
|
|
1183
|
-
firstTurn: true,
|
|
1184
|
-
};
|
|
1185
|
-
while (true) {
|
|
1186
|
-
const iter = await this.runTurnIteration({ ...args, state });
|
|
1187
|
-
if (iter.kind === 'mid_failure') {
|
|
1188
|
-
return { kind: 'mid_failure', publicReason: iter.publicReason, cleanupReason: iter.cleanupReason, turnsCompleted: state.turnsCompleted };
|
|
1189
|
-
}
|
|
1190
|
-
if (iter.kind === 'break')
|
|
1191
|
-
break;
|
|
1192
|
-
}
|
|
1193
|
-
return { kind: 'done', lastReason: state.lastReason, agentFailure: state.agentFailure, turnsCompleted: state.turnsCompleted };
|
|
1194
|
-
}
|
|
1195
|
-
async runTurnIteration(args) {
|
|
1196
|
-
const { ctx, client, resolved, cancelSignal, attempt, activeStates, state } = args;
|
|
1197
|
-
if (cancelSignal.cancelled) {
|
|
1198
|
-
state.lastReason = 'cancelled_by_reconciliation';
|
|
1199
|
-
return { kind: 'break' };
|
|
1200
|
-
}
|
|
1201
|
-
const promptRes = await this.prepareTurnPrompt(state, attempt, ctx.logger);
|
|
1202
|
-
if (promptRes.kind === 'mid_failure')
|
|
1203
|
-
return promptRes;
|
|
1204
|
-
const isSteeringReply = state.pendingSteering !== null;
|
|
1205
|
-
state.pendingSteering = null;
|
|
1206
|
-
state.firstTurn = false;
|
|
1207
|
-
this.events.onTurn(ctx.issue.id, state.turnsCompleted + 1);
|
|
1208
|
-
const outcome = await client.runPrompt(promptRes.prompt);
|
|
1209
|
-
const turnRes = this.applyTurnOutcome(state, outcome, ctx.runningEntry, isSteeringReply);
|
|
1210
|
-
if (turnRes.kind === 'break')
|
|
1211
|
-
return turnRes;
|
|
1212
|
-
return await this.handlePostTurnFlow({ ctx, cancelSignal, activeStates, resolved, state });
|
|
1213
|
-
}
|
|
1214
|
-
async prepareTurnPrompt(state, attempt, logger) {
|
|
1215
|
-
try {
|
|
1216
|
-
const prompt = await this.composeTurnPrompt(state, attempt);
|
|
1217
|
-
return { kind: 'ok', prompt };
|
|
1218
|
-
}
|
|
1219
|
-
catch (err) {
|
|
1220
|
-
logger.error('prompt rendering failed', { error: err.message });
|
|
1221
|
-
return { kind: 'mid_failure', publicReason: 'prompt error', cleanupReason: 'prompt_error' };
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* Render the prompt for the next iteration via the pure `selectPromptKind`
|
|
1226
|
-
* helper. Steering replies trump everything (the human is in the loop);
|
|
1227
|
-
* the first turn gets the full template; later autonomous turns get the
|
|
1228
|
-
* bare continuation prompt.
|
|
1229
|
-
*/
|
|
1230
|
-
async composeTurnPrompt(state, attempt) {
|
|
1231
|
-
const kind = selectPromptKind({
|
|
1232
|
-
pendingSteering: state.pendingSteering !== null,
|
|
1233
|
-
firstTurn: state.firstTurn,
|
|
1234
|
-
});
|
|
1235
|
-
if (kind === 'steering') {
|
|
1236
|
-
const ps = state.pendingSteering;
|
|
1237
|
-
return buildSteeringReplyPrompt(ps.question, ps.context, ps.reply);
|
|
1238
|
-
}
|
|
1239
|
-
if (kind === 'initial') {
|
|
1240
|
-
return renderPrompt({
|
|
1241
|
-
template: this.workflow.prompt_template,
|
|
1242
|
-
issue: state.currentIssue,
|
|
1243
|
-
attempt,
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
return continuationPrompt(this.cfg.mcp.enabled);
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* Classify the runPrompt outcome (delegated to pure `classifyTurnOutcome`)
|
|
1250
|
-
* and update the turn counters. Returns the next loop control: `break`
|
|
1251
|
-
* collapses agent failure / agent_transitioned into a single break signal
|
|
1252
|
-
* with state already populated; `continue` falls through to post-turn flow.
|
|
1253
|
-
*/
|
|
1254
|
-
applyTurnOutcome(state, outcome, runningEntry, isSteeringReply) {
|
|
1255
|
-
const cls = classifyTurnOutcome({
|
|
1256
|
-
outcomeReason: outcome.reason,
|
|
1257
|
-
outcomeMessage: outcome.message,
|
|
1258
|
-
transitioned: runningEntry?.transitioned === true,
|
|
1259
|
-
});
|
|
1260
|
-
if (cls.kind === 'agent_failure') {
|
|
1261
|
-
state.agentFailure = cls.agentFailure;
|
|
1262
|
-
state.lastReason = cls.reason;
|
|
1263
|
-
return { kind: 'break' };
|
|
1264
|
-
}
|
|
1265
|
-
if (cls.kind === 'agent_transitioned') {
|
|
1266
|
-
state.lastReason = 'agent_transitioned';
|
|
1267
|
-
return { kind: 'break' };
|
|
1268
|
-
}
|
|
1269
|
-
state.turnsCompleted++;
|
|
1270
|
-
if (!isSteeringReply)
|
|
1271
|
-
state.autonomousTurns++;
|
|
1272
|
-
return { kind: 'continue' };
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Post-turn flow: tool-driven exit > steering pause > tracker refresh +
|
|
1276
|
-
* continuation decision. The tracker-refresh branch is in its own helper
|
|
1277
|
-
* so the orchestrator stays under the shell complexity / statement budget;
|
|
1278
|
-
* the pure `decideTurnContinuation` and `handleSteeringRequest` carry the
|
|
1279
|
-
* decision-heavy work.
|
|
1280
|
-
*/
|
|
1281
|
-
async handlePostTurnFlow(args) {
|
|
1282
|
-
const { ctx, cancelSignal, activeStates, resolved, state } = args;
|
|
1283
|
-
if (ctx.runningEntry?.transitioned) {
|
|
1284
|
-
state.lastReason = 'agent_transitioned';
|
|
1285
|
-
return { kind: 'break' };
|
|
1286
|
-
}
|
|
1287
|
-
if (ctx.runningEntry?.steering_requested && this.mcp) {
|
|
1288
|
-
return await this.handleSteeringBranch(ctx, ctx.runningEntry, cancelSignal, state);
|
|
1289
|
-
}
|
|
1290
|
-
return await this.refreshAndDecideContinuation({ ctx, activeStates, resolved, state });
|
|
1291
|
-
}
|
|
1292
|
-
async handleSteeringBranch(ctx, entry, cancelSignal, state) {
|
|
1293
|
-
const steer = await this.handleSteeringRequest(ctx, entry, cancelSignal);
|
|
1294
|
-
if (steer.kind === 'cancelled') {
|
|
1295
|
-
state.lastReason = 'cancelled_while_awaiting_steering';
|
|
1296
|
-
return { kind: 'break' };
|
|
1297
|
-
}
|
|
1298
|
-
state.pendingSteering = steer.pendingSteering;
|
|
1299
|
-
return { kind: 'continue' };
|
|
1300
|
-
}
|
|
1301
|
-
async refreshAndDecideContinuation(args) {
|
|
1302
|
-
const { ctx, activeStates, resolved, state } = args;
|
|
1303
|
-
let refreshed;
|
|
1304
|
-
try {
|
|
1305
|
-
refreshed = await this.tracker.fetchIssueStatesByIds([ctx.issue.id]);
|
|
1306
|
-
}
|
|
1307
|
-
catch (err) {
|
|
1308
|
-
ctx.logger.error('issue state refresh failed', { error: err.message });
|
|
1309
|
-
return { kind: 'mid_failure', publicReason: 'issue state refresh error', cleanupReason: 'issue_state_refresh_failed' };
|
|
1310
|
-
}
|
|
1311
|
-
const found = refreshed[0] ?? null;
|
|
1312
|
-
const cont = decideTurnContinuation({
|
|
1313
|
-
refreshedIssue: found,
|
|
1314
|
-
activeStates,
|
|
1315
|
-
autonomousTurns: state.autonomousTurns,
|
|
1316
|
-
maxTurns: resolved.max_turns,
|
|
1317
|
-
});
|
|
1318
|
-
if (cont.kind === 'break') {
|
|
1319
|
-
if (found)
|
|
1320
|
-
state.currentIssue = found;
|
|
1321
|
-
state.lastReason = cont.reason;
|
|
1322
|
-
return { kind: 'break' };
|
|
1323
|
-
}
|
|
1324
|
-
state.currentIssue = found;
|
|
1325
|
-
await delay(25);
|
|
1326
|
-
return { kind: 'continue' };
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* Park the autonomous loop on a pending steering request. The wait does
|
|
1330
|
-
* not count against max_turns; cancellation breaks via the registry's
|
|
1331
|
-
* cancel-aware resolver (which resolves null when `cancelSignal.cancelled`
|
|
1332
|
-
* flips).
|
|
1333
|
-
*/
|
|
1334
|
-
async handleSteeringRequest(ctx, entry, cancelSignal) {
|
|
1335
|
-
const question = entry.steering_question ?? '';
|
|
1336
|
-
const context = entry.steering_context;
|
|
1337
|
-
const limit = AgentRunner.STEERING_PREVIEW_LIMIT;
|
|
1338
|
-
this.events.onRuntimeEvent(ctx.issue.id, {
|
|
1339
|
-
at: new Date().toISOString(),
|
|
1340
|
-
event: 'awaiting_human_steering',
|
|
1341
|
-
message: question.length > limit ? question.slice(0, limit) + '…' : question,
|
|
1342
|
-
});
|
|
1343
|
-
const reply = await this.mcp.awaitSteeringReply(ctx.issue.identifier, cancelSignal);
|
|
1344
|
-
if (reply === null)
|
|
1345
|
-
return { kind: 'cancelled' };
|
|
1346
|
-
entry.steering_requested = false;
|
|
1347
|
-
entry.steering_question = null;
|
|
1348
|
-
entry.steering_context = null;
|
|
1349
|
-
this.events.onRuntimeEvent(ctx.issue.id, {
|
|
1350
|
-
at: new Date().toISOString(),
|
|
1351
|
-
event: 'human_steering_received',
|
|
1352
|
-
message: reply.length > limit ? reply.slice(0, limit) + '…' : reply,
|
|
1353
|
-
});
|
|
1354
|
-
return { kind: 'received', pendingSteering: { question, context, reply } };
|
|
1355
|
-
}
|
|
1356
|
-
// -------------------------------------------------------------------------
|
|
1357
|
-
// Phase 8: session teardown (consolidates the old `cleanup(reason)` closure)
|
|
1358
|
-
// -------------------------------------------------------------------------
|
|
1359
|
-
/**
|
|
1360
|
-
* Unwind a session: cancel the bridge registration, destroy the socket and
|
|
1361
|
-
* kill the launch exec, deactivate MCP, run the per-state `actions:` block
|
|
1362
|
-
* (which may `run_in_vm` against the still-live VM), THEN tear down the
|
|
1363
|
-
* Gondolin dispatch — close the VM and deregister the per-VM secret manager.
|
|
1364
|
-
* The dispatch owns the VM lifecycle now (Gondolin), so teardown is synchronous
|
|
1365
|
-
* here rather than deferred to the reconciler.
|
|
1366
|
-
*
|
|
1367
|
-
* Returns the non-routed action failure reason, or null when the cleanup
|
|
1368
|
-
* succeeded or routed (so the caller can fold it into `decideAttemptOutcome`).
|
|
1369
|
-
*/
|
|
1370
|
-
async tearDownSession(ctx, reason) {
|
|
1371
|
-
this.detachSession(ctx, reason);
|
|
1372
|
-
await this.awaitExecExit(ctx.exec);
|
|
1373
|
-
this.deactivateMcpForEntry(ctx.runningEntry);
|
|
1374
|
-
ctx.logger.debug('agent runner cleanup', { reason });
|
|
1375
|
-
const nonRouted = await this.runCleanupActions(ctx.issue, resolveCleanupState(ctx.issue, ctx.runningEntry), ctx.runningEntry, ctx.workspacePath,
|
|
1376
|
-
// run_in_vm cleanup execs into the live dispatch VM before it is closed below.
|
|
1377
|
-
ctx.vm, ctx.hookCapture, ctx.runLog);
|
|
1378
|
-
await this.teardownDispatch(ctx, reason);
|
|
1379
|
-
return nonRouted;
|
|
1380
|
-
}
|
|
1381
|
-
detachSession(ctx, reason) {
|
|
1382
|
-
ctx.bridgeReg.cancel(reason);
|
|
1383
|
-
try {
|
|
1384
|
-
if (ctx.acpSocket && !ctx.acpSocket.destroyed)
|
|
1385
|
-
ctx.acpSocket.destroy();
|
|
1386
|
-
}
|
|
1387
|
-
catch { /* ignore */ }
|
|
1388
|
-
try {
|
|
1389
|
-
ctx.exec.kill();
|
|
1390
|
-
}
|
|
1391
|
-
catch { /* ignore */ }
|
|
1392
|
-
}
|
|
1393
|
-
async awaitExecExit(exec) {
|
|
1394
|
-
try {
|
|
1395
|
-
await exec.exit;
|
|
1396
|
-
}
|
|
1397
|
-
catch { /* ignore */ }
|
|
1398
|
-
}
|
|
1399
|
-
deactivateMcpForEntry(entry) {
|
|
1400
|
-
if (this.mcp && entry)
|
|
1401
|
-
this.mcp.deactivate(entry.identifier);
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* Close the dispatch's VM + deregister its secret manager (idempotent — the
|
|
1405
|
-
* dispatcher's `teardown()` guards re-entry). Runs after the cleanup actions
|
|
1406
|
-
* so `run_in_vm` still sees a live VM. A null teardown (the dispatch never
|
|
1407
|
-
* came up) is a no-op.
|
|
1408
|
-
*/
|
|
1409
|
-
async teardownDispatch(ctx, reason) {
|
|
1410
|
-
if (!ctx.teardownDispatch)
|
|
1411
|
-
return;
|
|
1412
|
-
ctx.runLog?.system('vm_teardown', { reason });
|
|
1413
|
-
try {
|
|
1414
|
-
await ctx.teardownDispatch();
|
|
1415
|
-
}
|
|
1416
|
-
catch (err) {
|
|
1417
|
-
ctx.logger.warn('gondolin dispatch teardown threw', { error: err.message });
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
class PhaseFailure extends Error {
|
|
1422
|
-
attemptResult;
|
|
1423
|
-
constructor(attemptResult) {
|
|
1424
|
-
super(`phase_failure: ${attemptResult.reason}`);
|
|
1425
|
-
this.attemptResult = attemptResult;
|
|
1426
|
-
this.name = 'PhaseFailure';
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
function failPhase(reason) {
|
|
1430
|
-
return { ok: false, result: { ok: false, reason, threadId: null, turnsCompleted: 0 } };
|
|
1431
|
-
}
|
|
1432
|
-
/**
|
|
1433
|
-
* Compose the final RunAttemptResult from the turn-loop outcome + teardown
|
|
1434
|
-
* action ledger. `mid_failure` (prompt render error, tracker refresh error)
|
|
1435
|
-
* gets surfaced verbatim; the happy path delegates to `decideAttemptOutcome`
|
|
1436
|
-
* which encodes the agentFailure > non-routed action failure > success
|
|
1437
|
-
* precedence.
|
|
1438
|
-
*/
|
|
1439
|
-
function composeAttemptResult(input) {
|
|
1440
|
-
if (input.loopRes.kind === 'mid_failure') {
|
|
1441
|
-
return {
|
|
1442
|
-
ok: false,
|
|
1443
|
-
reason: input.loopRes.publicReason,
|
|
1444
|
-
threadId: input.sessionId,
|
|
1445
|
-
turnsCompleted: input.loopRes.turnsCompleted,
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
return decideAttemptOutcome({
|
|
1449
|
-
agentFailure: input.loopRes.agentFailure,
|
|
1450
|
-
nonRoutedActionFailureReason: input.nonRouted,
|
|
1451
|
-
lastReason: input.loopRes.lastReason,
|
|
1452
|
-
sessionId: input.sessionId,
|
|
1453
|
-
turnsCompleted: input.loopRes.turnsCompleted,
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
//# sourceMappingURL=runner.js.map
|