smol-symphony 0.2.0 → 0.3.1
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/bin/symphony.js +22 -786
- package/dist/bin/symphony.js.map +1 -1
- package/dist/core/actions/context.js +109 -0
- package/dist/core/actions/context.js.map +1 -0
- package/dist/{actions/parsing.js → core/actions/parse.js} +33 -114
- package/dist/core/actions/parse.js.map +1 -0
- package/dist/core/actions/plan.js +197 -0
- package/dist/core/actions/plan.js.map +1 -0
- package/dist/core/actions/predicates.js +111 -0
- package/dist/core/actions/predicates.js.map +1 -0
- package/dist/core/actions/run-fold.js +248 -0
- package/dist/core/actions/run-fold.js.map +1 -0
- package/dist/core/actions/template.js +118 -0
- package/dist/core/actions/template.js.map +1 -0
- package/dist/core/cli/args.js +116 -0
- package/dist/core/cli/args.js.map +1 -0
- package/dist/core/coerce.js +75 -0
- package/dist/core/coerce.js.map +1 -0
- package/dist/core/credential/account-id.js +20 -0
- package/dist/core/credential/account-id.js.map +1 -0
- package/dist/core/credential/adapter-config.js +136 -0
- package/dist/core/credential/adapter-config.js.map +1 -0
- package/dist/core/credential/availability.js +98 -0
- package/dist/core/credential/availability.js.map +1 -0
- package/dist/core/credential/extract.js +228 -0
- package/dist/core/credential/extract.js.map +1 -0
- package/dist/core/credential/fake-creds.js +171 -0
- package/dist/core/credential/fake-creds.js.map +1 -0
- package/dist/core/credential/identity.js +125 -0
- package/dist/core/credential/identity.js.map +1 -0
- package/dist/core/credential/shape.js +230 -0
- package/dist/core/credential/shape.js.map +1 -0
- package/dist/core/credential/strings.js +15 -0
- package/dist/core/credential/strings.js.map +1 -0
- package/dist/core/doctor/checks.js +303 -0
- package/dist/core/doctor/checks.js.map +1 -0
- package/dist/core/git/result.js +107 -0
- package/dist/core/git/result.js.map +1 -0
- package/dist/core/http/decisions.js +225 -0
- package/dist/core/http/decisions.js.map +1 -0
- package/dist/{http.js → core/http/render.js} +472 -738
- package/dist/core/http/render.js.map +1 -0
- package/dist/{http-handlers.js → core/http/routes.js} +52 -87
- package/dist/core/http/routes.js.map +1 -0
- package/dist/core/http/views.js +181 -0
- package/dist/core/http/views.js.map +1 -0
- package/dist/core/image/managed-image.js +95 -0
- package/dist/core/image/managed-image.js.map +1 -0
- package/dist/core/issue/file.js +149 -0
- package/dist/core/issue/file.js.map +1 -0
- package/dist/core/issue/parse.js +210 -0
- package/dist/core/issue/parse.js.map +1 -0
- package/dist/core/mcp/dispatch.js +239 -0
- package/dist/core/mcp/dispatch.js.map +1 -0
- package/dist/core/mcp/post-move.js +92 -0
- package/dist/core/mcp/post-move.js.map +1 -0
- package/dist/core/mcp/protocol.js +293 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/url.js +162 -0
- package/dist/core/mcp/url.js.map +1 -0
- package/dist/core/path.js +63 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/reconcile/image-decide.js +48 -0
- package/dist/core/reconcile/image-decide.js.map +1 -0
- package/dist/core/reconcile/ledger.js +142 -0
- package/dist/core/reconcile/ledger.js.map +1 -0
- package/dist/core/reconcile/pr-classify.js +62 -0
- package/dist/core/reconcile/pr-classify.js.map +1 -0
- package/dist/{reconciler → core/reconcile}/pr-decide.js +25 -12
- package/dist/core/reconcile/pr-decide.js.map +1 -0
- package/dist/core/reconcile/pr-loop.js +161 -0
- package/dist/core/reconcile/pr-loop.js.map +1 -0
- package/dist/core/reconcile/pr-notes.js +35 -0
- package/dist/core/reconcile/pr-notes.js.map +1 -0
- package/dist/core/reconcile/vm-decide.js +70 -0
- package/dist/core/reconcile/vm-decide.js.map +1 -0
- package/dist/core/reconcile/vm-reap.js +207 -0
- package/dist/core/reconcile/vm-reap.js.map +1 -0
- package/dist/core/reconcile/workspace-decide.js +162 -0
- package/dist/core/reconcile/workspace-decide.js.map +1 -0
- package/dist/core/runlog/summary.js +231 -0
- package/dist/core/runlog/summary.js.map +1 -0
- package/dist/core/runner/dispatch-config.js +95 -0
- package/dist/core/runner/dispatch-config.js.map +1 -0
- package/dist/core/runner/injection.js +61 -0
- package/dist/core/runner/injection.js.map +1 -0
- package/dist/core/runner/mise.js +210 -0
- package/dist/core/runner/mise.js.map +1 -0
- package/dist/core/runner/prompt.js +720 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/turn.js +242 -0
- package/dist/core/runner/turn.js.map +1 -0
- package/dist/core/runner/vm-plan.js +390 -0
- package/dist/core/runner/vm-plan.js.map +1 -0
- package/dist/core/schedule/admission.js +123 -0
- package/dist/core/schedule/admission.js.map +1 -0
- package/dist/core/schedule/circuit-breaker.js +111 -0
- package/dist/core/schedule/circuit-breaker.js.map +1 -0
- package/dist/core/schedule/eligibility.js +83 -0
- package/dist/core/schedule/eligibility.js.map +1 -0
- package/dist/core/schedule/reconcile-issue.js +82 -0
- package/dist/core/schedule/reconcile-issue.js.map +1 -0
- package/dist/core/schedule/retry.js +96 -0
- package/dist/core/schedule/retry.js.map +1 -0
- package/dist/core/schedule/sleep-cycle.js +133 -0
- package/dist/core/schedule/sleep-cycle.js.map +1 -0
- package/dist/core/schedule/slots.js +124 -0
- package/dist/core/schedule/slots.js.map +1 -0
- package/dist/core/schedule/tick.js +553 -0
- package/dist/core/schedule/tick.js.map +1 -0
- package/dist/core/schedule/token-fold.js +181 -0
- package/dist/core/schedule/token-fold.js.map +1 -0
- package/dist/core/state-resolve.js +86 -0
- package/dist/core/state-resolve.js.map +1 -0
- package/dist/core/vm-guards.js +278 -0
- package/dist/core/vm-guards.js.map +1 -0
- package/dist/core/workflow/derive.js +107 -0
- package/dist/core/workflow/derive.js.map +1 -0
- package/dist/core/workflow/parse.js +687 -0
- package/dist/core/workflow/parse.js.map +1 -0
- package/dist/core/workflow/prompt-probe.js +78 -0
- package/dist/core/workflow/prompt-probe.js.map +1 -0
- package/dist/core/workflow/validate.js +189 -0
- package/dist/core/workflow/validate.js.map +1 -0
- package/dist/core/workspace-key.js +19 -0
- package/dist/core/workspace-key.js.map +1 -0
- package/dist/shell/actions-runner.js +356 -0
- package/dist/shell/actions-runner.js.map +1 -0
- package/dist/shell/adapter/adapter-registry.js +45 -0
- package/dist/shell/adapter/adapter-registry.js.map +1 -0
- package/dist/shell/adapter/clock-random.js +96 -0
- package/dist/shell/adapter/clock-random.js.map +1 -0
- package/dist/shell/adapter/gondolin-dispatch-helpers.js +158 -0
- package/dist/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
- package/dist/shell/adapter/gondolin-dispatch.js +385 -0
- package/dist/shell/adapter/gondolin-dispatch.js.map +1 -0
- package/dist/shell/adapter/gondolin-image-converter.js +233 -0
- package/dist/shell/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/shell/adapter/gondolin-image-fetch.js +180 -0
- package/dist/shell/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/shell/adapter/launcher-asset.js +57 -0
- package/dist/shell/adapter/launcher-asset.js.map +1 -0
- package/dist/shell/adapter/mise-config-asset.js +65 -0
- package/dist/shell/adapter/mise-config-asset.js.map +1 -0
- package/dist/shell/adapter/workflow-loader.js +304 -0
- package/dist/shell/adapter/workflow-loader.js.map +1 -0
- package/dist/shell/cli/doctor.js +268 -0
- package/dist/shell/cli/doctor.js.map +1 -0
- package/dist/shell/effect-interpreter-families.js +314 -0
- package/dist/shell/effect-interpreter-families.js.map +1 -0
- package/dist/shell/effect-interpreter.js +29 -0
- package/dist/shell/effect-interpreter.js.map +1 -0
- package/dist/shell/interp/acp-frame.js +137 -0
- package/dist/shell/interp/acp-frame.js.map +1 -0
- package/dist/shell/interp/acp-ws-conn.js +320 -0
- package/dist/shell/interp/acp-ws-conn.js.map +1 -0
- package/dist/shell/interp/acp-ws-frames.js +159 -0
- package/dist/shell/interp/acp-ws-frames.js.map +1 -0
- package/dist/shell/interp/acp-ws.js +197 -0
- package/dist/shell/interp/acp-ws.js.map +1 -0
- package/dist/shell/interp/acp.js +319 -0
- package/dist/shell/interp/acp.js.map +1 -0
- package/dist/shell/interp/credential-defaults.js +128 -0
- package/dist/shell/interp/credential-defaults.js.map +1 -0
- package/dist/shell/interp/credential-hooks.js +149 -0
- package/dist/shell/interp/credential-hooks.js.map +1 -0
- package/dist/shell/interp/credential-registry.js +226 -0
- package/dist/shell/interp/credential-registry.js.map +1 -0
- package/dist/shell/interp/credential.js +103 -0
- package/dist/shell/interp/credential.js.map +1 -0
- package/dist/shell/interp/gh.js +163 -0
- package/dist/shell/interp/gh.js.map +1 -0
- package/dist/shell/interp/git.js +28 -0
- package/dist/shell/interp/git.js.map +1 -0
- package/dist/shell/interp/log.js +213 -0
- package/dist/shell/interp/log.js.map +1 -0
- package/dist/shell/interp/process.js +178 -0
- package/dist/shell/interp/process.js.map +1 -0
- package/dist/shell/interp/runlog.js +193 -0
- package/dist/shell/interp/runlog.js.map +1 -0
- package/dist/shell/interp/timer.js +64 -0
- package/dist/shell/interp/timer.js.map +1 -0
- package/dist/shell/interp/tracker-disk.js +99 -0
- package/dist/shell/interp/tracker-disk.js.map +1 -0
- package/dist/shell/interp/tracker-parse.js +71 -0
- package/dist/shell/interp/tracker-parse.js.map +1 -0
- package/dist/shell/interp/tracker-scan.js +238 -0
- package/dist/shell/interp/tracker-scan.js.map +1 -0
- package/dist/shell/interp/tracker-write.js +91 -0
- package/dist/shell/interp/tracker-write.js.map +1 -0
- package/dist/shell/interp/tracker.js +41 -0
- package/dist/shell/interp/tracker.js.map +1 -0
- package/dist/shell/interp/tty.js +48 -0
- package/dist/shell/interp/tty.js.map +1 -0
- package/dist/shell/interp/vm.js +199 -0
- package/dist/shell/interp/vm.js.map +1 -0
- package/dist/shell/interp/workspace.js +310 -0
- package/dist/shell/interp/workspace.js.map +1 -0
- package/dist/shell/main-acp.js +78 -0
- package/dist/shell/main-acp.js.map +1 -0
- package/dist/shell/main-adapters.js +222 -0
- package/dist/shell/main-adapters.js.map +1 -0
- package/dist/shell/main-credential.js +122 -0
- package/dist/shell/main-credential.js.map +1 -0
- package/dist/shell/main-doctor.js +22 -0
- package/dist/shell/main-doctor.js.map +1 -0
- package/dist/shell/main-entry.js +46 -0
- package/dist/shell/main-entry.js.map +1 -0
- package/dist/shell/main-http-csrf.js +45 -0
- package/dist/shell/main-http-csrf.js.map +1 -0
- package/dist/shell/main-http-handler.js +389 -0
- package/dist/shell/main-http-handler.js.map +1 -0
- package/dist/shell/main-http-mcp.js +122 -0
- package/dist/shell/main-http-mcp.js.map +1 -0
- package/dist/shell/main-http-views.js +253 -0
- package/dist/shell/main-http-views.js.map +1 -0
- package/dist/shell/main-http.js +76 -0
- package/dist/shell/main-http.js.map +1 -0
- package/dist/shell/main-loops.js +130 -0
- package/dist/shell/main-loops.js.map +1 -0
- package/dist/shell/main-mcp.js +129 -0
- package/dist/shell/main-mcp.js.map +1 -0
- package/dist/shell/main-orchestrator.js +120 -0
- package/dist/shell/main-orchestrator.js.map +1 -0
- package/dist/shell/main-preflight.js +43 -0
- package/dist/shell/main-preflight.js.map +1 -0
- package/dist/shell/main-reconcilers-helpers.js +244 -0
- package/dist/shell/main-reconcilers-helpers.js.map +1 -0
- package/dist/shell/main-reconcilers-pr.js +148 -0
- package/dist/shell/main-reconcilers-pr.js.map +1 -0
- package/dist/shell/main-reconcilers.js +225 -0
- package/dist/shell/main-reconcilers.js.map +1 -0
- package/dist/shell/main-runner.js +355 -0
- package/dist/shell/main-runner.js.map +1 -0
- package/dist/shell/main-scaffold.js +116 -0
- package/dist/shell/main-scaffold.js.map +1 -0
- package/dist/shell/main-shutdown.js +115 -0
- package/dist/shell/main-shutdown.js.map +1 -0
- package/dist/shell/main-startup.js +48 -0
- package/dist/shell/main-startup.js.map +1 -0
- package/dist/shell/main-substrates.js +43 -0
- package/dist/shell/main-substrates.js.map +1 -0
- package/dist/shell/main.js +385 -0
- package/dist/shell/main.js.map +1 -0
- package/dist/shell/orchestrator-feedback.js +69 -0
- package/dist/shell/orchestrator-feedback.js.map +1 -0
- package/dist/shell/orchestrator-image.js +167 -0
- package/dist/shell/orchestrator-image.js.map +1 -0
- package/dist/shell/orchestrator-loop.js +468 -0
- package/dist/shell/orchestrator-loop.js.map +1 -0
- package/dist/shell/orchestrator-reconcile.js +36 -0
- package/dist/shell/orchestrator-reconcile.js.map +1 -0
- package/dist/shell/reconciler-loop.js +228 -0
- package/dist/shell/reconciler-loop.js.map +1 -0
- package/dist/shell/runner-loop-turn.js +301 -0
- package/dist/shell/runner-loop-turn.js.map +1 -0
- package/dist/shell/runner-loop.js +338 -0
- package/dist/shell/runner-loop.js.map +1 -0
- package/dist/shell/server/http.js +208 -0
- package/dist/shell/server/http.js.map +1 -0
- package/dist/shell/server/mcp-runtime-effects.js +237 -0
- package/dist/shell/server/mcp-runtime-effects.js.map +1 -0
- package/dist/shell/server/mcp-runtime.js +99 -0
- package/dist/shell/server/mcp-runtime.js.map +1 -0
- package/dist/shell/workspace-key.js +14 -0
- package/dist/shell/workspace-key.js.map +1 -0
- package/dist/types/acp.js +8 -0
- package/dist/types/acp.js.map +1 -0
- package/dist/types/actions/plan.js +6 -0
- package/dist/types/actions/plan.js.map +1 -0
- package/dist/types/actions/predicates.js +6 -0
- package/dist/types/actions/predicates.js.map +1 -0
- package/dist/types/actions/run-fold.js +8 -0
- package/dist/types/actions/run-fold.js.map +1 -0
- package/dist/types/actions.js +7 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/adapter/clock-random.js +4 -0
- package/dist/types/adapter/clock-random.js.map +1 -0
- package/dist/types/adapter/gondolin-image-converter.js +5 -0
- package/dist/types/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/types/adapter/gondolin-image-fetch.js +5 -0
- package/dist/types/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/types/adapter/workflow-loader.js +4 -0
- package/dist/types/adapter/workflow-loader.js.map +1 -0
- package/dist/types/cli/args.js +8 -0
- package/dist/types/cli/args.js.map +1 -0
- package/dist/types/config.js +8 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/credential-interp.js +6 -0
- package/dist/types/credential-interp.js.map +1 -0
- package/dist/types/credentials.js +10 -0
- package/dist/types/credentials.js.map +1 -0
- package/dist/types/doctor.js +7 -0
- package/dist/types/doctor.js.map +1 -0
- package/dist/types/domain.js +7 -0
- package/dist/types/domain.js.map +1 -0
- package/dist/types/effect.js +15 -0
- package/dist/types/effect.js.map +1 -0
- package/dist/types/errors.js +39 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/http/decisions.js +6 -0
- package/dist/types/http/decisions.js.map +1 -0
- package/dist/types/http/render.js +10 -0
- package/dist/types/http/render.js.map +1 -0
- package/dist/types/http/views.js +6 -0
- package/dist/types/http/views.js.map +1 -0
- package/dist/types/http.js +9 -0
- package/dist/types/http.js.map +1 -0
- package/dist/types/image/managed-image.js +7 -0
- package/dist/types/image/managed-image.js.map +1 -0
- package/dist/types/interp/effect-interpreter.js +8 -0
- package/dist/types/interp/effect-interpreter.js.map +1 -0
- package/dist/types/interp/tracker.js +7 -0
- package/dist/types/interp/tracker.js.map +1 -0
- package/dist/types/issue/file.js +6 -0
- package/dist/types/issue/file.js.map +1 -0
- package/dist/types/issue/parse.js +8 -0
- package/dist/types/issue/parse.js.map +1 -0
- package/dist/types/main-acp.js +13 -0
- package/dist/types/main-acp.js.map +1 -0
- package/dist/types/main-adapters.js +5 -0
- package/dist/types/main-adapters.js.map +1 -0
- package/dist/types/main-credential.js +21 -0
- package/dist/types/main-credential.js.map +1 -0
- package/dist/types/main-doctor.js +6 -0
- package/dist/types/main-doctor.js.map +1 -0
- package/dist/types/main-http-handler.js +12 -0
- package/dist/types/main-http-handler.js.map +1 -0
- package/dist/types/main-http.js +5 -0
- package/dist/types/main-http.js.map +1 -0
- package/dist/types/main-loops.js +5 -0
- package/dist/types/main-loops.js.map +1 -0
- package/dist/types/main-mcp.js +12 -0
- package/dist/types/main-mcp.js.map +1 -0
- package/dist/types/main-orchestrator.js +5 -0
- package/dist/types/main-orchestrator.js.map +1 -0
- package/dist/types/main-reconcilers.js +11 -0
- package/dist/types/main-reconcilers.js.map +1 -0
- package/dist/types/main-runner.js +13 -0
- package/dist/types/main-runner.js.map +1 -0
- package/dist/types/main-startup.js +5 -0
- package/dist/types/main-startup.js.map +1 -0
- package/dist/types/main-substrates.js +5 -0
- package/dist/types/main-substrates.js.map +1 -0
- package/dist/types/mcp/dispatch.js +4 -0
- package/dist/types/mcp/dispatch.js.map +1 -0
- package/dist/types/mcp/post-move.js +7 -0
- package/dist/types/mcp/post-move.js.map +1 -0
- package/dist/types/mcp.js +9 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/ports.js +12 -0
- package/dist/types/ports.js.map +1 -0
- package/dist/types/reconcile/image-decide.js +5 -0
- package/dist/types/reconcile/image-decide.js.map +1 -0
- package/dist/types/reconcile/ledger.js +7 -0
- package/dist/types/reconcile/ledger.js.map +1 -0
- package/dist/types/reconcile/pr-loop.js +8 -0
- package/dist/types/reconcile/pr-loop.js.map +1 -0
- package/dist/types/reconcile/vm-reap.js +8 -0
- package/dist/types/reconcile/vm-reap.js.map +1 -0
- package/dist/types/reconcile/workspace-decide.js +7 -0
- package/dist/types/reconcile/workspace-decide.js.map +1 -0
- package/dist/types/reconcile.js +9 -0
- package/dist/types/reconcile.js.map +1 -0
- package/dist/types/runlog.js +7 -0
- package/dist/types/runlog.js.map +1 -0
- package/dist/types/runner/actions-runner.js +12 -0
- package/dist/types/runner/actions-runner.js.map +1 -0
- package/dist/types/runner/gondolin-dispatch.js +5 -0
- package/dist/types/runner/gondolin-dispatch.js.map +1 -0
- package/dist/types/runner/injection.js +6 -0
- package/dist/types/runner/injection.js.map +1 -0
- package/dist/types/runner/runner-loop.js +5 -0
- package/dist/types/runner/runner-loop.js.map +1 -0
- package/dist/types/runner/turn.js +4 -0
- package/dist/types/runner/turn.js.map +1 -0
- package/dist/types/runner/vm-plan.js +4 -0
- package/dist/types/runner/vm-plan.js.map +1 -0
- package/dist/types/runtime.js +9 -0
- package/dist/types/runtime.js.map +1 -0
- package/dist/types/schedule/admission.js +7 -0
- package/dist/types/schedule/admission.js.map +1 -0
- package/dist/types/schedule/circuit-breaker.js +2 -0
- package/dist/types/schedule/circuit-breaker.js.map +1 -0
- package/dist/types/schedule/eligibility.js +9 -0
- package/dist/types/schedule/eligibility.js.map +1 -0
- package/dist/types/schedule/orchestrator-loop.js +10 -0
- package/dist/types/schedule/orchestrator-loop.js.map +1 -0
- package/dist/types/schedule/sleep-cycle.js +4 -0
- package/dist/types/schedule/sleep-cycle.js.map +1 -0
- package/dist/types/schedule/slots.js +8 -0
- package/dist/types/schedule/slots.js.map +1 -0
- package/dist/types/schedule/tick.js +9 -0
- package/dist/types/schedule/tick.js.map +1 -0
- package/dist/types/server/mcp-runtime.js +8 -0
- package/dist/types/server/mcp-runtime.js.map +1 -0
- package/dist/types/workflow/parse.js +4 -0
- package/dist/types/workflow/parse.js.map +1 -0
- package/package.json +22 -10
- 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/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/bin/symphony.js
CHANGED
|
@@ -1,794 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// symphony [path-to-WORKFLOW.md] [--port <port>]
|
|
4
|
-
// symphony reconcile [path-to-WORKFLOW.md] [--force] [--port <port>]
|
|
5
|
-
// symphony rerun --check=<name> [path-to-WORKFLOW.md]
|
|
2
|
+
// FCIS rewrite — the `symphony` binary ENTRYPOINT.
|
|
6
3
|
//
|
|
7
|
-
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// symphony [path-to-WORKFLOW.yaml] [--port <port>] [--reconcile-force] [--verbose]
|
|
6
|
+
// symphony doctor [path-to-WORKFLOW.yaml] [--port <port>]
|
|
7
|
+
// symphony reconcile [path-to-WORKFLOW.yaml] [--force] [--port <port>]
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// This file is intentionally a one-liner: the ENTIRE composition lives in the
|
|
10
|
+
// single composition root (src/shell/main.ts — the one file allowed to import the
|
|
11
|
+
// functional core). All this entrypoint does is invoke that root's `main()` and
|
|
12
|
+
// translate an unhandled rejection into a non-zero exit. Keeping it this thin
|
|
13
|
+
// means `package.json`'s `bin` target (dist/bin/symphony.js) and the `tsx`
|
|
14
|
+
// dev/start scripts share exactly the wiring path the tests exercise via
|
|
15
|
+
// `main(argv)`.
|
|
13
16
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// re-executes. It does not start a long-running process — it scans the
|
|
17
|
-
// workflow's state actions for a matching name and `rm`s the per-name cache
|
|
18
|
-
// namespace directory under `<cacheRoot>/actions/run_in_vm/<name>/`. The
|
|
19
|
-
// per-execution hash is workspace-dependent (the agent's edits change the
|
|
20
|
-
// tree); namespacing the cache by action name on disk means the CLI doesn't
|
|
21
|
-
// need to know any per-issue workspace state to invalidate the right
|
|
22
|
-
// entries.
|
|
23
|
-
import path from 'node:path';
|
|
17
|
+
// package.json declares: "bin": { "symphony": "dist/bin/symphony.js" }
|
|
18
|
+
// and dev scripts run this file directly via tsx (src/bin/symphony.ts).
|
|
24
19
|
import process from 'node:process';
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import { GondolinVmClient } from '../agent/gondolin.js';
|
|
34
|
-
import { CredentialSecretRegistry, buildAdapterCredentialSpecs, buildAdapterHooksConfig, } from '../agent/credential-secrets.js';
|
|
35
|
-
import { defaultHostIdentityReaders } from '../agent/gondolin-creds-staging.js';
|
|
36
|
-
import { KNOWN_ADAPTER_IDS } from '../agent/adapter-names.js';
|
|
37
|
-
import { AgentRunner } from '../agent/runner.js';
|
|
38
|
-
import { Orchestrator } from '../orchestrator.js';
|
|
39
|
-
import { startHttpServer } from '../http.js';
|
|
40
|
-
import { McpRegistry } from '../mcp.js';
|
|
41
|
-
import { AcpBridge } from '../acp-bridge.js';
|
|
42
|
-
import { CredentialTicker } from '../agent/credential-ticker.js';
|
|
43
|
-
import { GhCliPrApi, Reconciler } from '../reconciler/index.js';
|
|
44
|
-
import { closeLogFile, log, setLogFile, setLogVerbose } from '../logging.js';
|
|
45
|
-
import { derivePrRouting } from '../workflow.js';
|
|
46
|
-
/**
|
|
47
|
-
* Walk every declared state's `actions:` for a run_in_vm whose `name` matches
|
|
48
|
-
* `target`. Returns the first match; duplicates would let a single rerun
|
|
49
|
-
* invalidate multiple entries, which is rarely intended (operator wants to
|
|
50
|
-
* re-run *one* named check).
|
|
51
|
-
*/
|
|
52
|
-
function findRunInVmByName(states, target) {
|
|
53
|
-
for (const [stateName, sc] of Object.entries(states)) {
|
|
54
|
-
if (!sc.actions)
|
|
55
|
-
continue;
|
|
56
|
-
for (const a of sc.actions) {
|
|
57
|
-
if (a.kind === 'run_in_vm' && a.name === target) {
|
|
58
|
-
return { state: stateName, action: a };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
async function runRerunCheck(workflowPath, name) {
|
|
65
|
-
let cfg;
|
|
66
|
-
try {
|
|
67
|
-
({ config: cfg } = await loadWorkflow(workflowPath));
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
process.stderr.write(`error: failed to load workflow: ${err.message}\n`);
|
|
71
|
-
return 1;
|
|
72
|
-
}
|
|
73
|
-
const match = findRunInVmByName(cfg.states, name);
|
|
74
|
-
if (!match) {
|
|
75
|
-
process.stderr.write(`error: no run_in_vm action named "${name}" declared in WORKFLOW.md\n`);
|
|
76
|
-
return 2;
|
|
77
|
-
}
|
|
78
|
-
// Drop the per-name cache namespace directory. This invalidates every
|
|
79
|
-
// hash entry under that name regardless of which per-issue workspace the
|
|
80
|
-
// execution computed its hash against — the orchestrator's next dispatch
|
|
81
|
-
// re-executes the check because the namespace is empty.
|
|
82
|
-
await invalidateRunInVmByName(match.action);
|
|
83
|
-
process.stdout.write(`invalidated run_in_vm "${name}" (state=${match.state})\n`);
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Read a single line from stdin with the given prompt. Resolves to the
|
|
88
|
-
* trimmed input string (without the trailing newline). The readline interface
|
|
89
|
-
* is closed before resolving so the process can exit cleanly afterwards.
|
|
90
|
-
*/
|
|
91
|
-
function promptLine(message) {
|
|
92
|
-
const rl = readline.createInterface({
|
|
93
|
-
input: process.stdin,
|
|
94
|
-
output: process.stdout,
|
|
95
|
-
});
|
|
96
|
-
return new Promise((resolve) => {
|
|
97
|
-
rl.question(message, (answer) => {
|
|
98
|
-
rl.close();
|
|
99
|
-
resolve(answer);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* When the workflow file is missing and the operator is at an interactive
|
|
105
|
-
* terminal, ask whether to scaffold a starter file. Returns true if the
|
|
106
|
-
* scaffold was written (caller can continue boot), false otherwise (caller
|
|
107
|
-
* should fall through to the usual "file not found" error).
|
|
108
|
-
*
|
|
109
|
-
* Non-interactive invocations (cron jobs, CI, container ENTRYPOINTs) skip the
|
|
110
|
-
* prompt entirely and return false — silently scaffolding files into someone
|
|
111
|
-
* else's working tree without a confirmed yes is the wrong default for a tool
|
|
112
|
-
* that's usually run by an operator who knows where their workflow lives.
|
|
113
|
-
*/
|
|
114
|
-
async function maybeScaffoldMissingWorkflow(workflowPath) {
|
|
115
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
116
|
-
return false;
|
|
117
|
-
const answer = await promptLine(`WORKFLOW.md not found at ${workflowPath}.\nScaffold a starter workflow file here? [Y/n] `);
|
|
118
|
-
const normalized = answer.trim().toLowerCase();
|
|
119
|
-
// Default-accept: bare enter, "y", "yes". Anything else is "no".
|
|
120
|
-
const accept = normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
121
|
-
if (!accept)
|
|
122
|
-
return false;
|
|
123
|
-
try {
|
|
124
|
-
const result = await scaffoldWorkflow({ workflowPath });
|
|
125
|
-
process.stdout.write(`wrote ${result.workflowPath}\n`);
|
|
126
|
-
process.stdout.write(`Edit it to point gondolin.image at your built agent image (npm run build:image), ` +
|
|
127
|
-
`then run \`symphony ${path.relative(process.cwd(), result.workflowPath) || workflowPath}\` again.\n`);
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
const msg = err instanceof ScaffoldError ? err.message : err.message;
|
|
132
|
-
process.stderr.write(`error: scaffold failed: ${msg}\n`);
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Centralized startup-failure cleanup. Writes the message, closes the optional
|
|
138
|
-
* HTTP listener and workflow watcher, flushes the persistent log sink, and
|
|
139
|
-
* exits non-zero. Returns `never` so call sites can treat the path as a hard
|
|
140
|
-
* terminator and TypeScript narrows away post-call code.
|
|
141
|
-
*
|
|
142
|
-
* `flushLogs` matters because `process.exit` does NOT drain pending WriteStream
|
|
143
|
-
* writes — without `closeLogFile()` we'd lose the final lines symphony.log was
|
|
144
|
-
* about to receive (the startup-failure stderr line itself is unaffected, but
|
|
145
|
-
* any buffered `log.*` output would be dropped).
|
|
146
|
-
*/
|
|
147
|
-
async function bailStartup(message, opts) {
|
|
148
|
-
process.stderr.write(message);
|
|
149
|
-
if (opts.http)
|
|
150
|
-
await opts.http.close().catch(() => undefined);
|
|
151
|
-
await opts.src.stop().catch(() => undefined);
|
|
152
|
-
await closeLogFile().catch(() => undefined);
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Handle the two early-exit subcommands that don't enter the orchestrator
|
|
157
|
-
* graph: a missing workflow file (offer to scaffold, or fail), and the
|
|
158
|
-
* `rerun --check=<name>` subcommand (invalidate one action's cache and exit).
|
|
159
|
-
* Returns to the caller only on the happy path of `serve`/`reconcile` against
|
|
160
|
-
* an existing workflow.
|
|
161
|
-
*/
|
|
162
|
-
async function handlePreflight(cli, workflowPath) {
|
|
163
|
-
if (!existsSync(workflowPath)) {
|
|
164
|
-
// `rerun` operates on an existing workflow's action namespace; there is
|
|
165
|
-
// nothing to scaffold against. Same for `reconcile`, which only makes sense
|
|
166
|
-
// when a workflow already exists. Prompt only on the bare `serve` path.
|
|
167
|
-
if (cli.subcommand === 'serve') {
|
|
168
|
-
const scaffolded = await maybeScaffoldMissingWorkflow(workflowPath);
|
|
169
|
-
// Stop here on purpose: the operator hasn't finished filling in
|
|
170
|
-
// the gondolin.* / source-of-truth fields yet, and dispatching immediately
|
|
171
|
-
// would just fail at the first attempt with a confusing error. The
|
|
172
|
-
// scaffold message already tells them how to relaunch.
|
|
173
|
-
if (scaffolded)
|
|
174
|
-
process.exit(0);
|
|
175
|
-
}
|
|
176
|
-
process.stderr.write(`error: workflow file not found: ${workflowPath}\n`);
|
|
177
|
-
process.exit(2);
|
|
178
|
-
}
|
|
179
|
-
if (cli.subcommand === 'rerun') {
|
|
180
|
-
if (!cli.rerunCheck) {
|
|
181
|
-
process.stderr.write(`error: rerun requires --check=<name>\n`);
|
|
182
|
-
process.exit(2);
|
|
183
|
-
}
|
|
184
|
-
const code = await runRerunCheck(workflowPath, cli.rerunCheck);
|
|
185
|
-
process.exit(code);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Start the workflow watcher, validate that this build can serve the parsed
|
|
190
|
-
* tracker (currently `kind=local` only), and resolve the persistent log file
|
|
191
|
-
* path. Mirrors stderr to disk so an agent reviewing a run after the fact
|
|
192
|
-
* (typically inside a VM with the workspace + .symphony/logs/ mounted in) can
|
|
193
|
-
* read orchestrator-side events — workflow reloads, dispatch decisions, action
|
|
194
|
-
* results, reconciler ticks — alongside the per-issue JSONL run logs in the
|
|
195
|
-
* same directory.
|
|
196
|
-
*
|
|
197
|
-
* Path resolution: `SYMPHONY_LOG_FILE` env override wins (`""` disables the
|
|
198
|
-
* sink); otherwise `<logs.root>/symphony.log`. The directory is created on
|
|
199
|
-
* demand. File-sink failure is swallowed: symphony continues on stderr only.
|
|
200
|
-
*/
|
|
201
|
-
async function loadAndValidateConfig(workflowPath) {
|
|
202
|
-
let src;
|
|
203
|
-
try {
|
|
204
|
-
src = await watchWorkflow(workflowPath);
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
process.stderr.write(`error: failed to load workflow: ${err.message}\n`);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
const { definition, config } = src.current();
|
|
211
|
-
if (config.tracker.kind !== 'local') {
|
|
212
|
-
process.stderr.write(`error: this build supports tracker.kind=local only (got: ${config.tracker.kind || '<unset>'})\n`);
|
|
213
|
-
process.exit(2);
|
|
214
|
-
}
|
|
215
|
-
const envLogFile = process.env.SYMPHONY_LOG_FILE;
|
|
216
|
-
const logFile = envLogFile === undefined
|
|
217
|
-
? path.join(config.logs.root, 'symphony.log')
|
|
218
|
-
: envLogFile === ''
|
|
219
|
-
? null
|
|
220
|
-
: envLogFile;
|
|
221
|
-
setLogFile(logFile);
|
|
222
|
-
return { src, definition, config, envLogFile, logFile };
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Build the host credential pipeline for the Gondolin secret-substitution model
|
|
226
|
-
* (replaces the credential proxy). There is no HTTP proxy server and no base-URL
|
|
227
|
-
* injection: per-adapter specs carry the extractor/mint/flock-refresh logic, a
|
|
228
|
-
* single shared `CredentialSecretRegistry` owns every live per-VM secretManager
|
|
229
|
-
* and seeds it before first egress, and the per-adapter hooks configs (allowlist
|
|
230
|
-
* + token-shaped placeholder + request/response hooks) thread into each dispatch's
|
|
231
|
-
* `createHttpHooks`. The ticker fans `refreshAdapter` over every live adapter.
|
|
232
|
-
*/
|
|
233
|
-
async function buildCredentialPipeline(config) {
|
|
234
|
-
// Resolve the host's NON-SECRET codex `chatgpt_account_id` once and bind it into
|
|
235
|
-
// the codex placeholder JWT's auth claim — without it codex-acp attempts a
|
|
236
|
-
// mid-turn token refresh (egress-blocked → 403 → refusal; the go-live finding).
|
|
237
|
-
// Best-effort: a missing/malformed auth.json yields null (claim omitted).
|
|
238
|
-
const codexAccountId = await defaultHostIdentityReaders().readCodexAccountId();
|
|
239
|
-
const specs = buildAdapterCredentialSpecs({ codexAccountId });
|
|
240
|
-
const adapterHooks = buildAllAdapterHooks(specs, config.egress.allowed_hosts);
|
|
241
|
-
const credentialRegistry = new CredentialSecretRegistry({
|
|
242
|
-
readToken: (adapterId) => specs[adapterId].readToken(),
|
|
243
|
-
refresh: (adapterId) => specs[adapterId].refresh(),
|
|
244
|
-
});
|
|
245
|
-
const credentialTicker = new CredentialTicker({
|
|
246
|
-
intervalMs: config.credentials.ticker_interval_ms,
|
|
247
|
-
// Fan a host-side refresh over every adapter; each adapter's flock +
|
|
248
|
-
// single-flight collapses concurrent ticks into one host refresh, and the
|
|
249
|
-
// registry seeds every live per-VM manager with the fresh value.
|
|
250
|
-
refreshAll: () => refreshAllAdapters(credentialRegistry),
|
|
251
|
-
});
|
|
252
|
-
return { credentialRegistry, adapterHooks, credentialTicker };
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Build the per-adapter `createHttpHooks` config map from the credential specs.
|
|
256
|
-
* `egressAllowlist` is the general workspace dev-tooling firewall (npm/git/CDNs)
|
|
257
|
-
* unioned into every adapter's `allowedHosts` (never into its substitution scope).
|
|
258
|
-
*/
|
|
259
|
-
function buildAllAdapterHooks(specs, egressAllowlist) {
|
|
260
|
-
const out = {};
|
|
261
|
-
for (const id of KNOWN_ADAPTER_IDS) {
|
|
262
|
-
out[id] = buildAdapterHooksConfig(specs[id], egressAllowlist);
|
|
263
|
-
}
|
|
264
|
-
return out;
|
|
265
|
-
}
|
|
266
|
-
/** Drive a host-side refresh + fan-out for every adapter (the ticker cadence). */
|
|
267
|
-
async function refreshAllAdapters(registry) {
|
|
268
|
-
for (const id of KNOWN_ADAPTER_IDS) {
|
|
269
|
-
await registry.refreshAdapter(id);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Resolve the static Gondolin VM shape from config. Fail fast if no image is set
|
|
274
|
-
* so a misconfigured workflow surfaces at boot, not mid-dispatch after the VM
|
|
275
|
-
* bring-up cost is sunk.
|
|
276
|
-
*/
|
|
277
|
-
function resolveGondolinVmConfig(config) {
|
|
278
|
-
const imagePath = config.gondolin.image;
|
|
279
|
-
if (!imagePath || imagePath.length === 0) {
|
|
280
|
-
throw new Error('gondolin: no VM image configured. Set gondolin.image (a build id / `name:tag` ref / ' +
|
|
281
|
-
'asset dir exported by `npm run build:image` — see images/agents) in WORKFLOW.md.');
|
|
282
|
-
}
|
|
283
|
-
return { imagePath, cpus: config.gondolin.cpus, memMib: config.gondolin.mem_mib };
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Build the in-process graph: tracker, workspaces, vmClient, mcp, acpBridge,
|
|
287
|
-
* reconciler, runner, orchestrator. Wires the post-construction provider
|
|
288
|
-
* callbacks (`reconciler.setIntendedVmProvider` / `setWorkspaceProviders` /
|
|
289
|
-
* `setPrAutopilotProviders`) and the reload callback that propagates config
|
|
290
|
-
* updates through every component.
|
|
291
|
-
*
|
|
292
|
-
* The Reconciler is constructed before the Orchestrator (the runner needs the
|
|
293
|
-
* reconciler at its own construction time), so the vm reaper's
|
|
294
|
-
* IntendedVmProvider and workspace providers are plugged in after the
|
|
295
|
-
* orchestrator exists. The vm resource is only built when both `vmClient`
|
|
296
|
-
* (passed at Reconciler construction) and an intended provider are wired.
|
|
297
|
-
*/
|
|
298
|
-
async function buildOrchestratorGraph(opts) {
|
|
299
|
-
const { config, definition, src, envLogFile } = opts;
|
|
300
|
-
const tracker = new LocalMarkdownTracker(config.tracker);
|
|
301
|
-
// Materialize every declared state directory under tracker.root up front so
|
|
302
|
-
// the dashboard sees the full set of columns (including `holding` states like
|
|
303
|
-
// Triage) before any issue lands in them.
|
|
304
|
-
try {
|
|
305
|
-
await tracker.start();
|
|
306
|
-
}
|
|
307
|
-
catch (err) {
|
|
308
|
-
await bailStartup(`error: tracker init failed: ${err.message}\n`, { src });
|
|
309
|
-
}
|
|
310
|
-
const workspaces = new WorkspaceManager(config);
|
|
311
|
-
// Gondolin VM substrate (the in-process VM backend for the dispatch
|
|
312
|
-
// path). The runner builds a per-dispatch GondolinDispatcher over this client,
|
|
313
|
-
// and the Reconciler's VM reaper observes its session registry / runs its GC.
|
|
314
|
-
const vmClient = new GondolinVmClient();
|
|
315
|
-
const gondolinVmConfig = resolveGondolinVmConfig(config);
|
|
316
|
-
// Always instantiate the registry so a workflow reload that flips mcp.enabled from
|
|
317
|
-
// false to true takes effect without a process restart. The runner and HTTP routes
|
|
318
|
-
// gate behavior on cfg.mcp.enabled at runtime; an inactive registry holds no entries
|
|
319
|
-
// and answers all routes with "not active."
|
|
320
|
-
const mcp = new McpRegistry(tracker, {
|
|
321
|
-
states: config.states,
|
|
322
|
-
// The PR engine owns the merge state's workspace; pass the engine-enabled
|
|
323
|
-
// flag + the derived merge-state name so transitions into it defer terminal
|
|
324
|
-
// cleanup (issue 144 retired the old derived compatibility view).
|
|
325
|
-
prMerge: { enabled: config.pr.enabled, mergeState: derivePrRouting(config.states).mergeState },
|
|
326
|
-
now: () => Date.now(),
|
|
327
|
-
});
|
|
328
|
-
// ACP transport. The bridge listens on a loopback TCP port for the in-VM
|
|
329
|
-
// agent's dial-back (raw mapped TCP via Gondolin `tcp.hosts`). `loopbackOnly`
|
|
330
|
-
// hard-refuses a wider bind so the bearer-gated control channel can never be
|
|
331
|
-
// exposed to the host LAN. Started below alongside the HTTP server so a bind
|
|
332
|
-
// failure surfaces before we accept any dispatches.
|
|
333
|
-
const acpBridge = new AcpBridge({ loopbackOnly: true });
|
|
334
|
-
const { credentialRegistry, adapterHooks, credentialTicker } = await buildCredentialPipeline(config);
|
|
335
|
-
// Reconciler (issues 32, 33, 34). Owns the VM reaper (now Gondolin-backed:
|
|
336
|
-
// observes `vmClient.listSessions()` + runs `vmClient.gc()`, reaping
|
|
337
|
-
// `symphony-`-labelled sessions not in the orchestrator's intended set) + the
|
|
338
|
-
// per-issue workspace convergence. Bake is bypassed on the Gondolin dispatch
|
|
339
|
-
// path (the runner uses the prebuilt image directly); the bake resource stays
|
|
340
|
-
// for now and is deleted in a later PR.
|
|
341
|
-
const reconciler = new Reconciler(config, { vmClient });
|
|
342
|
-
// Build the runner with stubs first; we attach the orchestrator's provider callbacks after
|
|
343
|
-
// construction since they reference the orchestrator instance.
|
|
344
|
-
let orch;
|
|
345
|
-
const runner = new AgentRunner(config, definition, workspaces, tracker, vmClient, {
|
|
346
|
-
onRuntimeEvent: (id, ev) => orch.reportRuntimeEvent(id, ev),
|
|
347
|
-
onTokenUsage: (id, u) => orch.reportTokenUsage(id, u),
|
|
348
|
-
onRateLimits: (id, s) => orch.reportRateLimits(id, s),
|
|
349
|
-
onTurn: (id, turn) => orch.reportTurnStarted(id, turn),
|
|
350
|
-
onSessionStarted: (info) => orch.reportSessionStarted(info.issueId, {
|
|
351
|
-
sessionId: info.sessionId,
|
|
352
|
-
threadId: info.threadId,
|
|
353
|
-
pid: info.pid,
|
|
354
|
-
}),
|
|
355
|
-
}, mcp, acpBridge,
|
|
356
|
-
// propose_followup sink (issue 36): orchestrator owns the tracker write
|
|
357
|
-
// path, mirroring how the MCP `propose_issue` tool routes through the
|
|
358
|
-
// tracker. The runner forwards the parent identifier so provenance is
|
|
359
|
-
// recorded the same way.
|
|
360
|
-
{ proposeFollowup: (input) => orch.proposeFollowup(input) },
|
|
361
|
-
// Action snapshot sink (issue 36 AC5): per-attempt ledger surfaces on
|
|
362
|
-
// /api/v1/snapshot under reconciler.resources so the dashboard sees
|
|
363
|
-
// "Done.actions: …" alongside the bake/vm/workspace resources.
|
|
364
|
-
{ recordActionResult: (id, snap) => orch.recordActionResult(id, snap) },
|
|
365
|
-
// Gondolin credential layer (replaced the credential proxy): the shared
|
|
366
|
-
// registry of per-VM secret managers, the per-adapter hooks configs, and the
|
|
367
|
-
// static VM shape (image + cpus/mem).
|
|
368
|
-
credentialRegistry, adapterHooks, gondolinVmConfig);
|
|
369
|
-
orch = new Orchestrator(config, definition, src, tracker, workspaces, runner, undefined, reconciler);
|
|
370
|
-
wirePostConstructionProviders({ reconciler, orch, config });
|
|
371
|
-
// The tracker view is resolved through a getter so reloaded config (e.g. a moved
|
|
372
|
-
// tracker.root, changed active/terminal states) is reflected by both the propagation
|
|
373
|
-
// callback and the HTTP UI without rebinding the server.
|
|
374
|
-
let liveCfg = config;
|
|
375
|
-
orch.setOnConfigReloaded(buildReloadHandler({
|
|
376
|
-
tracker,
|
|
377
|
-
workspaces,
|
|
378
|
-
runner,
|
|
379
|
-
mcp,
|
|
380
|
-
envLogFile,
|
|
381
|
-
onLiveCfg: (cfg) => {
|
|
382
|
-
liveCfg = cfg;
|
|
383
|
-
},
|
|
384
|
-
}));
|
|
385
|
-
return {
|
|
386
|
-
tracker,
|
|
387
|
-
workspaces,
|
|
388
|
-
mcp,
|
|
389
|
-
acpBridge,
|
|
390
|
-
credentialTicker,
|
|
391
|
-
reconciler,
|
|
392
|
-
runner,
|
|
393
|
-
orch,
|
|
394
|
-
vmClient,
|
|
395
|
-
getLiveCfg: () => liveCfg,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Plug the orchestrator into the reconciler as the IntendedVmProvider, the
|
|
400
|
-
* workspace intended/baseRef provider (with remove + create delegating back
|
|
401
|
-
* through the orchestrator so the canonical workspace setup runs on
|
|
402
|
-
* reconciler-driven passes), and the PR autopilot's set of providers (intended
|
|
403
|
-
* set, PR/git adapters, transition router, cleanup callback, and workspace
|
|
404
|
-
* re-materializer). Kept as a separate function so `buildOrchestratorGraph`
|
|
405
|
-
* stays within the imperative-shell statement budget.
|
|
406
|
-
*/
|
|
407
|
-
function wirePostConstructionProviders(opts) {
|
|
408
|
-
const { reconciler, orch, config } = opts;
|
|
409
|
-
reconciler.setIntendedVmProvider(orch);
|
|
410
|
-
// Removal is delegated to WorkspaceManager (a best-effort `rm -rf`) so janitor
|
|
411
|
-
// removals reuse the same path the runner does — the closure captures
|
|
412
|
-
// `workspaces` (whose config is kept live via updateConfig on reload), so a
|
|
413
|
-
// rotated `workspace.root` takes effect without rebuilding the reconciler.
|
|
414
|
-
reconciler.setWorkspaceProviders(orch, {
|
|
415
|
-
baseRef: orch,
|
|
416
|
-
remove: (identifier) => orch.removeWorkspace(identifier),
|
|
417
|
-
// Create callback for the reconciler's eager-workspace pass (issue 34).
|
|
418
|
-
// Delegates to `WorkspaceManager.ensureFor` via the orchestrator so the
|
|
419
|
-
// canonical clone+branch+remote setup fires on reconciler-driven creates
|
|
420
|
-
// the same way it does on dispatch. The intended-set provider supplies the
|
|
421
|
-
// issue's current state alongside the identifier (used for the merge-state
|
|
422
|
-
// guard); the per-identifier ensureFor lock collapses any race with
|
|
423
|
-
// concurrent dispatch into one setup pass.
|
|
424
|
-
create: (identifier, state) => orch.createWorkspace(identifier, state),
|
|
425
|
-
});
|
|
426
|
-
// PR autopilot wiring (issue 38). The Reconciler ignores this when
|
|
427
|
-
// `pr.enabled` is false (it stays a no-op pass), so we set the
|
|
428
|
-
// providers unconditionally — a reload that flips the flag picks them up
|
|
429
|
-
// via `updateConfig`'s rebuild path.
|
|
430
|
-
reconciler.setPrAutopilotProviders({
|
|
431
|
-
intended: orch,
|
|
432
|
-
// Pin the autopilot's `gh` calls to the configured target repo (env
|
|
433
|
-
// SYMPHONY_REPO or workspace.github_repo) so they don't silently fall back
|
|
434
|
-
// to cwd-inference in the out-of-repo layout the github_repo knob enables.
|
|
435
|
-
// Captured at startup; retargeting needs a restart (see GhCliPrApi.repo).
|
|
436
|
-
pr: new GhCliPrApi({ timeoutMs: 30_000, repo: resolveGithubRepo(config.workspace.github_repo) }),
|
|
437
|
-
transition: {
|
|
438
|
-
routeIssue: (input) => orch.routeIssueForAutopilot(input),
|
|
439
|
-
},
|
|
440
|
-
cleanup: {
|
|
441
|
-
removeWorkspace: (identifier) => orch.removeWorkspace(identifier),
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Returns the orchestrator's `onConfigReloaded` callback. On every reload it
|
|
447
|
-
* forwards the freshly-parsed config to each long-lived component, retargets
|
|
448
|
-
* the persistent log sink if `logs.root` rotated (unless the env override
|
|
449
|
-
* locked it for the process lifetime), and re-materializes any state
|
|
450
|
-
* directory the new workflow introduced. The orchestrator's own onChange
|
|
451
|
-
* handler already forwards to the reconciler (so a config change rebinds its
|
|
452
|
-
* managed resources); we do not re-forward here.
|
|
453
|
-
*/
|
|
454
|
-
function buildReloadHandler(opts) {
|
|
455
|
-
const { tracker, workspaces, runner, mcp, envLogFile, onLiveCfg } = opts;
|
|
456
|
-
return (cfg, def) => {
|
|
457
|
-
tracker.updateConfig(cfg.tracker);
|
|
458
|
-
workspaces.updateConfig(cfg);
|
|
459
|
-
runner.updateConfig(cfg, def);
|
|
460
|
-
mcp.updateStates(cfg.states, {
|
|
461
|
-
enabled: cfg.pr.enabled,
|
|
462
|
-
mergeState: derivePrRouting(cfg.states).mergeState,
|
|
463
|
-
});
|
|
464
|
-
onLiveCfg(cfg);
|
|
465
|
-
if (envLogFile === undefined) {
|
|
466
|
-
setLogFile(path.join(cfg.logs.root, 'symphony.log'));
|
|
467
|
-
}
|
|
468
|
-
// Best-effort: a mkdir failure here would normally come from a tracker.root
|
|
469
|
-
// rotation that also failed at validateDispatch, so logging is enough.
|
|
470
|
-
void tracker.start().catch((err) => {
|
|
471
|
-
log.warn('tracker reinit after reload failed', { error: err.message });
|
|
472
|
-
});
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Bind the ACP TCP bridge, the optional HTTP server, and verify that — if MCP
|
|
477
|
-
* is enabled — a reachable MCP URL can be constructed. Each bind/precondition
|
|
478
|
-
* failure routes through `bailStartup` so the failure mode is uniform: write
|
|
479
|
-
* to stderr, close any partial listeners, flush logs, exit non-zero.
|
|
480
|
-
*
|
|
481
|
-
* The ACP bridge must come up BEFORE we accept any dispatches: a bind failure
|
|
482
|
-
* here is fatal because we cannot run agents without their transport. The MCP
|
|
483
|
-
* precondition check is hoisted to boot so an in-VM agent doesn't fail mid-
|
|
484
|
-
* dispatch (after the VM bring-up cost is sunk) with a misconfiguration the
|
|
485
|
-
* operator could have caught at startup.
|
|
486
|
-
*/
|
|
487
|
-
async function startTransports(opts) {
|
|
488
|
-
const { config, graph, cli, src, workflowPath } = opts;
|
|
489
|
-
// The Gondolin ACP channel is raw mapped TCP: the guest dials a synthetic name
|
|
490
|
-
// tunnelled to the host loopback via `tcp.hosts`. So the bridge binds loopback
|
|
491
|
-
// (the `reach_host`, default 127.0.0.1) and `loopbackOnly` hard-refuses a wider
|
|
492
|
-
// bind — never the config `bind_host` (which defaults to 0.0.0.0 for the old
|
|
493
|
-
// slirp gateway).
|
|
494
|
-
const bridgeHost = config.acp.bridge.reach_host;
|
|
495
|
-
try {
|
|
496
|
-
await graph.acpBridge.start(bridgeHost, config.acp.bridge.bind_port);
|
|
497
|
-
}
|
|
498
|
-
catch (err) {
|
|
499
|
-
await bailStartup(`error: failed to bind ACP bridge on ${bridgeHost}:${config.acp.bridge.bind_port}: ${err.message}\n`, { src });
|
|
500
|
-
}
|
|
501
|
-
startCredentialTicker(graph);
|
|
502
|
-
const http = await bindHttpServer({ config, graph, cli, src, workflowPath });
|
|
503
|
-
await checkMcpPrecondition({ config, graph, src, http });
|
|
504
|
-
return { http };
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Start the host credential ticker (Gondolin secret-substitution model). There
|
|
508
|
-
* is NO proxy server to bind — the registry seeds each per-VM secret manager at
|
|
509
|
-
* dispatch and the ticker drives a periodic host-side refresh fan-out. So this
|
|
510
|
-
* is just the ticker timer; nothing here can fail-to-bind.
|
|
511
|
-
*/
|
|
512
|
-
function startCredentialTicker(graph) {
|
|
513
|
-
graph.credentialTicker.start();
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Resolve the HTTP port (CLI override > workflow `server.port` > none), bind
|
|
517
|
-
* if requested, and tell the MCP registry the *actually* bound port. The
|
|
518
|
-
* registry needs the live port (not the requested one) so URLs injected into
|
|
519
|
-
* agents point at the real listener — with `--port 0` the kernel picks an
|
|
520
|
-
* ephemeral port that differs from what we asked for.
|
|
521
|
-
*/
|
|
522
|
-
async function bindHttpServer(opts) {
|
|
523
|
-
const { config, graph, cli, src, workflowPath } = opts;
|
|
524
|
-
const httpPort = cli.port ?? config.server.port;
|
|
525
|
-
if (httpPort === null || httpPort === undefined)
|
|
526
|
-
return null;
|
|
527
|
-
try {
|
|
528
|
-
const http = await startHttpServer(graph.orch, {
|
|
529
|
-
port: httpPort,
|
|
530
|
-
host: config.server.host,
|
|
531
|
-
// Canonical per-state config in workflow declaration order. The HTTP
|
|
532
|
-
// dashboard reads role from here for pill colours, declared order for
|
|
533
|
-
// the on-disk listing, and approve/discard targets — each consumer
|
|
534
|
-
// filters by role on demand. The closure reads `liveCfg.states` on
|
|
535
|
-
// every request, and the reload callback reassigns `liveCfg` to the
|
|
536
|
-
// freshly-parsed config, so a workflow reload is reflected here
|
|
537
|
-
// without rebinding the server. Phase 3 wired the equivalent for the
|
|
538
|
-
// MCP registry via `mcp.updateStates`; this view is its dashboard twin.
|
|
539
|
-
getTrackerView: () => ({
|
|
540
|
-
trackerRoot: graph.getLiveCfg().tracker.root,
|
|
541
|
-
states: Object.entries(graph.getLiveCfg().states).map(([name, cfg]) => ({
|
|
542
|
-
name,
|
|
543
|
-
role: cfg.role,
|
|
544
|
-
})),
|
|
545
|
-
workflowPath,
|
|
546
|
-
}),
|
|
547
|
-
mcp: graph.mcp,
|
|
548
|
-
tracker: graph.tracker,
|
|
549
|
-
});
|
|
550
|
-
graph.mcp.setEffectivePort(http.port);
|
|
551
|
-
return http;
|
|
552
|
-
}
|
|
553
|
-
catch (err) {
|
|
554
|
-
// `await bailStartup(...)` resolves to `never` at runtime (the helper calls
|
|
555
|
-
// process.exit), but TS doesn't propagate `Promise<never>` through `await`
|
|
556
|
-
// for unreachability — `return` is what tells the type checker this branch
|
|
557
|
-
// doesn't fall through.
|
|
558
|
-
return await bailStartup(`error: failed to bind HTTP server on ${config.server.host}:${httpPort}: ${err.message}\n`, { src });
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* MCP precondition check: with mcp.enabled (the default), every dispatch will
|
|
563
|
-
* require a reachable MCP URL. Verify NOW that one can be constructed, rather
|
|
564
|
-
* than letting each per-issue dispatch fail with the same error after VM
|
|
565
|
-
* bring-up costs are sunk. Two failure modes:
|
|
566
|
-
*
|
|
567
|
-
* 1. mcp.enabled but no HTTP listener bound — `mcp.host_url` lets an operator
|
|
568
|
-
* point the in-VM agent at a reverse proxy but symphony itself must still
|
|
569
|
-
* serve `/api/v1/issues/<id>/mcp`. Without a listener, the override would
|
|
570
|
-
* advertise a URL nothing answers.
|
|
571
|
-
* 2. mcp.enabled but the registry cannot build a URL (no port, no host_url).
|
|
572
|
-
*/
|
|
573
|
-
async function checkMcpPrecondition(opts) {
|
|
574
|
-
const { config, graph, src, http } = opts;
|
|
575
|
-
if (!config.mcp.enabled)
|
|
576
|
-
return;
|
|
577
|
-
if (http === null) {
|
|
578
|
-
await bailStartup(`error: mcp.enabled=true but no HTTP server is configured. Symphony itself\n` +
|
|
579
|
-
`must bind a listener (set --port or server.port) so it can serve the MCP\n` +
|
|
580
|
-
`endpoint, even when mcp.host_url points the in-VM agent at a reverse proxy.\n`, { src });
|
|
581
|
-
}
|
|
582
|
-
const probeUrl = graph.mcp.buildUrl('startup-check', {
|
|
583
|
-
host: config.mcp.host,
|
|
584
|
-
explicit_host_url: config.mcp.explicit_host_url,
|
|
585
|
-
});
|
|
586
|
-
if (probeUrl === null) {
|
|
587
|
-
await bailStartup(`error: mcp.enabled=true but no MCP URL can be constructed. ` +
|
|
588
|
-
`Set --port, server.port, or mcp.host_url so the in-VM agent can reach ` +
|
|
589
|
-
`the symphony MCP endpoint.\n`, { http, src });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Human-facing startup summary on stdout. Once a file sink is active (the
|
|
594
|
-
* default), this is the only orchestrator-side console output: structured
|
|
595
|
-
* `log.*` lines are routed to the log file, so the operator sees just this
|
|
596
|
-
* banner — what's running, where the dashboard is, and where the detailed log
|
|
597
|
-
* stream went. `--verbose` additionally mirrors the structured stream to the
|
|
598
|
-
* console. The companion `symphony started` structured line carries the same
|
|
599
|
-
* facts into the log file (and onto stderr under --verbose).
|
|
600
|
-
*/
|
|
601
|
-
function printStartupBanner(opts) {
|
|
602
|
-
const { workflowPath, trackerRoot, host, http, logFile } = opts;
|
|
603
|
-
// Map wildcard bind addresses to a clickable loopback host for the URL.
|
|
604
|
-
const displayHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
|
|
605
|
-
const dashboard = http === null ? '(disabled — pass --port or set server.port)' : `http://${displayHost}:${http.port}/`;
|
|
606
|
-
const logs = logFile === null ? '(disabled — structured logs on stderr)' : `${logFile} (tail -f to follow)`;
|
|
607
|
-
process.stdout.write(`symphony\n` +
|
|
608
|
-
` workflow ${workflowPath}\n` +
|
|
609
|
-
` tracker root ${trackerRoot ?? '<unset>'}\n` +
|
|
610
|
-
` dashboard ${dashboard}\n` +
|
|
611
|
-
` logs ${logs}\n`);
|
|
612
|
-
}
|
|
613
|
-
/** Per-step ceiling for each graceful-teardown step (`orch.stop`, the ACP
|
|
614
|
-
* bridge, the HTTP listener). One stuck step — a wedged VM teardown, a
|
|
615
|
-
* half-open ACP socket — times out and the remaining steps still run, so a
|
|
616
|
-
* single hang can't wedge the whole shutdown (issue 152). */
|
|
617
|
-
const SHUTDOWN_STEP_TIMEOUT_MS = 8_000;
|
|
618
|
-
/** Hard ceiling on the entire graceful path. If teardown hasn't finished by
|
|
619
|
-
* here, we stop waiting, force-kill child VMs, and exit non-zero — Ctrl+C
|
|
620
|
-
* never appears dead, even when every per-step timeout is also wedged. */
|
|
621
|
-
const SHUTDOWN_DEADLINE_MS = 15_000;
|
|
622
|
-
/** Short cap on the log-sink flush during a force-quit. The VM SIGKILL itself is
|
|
623
|
-
* synchronous, so the only thing the forced path awaits is draining symphony.log;
|
|
624
|
-
* cap it so a wedged sink can't keep a second Ctrl+C from exiting promptly. */
|
|
625
|
-
const FORCE_QUIT_FLUSH_TIMEOUT_MS = 2_000;
|
|
626
|
-
/**
|
|
627
|
-
* Race `work` against a `label`ed deadline. Rejects with a timeout Error if the
|
|
628
|
-
* deadline wins; the underlying promise is left to settle on its own (we only
|
|
629
|
-
* ever call this on the shutdown path, where the process exits shortly after).
|
|
630
|
-
* The timer is `unref`'d so it can't by itself hold the event loop open.
|
|
631
|
-
*/
|
|
632
|
-
function withTimeout(work, ms, label) {
|
|
633
|
-
return new Promise((resolve, reject) => {
|
|
634
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
635
|
-
timer.unref();
|
|
636
|
-
work.then((v) => {
|
|
637
|
-
clearTimeout(timer);
|
|
638
|
-
resolve(v);
|
|
639
|
-
}, (e) => {
|
|
640
|
-
clearTimeout(timer);
|
|
641
|
-
reject(e);
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* The graceful teardown sequence. Each step gets its own per-step timeout +
|
|
647
|
-
* catch so one wedged teardown can't starve the steps below it (the issue's
|
|
648
|
-
* `orch.stop()` no-catch/no-timeout wedge). `orch.stop()` signals every worker
|
|
649
|
-
* to unwind (each closes its VM) but does NOT await them, so the trailing
|
|
650
|
-
* `vmClient.killAllVms()` is the backstop that guarantees no orphaned qemu: a
|
|
651
|
-
* no-op once every worker has closed its VM, and a synchronous SIGKILL of any
|
|
652
|
-
* qemu still live when a slow/timed-out `orch.stop()` returns early.
|
|
653
|
-
*/
|
|
654
|
-
async function gracefulStop(deps) {
|
|
655
|
-
const { graph, http, src } = deps;
|
|
656
|
-
await withTimeout(graph.orch.stop(), SHUTDOWN_STEP_TIMEOUT_MS, 'orch.stop').catch((err) => log.warn('shutdown: orch.stop did not finish cleanly', { error: err.message }));
|
|
657
|
-
await withTimeout(graph.acpBridge.stop(), SHUTDOWN_STEP_TIMEOUT_MS, 'acpBridge.stop').catch((err) => log.warn('shutdown: acpBridge.stop did not finish cleanly', { error: err.message }));
|
|
658
|
-
graph.credentialTicker.stop();
|
|
659
|
-
if (http) {
|
|
660
|
-
await withTimeout(http.close(), SHUTDOWN_STEP_TIMEOUT_MS, 'http.close').catch((err) => log.warn('shutdown: http.close did not finish cleanly', { error: err.message }));
|
|
661
|
-
}
|
|
662
|
-
await src.stop().catch(() => undefined);
|
|
663
|
-
// Synchronous backstop: SIGKILL any qemu whose VM didn't close in time. No
|
|
664
|
-
// await — it can't hang, and a clean stop has already deregistered every VM.
|
|
665
|
-
killChildVms(graph);
|
|
666
|
-
await closeLogFile().catch(() => undefined);
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Force-quit path: SIGKILL any live child VMs (so a forced exit doesn't orphan
|
|
670
|
-
* `qemu-system-x86_64`), flush the log sink under a short cap, then exit
|
|
671
|
-
* non-zero. The kill is synchronous so a second Ctrl+C terminates immediately —
|
|
672
|
-
* the only awaited step is the bounded log flush.
|
|
673
|
-
*/
|
|
674
|
-
async function forceQuit(message, graph) {
|
|
675
|
-
process.stdout.write(`\n${message}\n`);
|
|
676
|
-
log.warn('shutdown: force-quitting — SIGKILL child VMs then exit');
|
|
677
|
-
killChildVms(graph);
|
|
678
|
-
await withTimeout(closeLogFile(), FORCE_QUIT_FLUSH_TIMEOUT_MS, 'closeLogFile').catch(() => undefined);
|
|
679
|
-
process.exit(1);
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Synchronously SIGKILL every live VM's backing qemu (issue 152). The qemu
|
|
683
|
-
* processes are direct children of this in-process orchestrator; left alone,
|
|
684
|
-
* `process.exit` reparents them to init and they survive as orphans. Bounded and
|
|
685
|
-
* best-effort — never throws — so both the graceful backstop and the force path
|
|
686
|
-
* can call it on the way out without risking a hang.
|
|
687
|
-
*/
|
|
688
|
-
function killChildVms(graph) {
|
|
689
|
-
try {
|
|
690
|
-
const killed = graph.vmClient.killAllVms();
|
|
691
|
-
if (killed > 0)
|
|
692
|
-
log.info('shutdown: SIGKILL child VMs', { count: killed });
|
|
693
|
-
}
|
|
694
|
-
catch (err) {
|
|
695
|
-
log.warn('shutdown: killAllVms threw', { error: err.message });
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Run the graceful path under a hard deadline. On a clean stop we exit 0; if the
|
|
700
|
-
* deadline elapses (teardown wedged past even the per-step timeouts) we fall
|
|
701
|
-
* through to the force-quit path so the process never hangs on Ctrl+C.
|
|
702
|
-
*/
|
|
703
|
-
async function runGracefulShutdown(signal, deps) {
|
|
704
|
-
process.stdout.write(`\nsymphony: ${signal} received — shutting down gracefully… (press Ctrl+C again to force-quit)\n`);
|
|
705
|
-
log.info('shutdown requested', { signal });
|
|
706
|
-
try {
|
|
707
|
-
await withTimeout(gracefulStop(deps), SHUTDOWN_DEADLINE_MS, 'graceful shutdown');
|
|
708
|
-
}
|
|
709
|
-
catch (err) {
|
|
710
|
-
log.warn('graceful shutdown exceeded deadline; forcing', { error: err.message });
|
|
711
|
-
await forceQuit(`symphony: graceful shutdown exceeded ${SHUTDOWN_DEADLINE_MS}ms — force-quitting; killing child VMs.`, deps.graph);
|
|
712
|
-
}
|
|
713
|
-
process.stdout.write('symphony: shutdown complete.\n');
|
|
714
|
-
process.exit(0);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Wire SIGINT/SIGTERM. The first signal runs the deadline-bounded graceful path;
|
|
718
|
-
* a SECOND signal force-quits immediately (force-killing child VMs first)
|
|
719
|
-
* instead of re-entering the same in-flight teardown (issue 152). With no
|
|
720
|
-
* console output the old graceful shutdown looked dead on a wedged VM/bridge
|
|
721
|
-
* teardown, so both paths announce on the operator's console.
|
|
722
|
-
*/
|
|
723
|
-
function installShutdownHandlers(deps) {
|
|
724
|
-
let shuttingDown = false;
|
|
725
|
-
const onSignal = (signal) => {
|
|
726
|
-
if (shuttingDown) {
|
|
727
|
-
void forceQuit(`symphony: ${signal} again — graceful shutdown is taking too long, force-quitting; killing child VMs.`, deps.graph);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
shuttingDown = true;
|
|
731
|
-
void runGracefulShutdown(signal, deps);
|
|
732
|
-
};
|
|
733
|
-
process.on('SIGINT', () => onSignal('SIGINT'));
|
|
734
|
-
process.on('SIGTERM', () => onSignal('SIGTERM'));
|
|
735
|
-
}
|
|
736
|
-
async function main() {
|
|
737
|
-
const cli = parseCli(process.argv.slice(2));
|
|
738
|
-
// --verbose / --foreground: mirror structured logs to the console even when
|
|
739
|
-
// the file sink is active. Set before any log.* call so every line honors it.
|
|
740
|
-
setLogVerbose(cli.verbose);
|
|
741
|
-
const workflowPath = path.resolve(cli.workflow);
|
|
742
|
-
await handlePreflight(cli, workflowPath);
|
|
743
|
-
const { src, config, definition, envLogFile, logFile } = await loadAndValidateConfig(workflowPath);
|
|
744
|
-
const graph = await buildOrchestratorGraph({ config, definition, src, envLogFile });
|
|
745
|
-
const { http } = await startTransports({ config, graph, cli, src, workflowPath });
|
|
746
|
-
try {
|
|
747
|
-
await graph.orch.start();
|
|
748
|
-
}
|
|
749
|
-
catch (err) {
|
|
750
|
-
await bailStartup(`startup failed: ${err.message}\n`, { http, src });
|
|
751
|
-
}
|
|
752
|
-
if (cli.reconcileForce) {
|
|
753
|
-
// `--reconcile-force`: request an immediate reconcile pass (VM/workspace/PR
|
|
754
|
-
// janitors) instead of waiting on the backstop tick. The bake artifact this
|
|
755
|
-
// flag used to invalidate is gone (the agent image is built ahead of time),
|
|
756
|
-
// so it now just triggers an eager pass.
|
|
757
|
-
log.info('reconcile --force requested');
|
|
758
|
-
void graph.orch.triggerReconcile({ force: true }).catch((err) => log.warn('reconcile --force failed', { error: err.message }));
|
|
759
|
-
}
|
|
760
|
-
log.info('symphony started', {
|
|
761
|
-
workflow: workflowPath,
|
|
762
|
-
workspace_root: config.workspace.root,
|
|
763
|
-
tracker_root: config.tracker.root,
|
|
764
|
-
log_file: logFile ?? '<disabled>',
|
|
765
|
-
poll_interval_ms: config.polling.interval_ms,
|
|
766
|
-
// Actually-bound port (differs from the requested port with --port 0); null
|
|
767
|
-
// when no HTTP listener is configured.
|
|
768
|
-
http_port: http?.port ?? null,
|
|
769
|
-
});
|
|
770
|
-
// Clean human-facing summary on stdout. With a file sink active (the default)
|
|
771
|
-
// the structured line above goes to the log file only, so this banner is what
|
|
772
|
-
// the operator sees on the console (issue 118).
|
|
773
|
-
printStartupBanner({
|
|
774
|
-
workflowPath,
|
|
775
|
-
trackerRoot: config.tracker.root,
|
|
776
|
-
host: config.server.host,
|
|
777
|
-
http,
|
|
778
|
-
logFile,
|
|
779
|
-
});
|
|
780
|
-
// Graceful shutdown is async and can stall on a wedged VM/bridge teardown
|
|
781
|
-
// (issue 152), so the handlers run the graceful path under a hard deadline,
|
|
782
|
-
// announce on the operator's console, and escalate a SECOND signal to an
|
|
783
|
-
// immediate force-quit (force-killing child VMs first) instead of re-entering
|
|
784
|
-
// the same hanging await.
|
|
785
|
-
installShutdownHandlers({ graph, http, src });
|
|
786
|
-
}
|
|
787
|
-
main().catch(async (err) => {
|
|
788
|
-
process.stderr.write(`fatal: ${err.message}\n${err.stack ?? ''}\n`);
|
|
789
|
-
// setLogFile() may have been called before main() threw; flush the sink so
|
|
790
|
-
// any log.* lines emitted before the fault reach symphony.log.
|
|
791
|
-
await closeLogFile().catch(() => undefined);
|
|
20
|
+
import { main } from '../shell/main.js';
|
|
21
|
+
// The composition root installs its own SIGINT/SIGTERM graceful-shutdown handlers
|
|
22
|
+
// and owns every process.exit on the preflight / bind-failure paths. Here we only
|
|
23
|
+
// backstop a thrown/ rejected main() — a startup failure that escaped bailStartup
|
|
24
|
+
// — with a fatal log + non-zero exit so the operator sees the cause.
|
|
25
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
26
|
+
const e = err;
|
|
27
|
+
process.stderr.write(`fatal: ${e.message}\n${e.stack ?? ''}\n`);
|
|
792
28
|
process.exit(1);
|
|
793
29
|
});
|
|
794
30
|
//# sourceMappingURL=symphony.js.map
|