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/workflow.js
DELETED
|
@@ -1,822 +0,0 @@
|
|
|
1
|
-
// WORKFLOW.md parser and typed config view (SPEC §4). Pure: no fs, no process.
|
|
2
|
-
// The on-disk read and watcher live in `./workflow-loader.ts` (shell).
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import { parseFrontMatter, FrontMatterError } from './util/frontmatter.js';
|
|
6
|
-
import { isKnownAdapter } from './agent/adapter-names.js';
|
|
7
|
-
import { parseActionsBlock } from './actions/parsing.js';
|
|
8
|
-
import { WorkflowError } from './errors.js';
|
|
9
|
-
import { log } from './logging.js';
|
|
10
|
-
export { WorkflowError };
|
|
11
|
-
// §4.2: split YAML front matter from prompt body. Thin wrapper over the shared
|
|
12
|
-
// parser that translates FrontMatterError → WorkflowError so callers keep
|
|
13
|
-
// matching on the existing error codes.
|
|
14
|
-
export function splitFrontMatter(text) {
|
|
15
|
-
let fm;
|
|
16
|
-
try {
|
|
17
|
-
fm = parseFrontMatter(text);
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
if (err instanceof FrontMatterError) {
|
|
21
|
-
const code = err.code === 'not_a_map' ? 'workflow_front_matter_not_a_map' : 'workflow_parse_error';
|
|
22
|
-
throw new WorkflowError(code, err.message);
|
|
23
|
-
}
|
|
24
|
-
throw err;
|
|
25
|
-
}
|
|
26
|
-
return { config: fm.fields, body: fm.body };
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Pure entry point: split front matter, build the typed view, and return both
|
|
30
|
-
* shapes. The shell loader reads the file from disk and the operator's env,
|
|
31
|
-
* then calls this. `env` defaults to an empty map so tests that do not exercise
|
|
32
|
-
* `$VAR` expansion need not thread it through.
|
|
33
|
-
*/
|
|
34
|
-
export function parseWorkflow(text, workflowPath, env = {}) {
|
|
35
|
-
const { config: raw, body } = splitFrontMatter(text);
|
|
36
|
-
const definition = { config: raw, prompt_template: body };
|
|
37
|
-
const config = buildServiceConfig(raw, workflowPath, env);
|
|
38
|
-
return { definition, config };
|
|
39
|
-
}
|
|
40
|
-
// $VAR / ~ expansion for path/command fields. `env` carries the variable map
|
|
41
|
-
// (the shell loader passes process.env; tests pass an explicit shape).
|
|
42
|
-
export function expandVar(value, env = {}) {
|
|
43
|
-
if (typeof value !== 'string')
|
|
44
|
-
return value;
|
|
45
|
-
let s = value;
|
|
46
|
-
if (s.startsWith('~/') || s === '~') {
|
|
47
|
-
s = s === '~' ? os.homedir() : path.join(os.homedir(), s.slice(2));
|
|
48
|
-
}
|
|
49
|
-
const m = s.match(/^\$([A-Z_][A-Z0-9_]*)$/);
|
|
50
|
-
if (m) {
|
|
51
|
-
const envVal = env[m[1]];
|
|
52
|
-
return envVal ?? '';
|
|
53
|
-
}
|
|
54
|
-
return s;
|
|
55
|
-
}
|
|
56
|
-
function asString(v) {
|
|
57
|
-
if (typeof v === 'string')
|
|
58
|
-
return v;
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
function asInt(v, fallback) {
|
|
62
|
-
if (typeof v === 'number' && Number.isFinite(v))
|
|
63
|
-
return Math.trunc(v);
|
|
64
|
-
if (typeof v === 'string' && /^-?\d+$/.test(v))
|
|
65
|
-
return parseInt(v, 10);
|
|
66
|
-
return fallback;
|
|
67
|
-
}
|
|
68
|
-
function asStringList(v, fallback) {
|
|
69
|
-
if (Array.isArray(v))
|
|
70
|
-
return v.filter((x) => typeof x === 'string');
|
|
71
|
-
return fallback;
|
|
72
|
-
}
|
|
73
|
-
function getObject(parent, key) {
|
|
74
|
-
const v = parent[key];
|
|
75
|
-
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
76
|
-
return v;
|
|
77
|
-
return {};
|
|
78
|
-
}
|
|
79
|
-
// workspace.github_repo: a GitHub `owner/repo` slug that enables push/PR mode,
|
|
80
|
-
// or null for local-only. There is no auto-detection — the value is either
|
|
81
|
-
// absent (local-only) or a literal slug. Empty string / "none"
|
|
82
|
-
// (case-insensitive) normalize to null so an operator can disable push mode
|
|
83
|
-
// without deleting the key. Any other value MUST be a GitHub owner/repo slug:
|
|
84
|
-
// `owner` is GitHub's `[alnum]`/`-` charset (no `.`, `:`, `@`, scheme, or host)
|
|
85
|
-
// and `repo` is `[alnum]`/`.`/`_`/`-`. This rejects whole-URL and SSH-style
|
|
86
|
-
// remotes (`https://github.com/foo/bar`, `git@github.com:foo/bar`) and bare
|
|
87
|
-
// names (`foo`) at parse time, so a typo can't slip through and build a broken
|
|
88
|
-
// `https://github.com/<garbage>.git` origin that fails later during setup.
|
|
89
|
-
// Takes the raw YAML value (not asString'd) so a present-but-wrong-type value
|
|
90
|
-
// (`github_repo: true`, `123`, `{}`) is rejected rather than coerced to null —
|
|
91
|
-
// otherwise the fail-fast contract has a hole that silently disables push/PR.
|
|
92
|
-
function parseGithubRepo(input) {
|
|
93
|
-
if (input === undefined || input === null)
|
|
94
|
-
return null;
|
|
95
|
-
if (typeof input !== 'string') {
|
|
96
|
-
throw new WorkflowError('workflow_parse_error', `workspace.github_repo must be a string "owner/repo" slug or "none" (got ${typeof input})`);
|
|
97
|
-
}
|
|
98
|
-
const trimmed = input.trim();
|
|
99
|
-
if (trimmed === '' || trimmed.toLowerCase() === 'none')
|
|
100
|
-
return null;
|
|
101
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
102
|
-
throw new WorkflowError('workflow_parse_error', `workspace.github_repo must be a GitHub "owner/repo" slug or "none" (got: ${input})`);
|
|
103
|
-
}
|
|
104
|
-
return trimmed;
|
|
105
|
-
}
|
|
106
|
-
// Build a fully typed ServiceConfig from a parsed front matter map. `env`
|
|
107
|
-
// supplies the variable map for `$VAR` expansion; defaults to {} so pure
|
|
108
|
-
// callers don't need to thread one in.
|
|
109
|
-
export function buildServiceConfig(raw, workflowPath, env = {}) {
|
|
110
|
-
const workflowAbs = path.resolve(workflowPath);
|
|
111
|
-
const workflowDir = path.dirname(workflowAbs);
|
|
112
|
-
// tracker (§4.3.1)
|
|
113
|
-
const trackerRaw = getObject(raw, 'tracker');
|
|
114
|
-
const trackerKind = (asString(trackerRaw['kind']) ?? '').trim();
|
|
115
|
-
// local-tracker extension: optional `tracker.root` path.
|
|
116
|
-
const trackerRootRaw = asString(trackerRaw['root']);
|
|
117
|
-
let trackerRoot = null;
|
|
118
|
-
if (trackerRootRaw) {
|
|
119
|
-
const expanded = expandVar(trackerRootRaw, env);
|
|
120
|
-
if (expanded === '') {
|
|
121
|
-
throw new WorkflowError('workflow_parse_error', `tracker.root references an unset variable: ${trackerRootRaw}`);
|
|
122
|
-
}
|
|
123
|
-
trackerRoot = path.isAbsolute(expanded) ? expanded : path.resolve(workflowDir, expanded);
|
|
124
|
-
}
|
|
125
|
-
else if (trackerKind === 'local') {
|
|
126
|
-
// Default local tracker root: <workflow-dir>/issues
|
|
127
|
-
trackerRoot = path.resolve(workflowDir, 'issues');
|
|
128
|
-
}
|
|
129
|
-
const states = parseStatesBlock(raw['states']);
|
|
130
|
-
const tracker = {
|
|
131
|
-
kind: trackerKind,
|
|
132
|
-
states,
|
|
133
|
-
root: trackerRoot,
|
|
134
|
-
};
|
|
135
|
-
// polling (§4.3.2)
|
|
136
|
-
const pollingRaw = getObject(raw, 'polling');
|
|
137
|
-
const polling = {
|
|
138
|
-
interval_ms: asInt(pollingRaw['interval_ms'], 30_000),
|
|
139
|
-
};
|
|
140
|
-
// workspace (§4.3.3)
|
|
141
|
-
const workspaceRaw = getObject(raw, 'workspace');
|
|
142
|
-
const wsRootInput = asString(workspaceRaw['root']);
|
|
143
|
-
let workspaceRoot;
|
|
144
|
-
if (wsRootInput) {
|
|
145
|
-
const expanded = expandVar(wsRootInput, env);
|
|
146
|
-
if (expanded === '') {
|
|
147
|
-
throw new WorkflowError('workflow_parse_error', `workspace.root references an unset variable: ${wsRootInput}`);
|
|
148
|
-
}
|
|
149
|
-
workspaceRoot = path.isAbsolute(expanded) ? expanded : path.resolve(workflowDir, expanded);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
workspaceRoot = path.join(os.tmpdir(), 'symphony_workspaces');
|
|
153
|
-
}
|
|
154
|
-
const baseBranchInput = asString(workspaceRaw['base_branch']);
|
|
155
|
-
const baseBranch = baseBranchInput && baseBranchInput.trim().length > 0 ? baseBranchInput.trim() : 'main';
|
|
156
|
-
const workspace = {
|
|
157
|
-
root: path.resolve(workspaceRoot),
|
|
158
|
-
github_repo: parseGithubRepo(workspaceRaw['github_repo']),
|
|
159
|
-
base_branch: baseBranch,
|
|
160
|
-
};
|
|
161
|
-
// logs (symphony extension): per-issue JSONL run logs. Default sits next to the workspace
|
|
162
|
-
// root under `.symphony/logs/` so all symphony-managed state for a project lives in one
|
|
163
|
-
// tree. Same expansion rules as workspace.root.
|
|
164
|
-
const logsRaw = getObject(raw, 'logs');
|
|
165
|
-
const logsRootInput = asString(logsRaw['root']);
|
|
166
|
-
let logsRoot;
|
|
167
|
-
if (logsRootInput) {
|
|
168
|
-
const expanded = expandVar(logsRootInput, env);
|
|
169
|
-
if (expanded === '') {
|
|
170
|
-
throw new WorkflowError('workflow_parse_error', `logs.root references an unset variable: ${logsRootInput}`);
|
|
171
|
-
}
|
|
172
|
-
logsRoot = path.isAbsolute(expanded) ? expanded : path.resolve(workflowDir, expanded);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
logsRoot = path.resolve(workflowDir, '.symphony', 'logs');
|
|
176
|
-
}
|
|
177
|
-
const logs = { root: path.resolve(logsRoot) };
|
|
178
|
-
// agent (§4.3.4)
|
|
179
|
-
const agentRaw = getObject(raw, 'agent');
|
|
180
|
-
const maxTurns = asInt(agentRaw['max_turns'], 20);
|
|
181
|
-
if (maxTurns <= 0) {
|
|
182
|
-
throw new WorkflowError('workflow_parse_error', 'agent.max_turns must be positive');
|
|
183
|
-
}
|
|
184
|
-
// Memory-aware admission cap (issue 27). Default-on with a 2 GiB host reserve — that's
|
|
185
|
-
// enough headroom for the orchestrator process, the per-VM Gondolin runners,
|
|
186
|
-
// and the kernel's working set on a typical workstation. Operators can disable the cap (set
|
|
187
|
-
// `memory_admission_enabled: false`) on hosts that don't expose /proc/meminfo or where
|
|
188
|
-
// the static cap is already the binding constraint.
|
|
189
|
-
const memAdmissionEnabledRaw = agentRaw['memory_admission_enabled'];
|
|
190
|
-
const memoryAdmissionEnabled = memAdmissionEnabledRaw === undefined ? true : memAdmissionEnabledRaw !== false;
|
|
191
|
-
const hostMemoryReserveMib = asInt(agentRaw['host_memory_reserve_mib'], 2048);
|
|
192
|
-
if (hostMemoryReserveMib < 0) {
|
|
193
|
-
throw new WorkflowError('workflow_parse_error', 'agent.host_memory_reserve_mib must be a non-negative integer');
|
|
194
|
-
}
|
|
195
|
-
// Circuit breaker (issue 128). Default 5: after five consecutive identical
|
|
196
|
-
// failures the orchestrator stops retrying and routes the issue to a holding
|
|
197
|
-
// state. 0 disables the breaker; 1 would trip on the first failure (no retry
|
|
198
|
-
// ever), which is rarely wanted, so the parser rejects it as a likely
|
|
199
|
-
// misconfiguration — use 0 to disable or >= 2 to bound the loop.
|
|
200
|
-
const circuitBreakerThreshold = asInt(agentRaw['circuit_breaker_threshold'], 5);
|
|
201
|
-
if (circuitBreakerThreshold < 0 || circuitBreakerThreshold === 1) {
|
|
202
|
-
throw new WorkflowError('workflow_parse_error', 'agent.circuit_breaker_threshold must be 0 (disabled) or an integer >= 2');
|
|
203
|
-
}
|
|
204
|
-
const agent = {
|
|
205
|
-
max_concurrent_agents: asInt(agentRaw['max_concurrent_agents'], 10),
|
|
206
|
-
max_turns: maxTurns,
|
|
207
|
-
max_retry_backoff_ms: asInt(agentRaw['max_retry_backoff_ms'], 300_000),
|
|
208
|
-
memory_admission_enabled: memoryAdmissionEnabled,
|
|
209
|
-
host_memory_reserve_mib: hostMemoryReserveMib,
|
|
210
|
-
circuit_breaker_threshold: circuitBreakerThreshold,
|
|
211
|
-
};
|
|
212
|
-
// acp (Symphony extension; see §4.3.5). `adapter` selects
|
|
213
|
-
// one of symphony's known profiles (claude, codex, opencode); symphony auto-derives the
|
|
214
|
-
// launch command from the adapter profile. Credentials are NOT staged into the workspace:
|
|
215
|
-
// the guest only ever holds a token-shaped placeholder, and the host substitutes the real
|
|
216
|
-
// upstream token into the outbound request at Gondolin egress (TLS-MITM via
|
|
217
|
-
// `createHttpHooks` in src/agent/credential-secrets.ts). The real host credential
|
|
218
|
-
// (`~/.claude/.credentials.json` for claude; `~/.codex/auth.json` access token or
|
|
219
|
-
// `OPENAI_API_KEY` for codex; the GitHub Copilot token exchanged from
|
|
220
|
-
// `~/.local/share/opencode/auth.json` for opencode) never enters the VM.
|
|
221
|
-
//
|
|
222
|
-
// `acp.bridge` configures the host-side TCP listener that the in-VM agent dials back
|
|
223
|
-
// to for ACP traffic. The bridge replaced the earlier in-VM-exec stdio path; see
|
|
224
|
-
// src/acp-bridge.ts for rationale.
|
|
225
|
-
const acpRaw = getObject(raw, 'acp');
|
|
226
|
-
const bridgeRaw = getObject(acpRaw, 'bridge');
|
|
227
|
-
const modelRaw = asString(acpRaw['model']);
|
|
228
|
-
const modelTrimmed = modelRaw === null ? null : modelRaw.trim();
|
|
229
|
-
const effortRaw = asString(acpRaw['effort']);
|
|
230
|
-
const effortTrimmed = effortRaw === null ? null : effortRaw.trim();
|
|
231
|
-
const acp = {
|
|
232
|
-
adapter: asString(acpRaw['adapter']) ?? 'claude',
|
|
233
|
-
model: modelTrimmed && modelTrimmed.length > 0 ? modelTrimmed : null,
|
|
234
|
-
effort: effortTrimmed && effortTrimmed.length > 0 ? effortTrimmed : null,
|
|
235
|
-
shell: asString(acpRaw['shell']) ?? 'bash',
|
|
236
|
-
prompt_timeout_ms: asInt(acpRaw['prompt_timeout_ms'], 3_600_000),
|
|
237
|
-
read_timeout_ms: asInt(acpRaw['read_timeout_ms'], 30_000),
|
|
238
|
-
stall_timeout_ms: asInt(acpRaw['stall_timeout_ms'], 300_000),
|
|
239
|
-
bridge: {
|
|
240
|
-
bind_host: asString(bridgeRaw['bind_host']) ?? '0.0.0.0',
|
|
241
|
-
bind_port: asInt(bridgeRaw['bind_port'], 8788),
|
|
242
|
-
reach_host: asString(bridgeRaw['reach_host']) ?? '127.0.0.1',
|
|
243
|
-
reach_url: asString(bridgeRaw['reach_url']),
|
|
244
|
-
connect_timeout_ms: asInt(bridgeRaw['connect_timeout_ms'], 30_000),
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
// credentials extension (issue 113). Defaults work out of the box for the
|
|
248
|
-
// common case: run the host ticker every 6 hours.
|
|
249
|
-
const credentialsRaw = getObject(raw, 'credentials');
|
|
250
|
-
const credentials = {
|
|
251
|
-
ticker_interval_ms: asInt(credentialsRaw['ticker_interval_ms'], 6 * 60 * 60 * 1000),
|
|
252
|
-
};
|
|
253
|
-
// gondolin VM extension
|
|
254
|
-
const gondolinRaw = getObject(raw, 'gondolin');
|
|
255
|
-
const volumesRaw = gondolinRaw['volumes'];
|
|
256
|
-
const volumes = Array.isArray(volumesRaw)
|
|
257
|
-
? volumesRaw.flatMap((v) => {
|
|
258
|
-
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
259
|
-
return [];
|
|
260
|
-
const m = v;
|
|
261
|
-
const hostRaw = asString(m['host']);
|
|
262
|
-
const guest = asString(m['guest']);
|
|
263
|
-
if (!hostRaw || !guest)
|
|
264
|
-
return [];
|
|
265
|
-
const expandedHost = expandVar(hostRaw, env);
|
|
266
|
-
if (expandedHost === '')
|
|
267
|
-
return [];
|
|
268
|
-
const host = path.isAbsolute(expandedHost)
|
|
269
|
-
? expandedHost
|
|
270
|
-
: path.resolve(workflowDir, expandedHost);
|
|
271
|
-
const readonly = m['readonly'] === true;
|
|
272
|
-
return [{ host, guest, readonly }];
|
|
273
|
-
})
|
|
274
|
-
: [];
|
|
275
|
-
const gondolin = {
|
|
276
|
-
image: asString(gondolinRaw['image']),
|
|
277
|
-
cpus: asInt(gondolinRaw['cpus'], 2),
|
|
278
|
-
mem_mib: asInt(gondolinRaw['mem_mib'], 2048),
|
|
279
|
-
volumes,
|
|
280
|
-
// `forward_env` is forwarded into the VM boot env, but the runner strips EVERY
|
|
281
|
-
// credential-bearing var (`stripCredentialEnv`) before boot — the guest holds
|
|
282
|
-
// only the token-shaped placeholder Gondolin substitutes at egress, never a real
|
|
283
|
-
// key. So even if an operator lists `OPENAI_API_KEY` here, it never reaches a VM.
|
|
284
|
-
// The defaults are retained for any future forward-env-strategy adapter.
|
|
285
|
-
forward_env: asStringList(gondolinRaw['forward_env'], [
|
|
286
|
-
'OPENAI_API_KEY',
|
|
287
|
-
'ANTHROPIC_API_KEY',
|
|
288
|
-
]),
|
|
289
|
-
};
|
|
290
|
-
// egress firewall: the general dev-tooling allowlist the in-VM agent may reach
|
|
291
|
-
// (npm/git/CDNs) for gates. DISTINCT from the credential layer's per-adapter
|
|
292
|
-
// substitution hosts — nothing listed here ever gets a real token substituted
|
|
293
|
-
// (see credential-secrets.ts buildAdapterHooksConfig). Empty default: the agent
|
|
294
|
-
// can reach only each adapter's inference host until the operator opts hosts in.
|
|
295
|
-
const egressRaw = getObject(raw, 'egress');
|
|
296
|
-
const egress = {
|
|
297
|
-
allowed_hosts: asStringList(egressRaw['allowed_hosts'], []),
|
|
298
|
-
};
|
|
299
|
-
// server extension (§9.5)
|
|
300
|
-
const serverRaw = getObject(raw, 'server');
|
|
301
|
-
const server = {
|
|
302
|
-
port: typeof serverRaw['port'] === 'number' ? serverRaw['port'] : null,
|
|
303
|
-
host: asString(serverRaw['host']) ?? '127.0.0.1',
|
|
304
|
-
};
|
|
305
|
-
// mcp extension: per-issue MCP server (transition + request_human_steering + propose_issue
|
|
306
|
-
// tools) injected into each ACP session. `host` defaults to the QEMU slirp gateway; the port is the
|
|
307
|
-
// actually-bound HTTP server's port (resolved at runtime, not config-parse time, so
|
|
308
|
-
// `--port` and an unset server.port can never desync). `host_url` is an explicit full-URL
|
|
309
|
-
// override for cases where the VM can't reach the orchestrator via the host gateway.
|
|
310
|
-
const mcpRaw = getObject(raw, 'mcp');
|
|
311
|
-
const mcpEnabledRaw = mcpRaw['enabled'];
|
|
312
|
-
const mcpEnabled = mcpEnabledRaw === undefined ? true : mcpEnabledRaw !== false;
|
|
313
|
-
const mcp = {
|
|
314
|
-
enabled: mcpEnabled,
|
|
315
|
-
// 127.0.0.1 works because Gondolin maps a synthetic guest host to the host's
|
|
316
|
-
// loopback (`tcp.hosts`). (Empirically verified;
|
|
317
|
-
// 10.0.2.2 — the QEMU slirp gateway — is NOT reachable here.) Other VMMs
|
|
318
|
-
// can override via the `host` field in the WORKFLOW.md mcp block.
|
|
319
|
-
host: asString(mcpRaw['host']) ?? '127.0.0.1',
|
|
320
|
-
explicit_host_url: asString(mcpRaw['host_url']),
|
|
321
|
-
};
|
|
322
|
-
// pr (issue 38, slimmed in issue 139). Optional block; default off. The slim
|
|
323
|
-
// host-global engine toggle: `pr: { enabled, poll_interval_ms }`. The
|
|
324
|
-
// merge/close/route targets and auto-merge strategy live ON the terminal
|
|
325
|
-
// states they describe (`states.<name>.pr`, parsed in parseStatesBlock) and
|
|
326
|
-
// are derived by scanning states (`derivePrRouting`), never named here.
|
|
327
|
-
const prRaw = getObject(raw, 'pr');
|
|
328
|
-
const pr = {
|
|
329
|
-
enabled: prRaw['enabled'] === true,
|
|
330
|
-
poll_interval_ms: asInt(prRaw['poll_interval_ms'], 30_000),
|
|
331
|
-
};
|
|
332
|
-
if (pr.poll_interval_ms < 0) {
|
|
333
|
-
throw new WorkflowError('workflow_parse_error', 'pr.poll_interval_ms must be non-negative');
|
|
334
|
-
}
|
|
335
|
-
// sleep_cycle (issue 125; retired in issue 140). The auto-arm trigger moved
|
|
336
|
-
// ONTO the active state it arms (`states.<name>.arm`, parsed in
|
|
337
|
-
// parseStatesBlock). A legacy top-level `sleep_cycle:` block is folded onto the
|
|
338
|
-
// reflect_state it named (foldLegacySleepCycle) for one release with a
|
|
339
|
-
// deprecation warning; there is no top-level sleep-cycle field on the resulting
|
|
340
|
-
// ServiceConfig anymore — the orchestrator derives the armed state by scanning
|
|
341
|
-
// states (`deriveArmRouting`).
|
|
342
|
-
foldLegacySleepCycle(states, getObject(raw, 'sleep_cycle'), Object.prototype.hasOwnProperty.call(raw, 'sleep_cycle'));
|
|
343
|
-
return {
|
|
344
|
-
workflow_path: workflowAbs,
|
|
345
|
-
workflow_dir: workflowDir,
|
|
346
|
-
tracker,
|
|
347
|
-
polling,
|
|
348
|
-
workspace,
|
|
349
|
-
logs,
|
|
350
|
-
agent,
|
|
351
|
-
acp,
|
|
352
|
-
gondolin,
|
|
353
|
-
egress,
|
|
354
|
-
server,
|
|
355
|
-
mcp,
|
|
356
|
-
pr,
|
|
357
|
-
credentials,
|
|
358
|
-
states,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Per-state `arm:` block (issue 140). Optional, valid on an active state.
|
|
363
|
-
* `issue` is the recurring issue armed into this state; `from` the holding state
|
|
364
|
-
* it rests in between runs; `on_idle` / `after_terminal` are the two triggers.
|
|
365
|
-
* The structural shape (types, `from` non-empty, `after_terminal` non-negative)
|
|
366
|
-
* is validated here; the cross-reference (arm only on active states, `from` is a
|
|
367
|
-
* declared holding state, `issue` required, at most one armed state) lives in
|
|
368
|
-
* `validateStates`. Returns `undefined` when the block is absent. `on_idle`
|
|
369
|
-
* defaults to false (opt-in, symmetric with `after_terminal: 0`).
|
|
370
|
-
*/
|
|
371
|
-
function parseStateArmBlock(stateName, raw) {
|
|
372
|
-
if (raw === undefined || raw === null)
|
|
373
|
-
return undefined;
|
|
374
|
-
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
375
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": arm must be a map (issue / from / on_idle / after_terminal)`);
|
|
376
|
-
}
|
|
377
|
-
const m = raw;
|
|
378
|
-
const issueRaw = asString(m['issue']);
|
|
379
|
-
const issue = issueRaw && issueRaw.trim().length > 0 ? issueRaw.trim() : null;
|
|
380
|
-
const fromRaw = asString(m['from']);
|
|
381
|
-
if (!fromRaw || fromRaw.trim().length === 0) {
|
|
382
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": arm.from must be a non-empty holding state name`);
|
|
383
|
-
}
|
|
384
|
-
const afterTerminal = asInt(m['after_terminal'], 0);
|
|
385
|
-
if (afterTerminal < 0) {
|
|
386
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": arm.after_terminal must be a non-negative integer (0 disables the terminal-count trigger)`);
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
issue,
|
|
390
|
-
from: fromRaw.trim(),
|
|
391
|
-
on_idle: m['on_idle'] === true,
|
|
392
|
-
after_terminal: afterTerminal,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
export function deriveArmRouting(states) {
|
|
396
|
-
for (const [name, sc] of Object.entries(states)) {
|
|
397
|
-
if (sc.role !== 'active' || !sc.arm)
|
|
398
|
-
continue;
|
|
399
|
-
return {
|
|
400
|
-
armState: name,
|
|
401
|
-
issue: sc.arm.issue,
|
|
402
|
-
from: sc.arm.from,
|
|
403
|
-
onIdle: sc.arm.on_idle,
|
|
404
|
-
afterTerminal: sc.arm.after_terminal,
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
return { armState: null, issue: null, from: null, onIdle: false, afterTerminal: 0 };
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Fold a deprecated top-level `sleep_cycle:` block onto the active state it named
|
|
411
|
-
* (issue 140). Mirrors the now-removed legacy PR-autopilot fold: the trigger
|
|
412
|
-
* that used to live as named strings (`reflect_state` / `dormant_state` / `issue_id` /
|
|
413
|
-
* `arm_on_idle` / `arm_after_done`) is injected onto the reflect_state's `arm:`
|
|
414
|
-
* field so the state scan (`deriveArmRouting`) is the single runtime source of
|
|
415
|
-
* truth. A state that already declares `arm:` wins on conflict. Emits one
|
|
416
|
-
* deprecation warning whenever the block is present. Only folds when the legacy
|
|
417
|
-
* block is `enabled: true` — in the new model declaring `arm:` IS the enable, so
|
|
418
|
-
* a disabled legacy block produces no arm. The legacy `arm_on_idle` default of
|
|
419
|
-
* true is preserved (`!== false`). A `reflect_state` matching no declared state
|
|
420
|
-
* is a silent no-op (the warning already points at the new shape).
|
|
421
|
-
*/
|
|
422
|
-
function foldLegacySleepCycle(states, legacyRaw, present) {
|
|
423
|
-
if (!present)
|
|
424
|
-
return;
|
|
425
|
-
log.warn('sleep_cycle: is deprecated; declare the auto-arm trigger on the active state it arms as `states.<name>.arm` (issue / from / on_idle / after_terminal)', {});
|
|
426
|
-
// Preserve the original sleep-cycle parser's non-negative validation: a
|
|
427
|
-
// negative `arm_after_done` was a parse error, not a silent disable. Validated
|
|
428
|
-
// whenever the block is present (independent of `enabled`), matching the
|
|
429
|
-
// retired `validateSleepCycle` and the new `arm.after_terminal` parser.
|
|
430
|
-
const afterTerminal = asInt(legacyRaw['arm_after_done'], 0);
|
|
431
|
-
if (afterTerminal < 0) {
|
|
432
|
-
throw new WorkflowError('workflow_parse_error', 'sleep_cycle.arm_after_done must be a non-negative integer (0 disables the terminal-count trigger)');
|
|
433
|
-
}
|
|
434
|
-
if (legacyRaw['enabled'] !== true)
|
|
435
|
-
return;
|
|
436
|
-
const reflectRaw = asString(legacyRaw['reflect_state']);
|
|
437
|
-
const reflectState = reflectRaw && reflectRaw.trim().length > 0 ? reflectRaw.trim() : 'Reflect';
|
|
438
|
-
const dormantRaw = asString(legacyRaw['dormant_state']);
|
|
439
|
-
const dormantState = dormantRaw && dormantRaw.trim().length > 0 ? dormantRaw.trim() : 'Dormant';
|
|
440
|
-
const issueRaw = asString(legacyRaw['issue_id']);
|
|
441
|
-
const issue = issueRaw && issueRaw.trim().length > 0 ? issueRaw.trim() : null;
|
|
442
|
-
injectStateArm(states, reflectState, {
|
|
443
|
-
issue,
|
|
444
|
-
from: dormantState,
|
|
445
|
-
on_idle: legacyRaw['arm_on_idle'] !== false,
|
|
446
|
-
after_terminal: afterTerminal,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Inject a derived `arm:` block onto the (case-insensitively) named active state,
|
|
451
|
-
* but only when that state declares no `arm:` of its own — state-level config
|
|
452
|
-
* always wins over a folded legacy value. No-op when the name matches no state.
|
|
453
|
-
*/
|
|
454
|
-
function injectStateArm(states, name, arm) {
|
|
455
|
-
const lower = name.toLowerCase();
|
|
456
|
-
for (const [stateName, sc] of Object.entries(states)) {
|
|
457
|
-
if (stateName.toLowerCase() !== lower)
|
|
458
|
-
continue;
|
|
459
|
-
if (sc.arm)
|
|
460
|
-
return;
|
|
461
|
-
sc.arm = arm;
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// Parse the top-level `states:` block. The block is mandatory: every workflow
|
|
466
|
-
// must declare at least one `active`, one `terminal`, and one `holding` state
|
|
467
|
-
// (validation happens in `validateStates`). Insertion order matters —
|
|
468
|
-
// downstream consumers (dashboard, role-filtered active/terminal listings)
|
|
469
|
-
// follow declaration order — so we build a plain object incrementally rather
|
|
470
|
-
// than reconstructing via `Object.fromEntries`.
|
|
471
|
-
function parseStatesBlock(raw) {
|
|
472
|
-
if (raw === undefined || raw === null) {
|
|
473
|
-
throw new WorkflowError('workflow_parse_error', 'workflow YAML must declare a top-level `states:` block with at least one active, one terminal, and one holding state. See WORKFLOW.template.md for the schema.');
|
|
474
|
-
}
|
|
475
|
-
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
476
|
-
throw new WorkflowError('workflow_parse_error', 'states: must be a map of name → config');
|
|
477
|
-
}
|
|
478
|
-
const entries = Object.entries(raw);
|
|
479
|
-
if (entries.length === 0) {
|
|
480
|
-
throw new WorkflowError('workflow_parse_error', 'workflow YAML `states:` block is empty; declare at least one active, one terminal, and one holding state. See WORKFLOW.template.md for the schema.');
|
|
481
|
-
}
|
|
482
|
-
const out = {};
|
|
483
|
-
for (const [name, value] of entries) {
|
|
484
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
485
|
-
throw new WorkflowError('workflow_parse_error', `state "${name}": value must be a map`);
|
|
486
|
-
}
|
|
487
|
-
const m = value;
|
|
488
|
-
const roleRaw = asString(m['role']);
|
|
489
|
-
if (roleRaw !== 'active' && roleRaw !== 'terminal' && roleRaw !== 'holding') {
|
|
490
|
-
throw new WorkflowError('workflow_parse_error', `state "${name}": role must be one of active|terminal|holding (got: ${String(m['role'])})`);
|
|
491
|
-
}
|
|
492
|
-
const adapter = asString(m['adapter']);
|
|
493
|
-
const modelRaw = asString(m['model']);
|
|
494
|
-
const modelTrimmed = modelRaw === null ? undefined : modelRaw.trim();
|
|
495
|
-
const model = modelTrimmed === undefined ? undefined : modelTrimmed.length > 0 ? modelTrimmed : null;
|
|
496
|
-
// Same undefined-vs-null semantics as `model`: a missing key inherits the
|
|
497
|
-
// workflow-level `acp.effort`; a blank/whitespace string normalizes to null
|
|
498
|
-
// (an explicit "use the adapter default for this state" signal).
|
|
499
|
-
const effortRaw = asString(m['effort']);
|
|
500
|
-
const effortTrimmed = effortRaw === null ? undefined : effortRaw.trim();
|
|
501
|
-
const effort = effortTrimmed === undefined ? undefined : effortTrimmed.length > 0 ? effortTrimmed : null;
|
|
502
|
-
let maxTurns;
|
|
503
|
-
if (m['max_turns'] !== undefined) {
|
|
504
|
-
const n = asInt(m['max_turns'], -1);
|
|
505
|
-
if (n <= 0) {
|
|
506
|
-
throw new WorkflowError('workflow_parse_error', `state "${name}": max_turns must be a positive integer`);
|
|
507
|
-
}
|
|
508
|
-
maxTurns = n;
|
|
509
|
-
}
|
|
510
|
-
// Per-state concurrency cap (issue 137) — same positive-integer validation
|
|
511
|
-
// as max_turns. Undefined when omitted (no per-state cap; only the global
|
|
512
|
-
// agent.max_concurrent_agents ceiling applies).
|
|
513
|
-
let maxConcurrent;
|
|
514
|
-
if (m['max_concurrent'] !== undefined) {
|
|
515
|
-
const n = asInt(m['max_concurrent'], -1);
|
|
516
|
-
if (n <= 0) {
|
|
517
|
-
throw new WorkflowError('workflow_parse_error', `state "${name}": max_concurrent must be a positive integer`);
|
|
518
|
-
}
|
|
519
|
-
maxConcurrent = n;
|
|
520
|
-
}
|
|
521
|
-
let allowed;
|
|
522
|
-
if (m['allowed_transitions'] === undefined) {
|
|
523
|
-
allowed = undefined;
|
|
524
|
-
}
|
|
525
|
-
else if (m['allowed_transitions'] === null) {
|
|
526
|
-
allowed = null;
|
|
527
|
-
}
|
|
528
|
-
else if (Array.isArray(m['allowed_transitions'])) {
|
|
529
|
-
allowed = m['allowed_transitions'].filter((x) => typeof x === 'string');
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
throw new WorkflowError('workflow_parse_error', `state "${name}": allowed_transitions must be a list of state names (or null/omitted)`);
|
|
533
|
-
}
|
|
534
|
-
const stateActions = parseActionsBlock(name, m['actions']);
|
|
535
|
-
// eval_mode is a strict boolean opt-in: only true enables it, any other
|
|
536
|
-
// value (including undefined, null, "true" string) leaves it off. Strict
|
|
537
|
-
// typing here matches the rest of the YAML-flag plumbing in the parser
|
|
538
|
-
// and stops a YAML-quoting accident ("true") from silently enabling the
|
|
539
|
-
// mounts.
|
|
540
|
-
const evalModeRaw = m['eval_mode'];
|
|
541
|
-
if (evalModeRaw !== undefined && typeof evalModeRaw !== 'boolean') {
|
|
542
|
-
throw new WorkflowError('workflow_parse_error', `state "${name}": eval_mode must be a boolean (true/false)`);
|
|
543
|
-
}
|
|
544
|
-
const statePr = parseStatePrBlock(name, m['pr']);
|
|
545
|
-
const stateArm = parseStateArmBlock(name, m['arm']);
|
|
546
|
-
const sc = { role: roleRaw };
|
|
547
|
-
if (adapter !== null)
|
|
548
|
-
sc.adapter = adapter;
|
|
549
|
-
if (model !== undefined)
|
|
550
|
-
sc.model = model;
|
|
551
|
-
if (effort !== undefined)
|
|
552
|
-
sc.effort = effort;
|
|
553
|
-
if (maxTurns !== undefined)
|
|
554
|
-
sc.max_turns = maxTurns;
|
|
555
|
-
if (maxConcurrent !== undefined)
|
|
556
|
-
sc.max_concurrent = maxConcurrent;
|
|
557
|
-
if (allowed !== undefined)
|
|
558
|
-
sc.allowed_transitions = allowed;
|
|
559
|
-
if (stateActions !== undefined)
|
|
560
|
-
sc.actions = stateActions;
|
|
561
|
-
if (evalModeRaw === true)
|
|
562
|
-
sc.eval_mode = true;
|
|
563
|
-
if (statePr !== undefined)
|
|
564
|
-
sc.pr = statePr;
|
|
565
|
-
if (stateArm !== undefined)
|
|
566
|
-
sc.arm = stateArm;
|
|
567
|
-
out[name] = sc;
|
|
568
|
-
}
|
|
569
|
-
return out;
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Per-state `pr:` block (issue 139). Optional, valid on a terminal state.
|
|
573
|
-
* `auto_merge` (squash|merge|rebase) marks the merge state and picks the
|
|
574
|
-
* `gh pr merge --auto` strategy; `on_conflict.route_to` names the active state
|
|
575
|
-
* a non-mergeable PR is routed back into; `close: true` marks the close state.
|
|
576
|
-
* Returns `undefined` when the block is absent or declares nothing meaningful
|
|
577
|
-
* (so an empty `pr: {}` doesn't shadow a legacy fold). The structural shape is
|
|
578
|
-
* validated here; the cross-reference (route_to is a declared state, pr only on
|
|
579
|
-
* terminal states, merge/close uniqueness) is in `validateStates`.
|
|
580
|
-
*/
|
|
581
|
-
function parseStatePrBlock(stateName, raw) {
|
|
582
|
-
if (raw === undefined || raw === null)
|
|
583
|
-
return undefined;
|
|
584
|
-
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
585
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr must be a map (auto_merge / on_conflict / close)`);
|
|
586
|
-
}
|
|
587
|
-
const m = raw;
|
|
588
|
-
const out = {};
|
|
589
|
-
if (m['auto_merge'] !== undefined && m['auto_merge'] !== null) {
|
|
590
|
-
const s = asString(m['auto_merge']);
|
|
591
|
-
if (s !== 'squash' && s !== 'merge' && s !== 'rebase') {
|
|
592
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.auto_merge must be one of squash|merge|rebase`);
|
|
593
|
-
}
|
|
594
|
-
out.auto_merge = s;
|
|
595
|
-
}
|
|
596
|
-
if (m['on_conflict'] !== undefined && m['on_conflict'] !== null) {
|
|
597
|
-
const oc = m['on_conflict'];
|
|
598
|
-
if (typeof oc !== 'object' || Array.isArray(oc)) {
|
|
599
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.on_conflict must be a map with a route_to field`);
|
|
600
|
-
}
|
|
601
|
-
const routeTo = asString(oc['route_to']);
|
|
602
|
-
if (!routeTo || routeTo.trim().length === 0) {
|
|
603
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.on_conflict.route_to must be a non-empty state name`);
|
|
604
|
-
}
|
|
605
|
-
out.on_conflict = { route_to: routeTo.trim() };
|
|
606
|
-
}
|
|
607
|
-
if (m['close'] !== undefined && m['close'] !== null) {
|
|
608
|
-
if (typeof m['close'] !== 'boolean') {
|
|
609
|
-
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.close must be a boolean`);
|
|
610
|
-
}
|
|
611
|
-
if (m['close'] === true)
|
|
612
|
-
out.close = true;
|
|
613
|
-
}
|
|
614
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
615
|
-
}
|
|
616
|
-
export function derivePrRouting(states) {
|
|
617
|
-
let mergeState = null;
|
|
618
|
-
let closeState = null;
|
|
619
|
-
let conflictRouteTo = null;
|
|
620
|
-
let strategy = 'squash';
|
|
621
|
-
for (const [name, sc] of Object.entries(states)) {
|
|
622
|
-
if (sc.role !== 'terminal' || !sc.pr)
|
|
623
|
-
continue;
|
|
624
|
-
if (sc.pr.auto_merge && mergeState === null) {
|
|
625
|
-
mergeState = name;
|
|
626
|
-
strategy = sc.pr.auto_merge;
|
|
627
|
-
conflictRouteTo = sc.pr.on_conflict?.route_to ?? null;
|
|
628
|
-
}
|
|
629
|
-
if (sc.pr.close && closeState === null) {
|
|
630
|
-
closeState = name;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return { mergeState, closeState, conflictRouteTo, strategy };
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Resolve the typed action list a given state should run on transition-in.
|
|
637
|
-
* Case-insensitive state lookup; returns the parsed `WorkflowAction[]` (or
|
|
638
|
-
* undefined when the state has no actions block). The runner consults this on
|
|
639
|
-
* transition into a terminal state to drive the push/PR handoff.
|
|
640
|
-
*/
|
|
641
|
-
export function resolveActionsForState(cfg, stateName) {
|
|
642
|
-
const states = cfg.states;
|
|
643
|
-
let key = null;
|
|
644
|
-
if (Object.prototype.hasOwnProperty.call(states, stateName)) {
|
|
645
|
-
key = stateName;
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
const lower = stateName.toLowerCase();
|
|
649
|
-
for (const name of Object.keys(states)) {
|
|
650
|
-
if (name.toLowerCase() === lower) {
|
|
651
|
-
key = name;
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (key === null)
|
|
657
|
-
return undefined;
|
|
658
|
-
return states[key].actions;
|
|
659
|
-
}
|
|
660
|
-
// Dispatch preflight validation (structural, pure). The fs-touching probes —
|
|
661
|
-
// `tracker.root` existence and the adapter credential files — live in the shell
|
|
662
|
-
// loader's `validateDispatchIo`, which the orchestrator calls alongside this
|
|
663
|
-
// function. Both adapters are startup-probed: claude requires a single readable
|
|
664
|
-
// host file (`~/.claude/.credentials.json`); codex passes when either
|
|
665
|
-
// `~/.codex/auth.json` holds a token (ChatGPT-OAuth `tokens.access_token` or a
|
|
666
|
-
// top-level `OPENAI_API_KEY`) or the host `OPENAI_API_KEY` env var is set. Keeping this
|
|
667
|
-
// structural half pure means tests and the reload tick can re-run it cheaply on
|
|
668
|
-
// every reconcile without re-hitting the disk.
|
|
669
|
-
export function validateDispatch(cfg) {
|
|
670
|
-
if (cfg.tracker.kind !== 'local') {
|
|
671
|
-
return `unsupported_tracker_kind: ${cfg.tracker.kind || '<missing>'}`;
|
|
672
|
-
}
|
|
673
|
-
if (!cfg.tracker.root)
|
|
674
|
-
return 'tracker.root must be set for local tracker';
|
|
675
|
-
// `cfg.states` is always populated by buildServiceConfig — the parser refuses
|
|
676
|
-
// workflows without a `states:` block — so callers never need a fallback here.
|
|
677
|
-
const statesError = validateStates(cfg.states);
|
|
678
|
-
if (statesError)
|
|
679
|
-
return statesError;
|
|
680
|
-
// cfg.agent is always populated by buildServiceConfig; guard for legacy
|
|
681
|
-
// hand-built ServiceConfigs (older test fixtures) that omit the block.
|
|
682
|
-
const concurrencyError = validateConcurrencyCaps(cfg.states, cfg.agent?.max_concurrent_agents);
|
|
683
|
-
if (concurrencyError)
|
|
684
|
-
return concurrencyError;
|
|
685
|
-
if (!isKnownAdapter(cfg.acp.adapter)) {
|
|
686
|
-
return `acp.adapter "${cfg.acp.adapter}" is not a known profile; use one of: claude, codex, opencode`;
|
|
687
|
-
}
|
|
688
|
-
// PR autopilot routing (issue 139) is validated structurally inside
|
|
689
|
-
// `validateStates` (pr: only on terminal states; on_conflict.route_to must be
|
|
690
|
-
// a declared state; at most one merge/close state). The state's own `role` is
|
|
691
|
-
// authoritative, so the old `validatePrAutopilot` role re-validator is gone.
|
|
692
|
-
// Auto-arm trigger (issue 140) is validated structurally inside
|
|
693
|
-
// `validateStates` (arm: only on active states; arm.from must be a declared
|
|
694
|
-
// holding state; arm.issue required; at most one armed state). The state's own
|
|
695
|
-
// `role` is authoritative, so the old `validateSleepCycle` role re-validator is
|
|
696
|
-
// gone.
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Validate that the sum of per-state `max_concurrent` caps does not exceed the
|
|
701
|
-
* global `agent.max_concurrent_agents` host ceiling (issue 137). A sum greater
|
|
702
|
-
* than the ceiling can never be satisfied — the global clamp binds first — so it
|
|
703
|
-
* is almost always a misconfiguration worth surfacing at startup. Returns null
|
|
704
|
-
* when in budget or when the ceiling is unknown (legacy hand-built configs that
|
|
705
|
-
* omit `agent`). The legacy by-name map is already folded into the per-state
|
|
706
|
-
* caps by the time this runs, so its entries count toward the sum too.
|
|
707
|
-
*/
|
|
708
|
-
function validateConcurrencyCaps(states, ceiling) {
|
|
709
|
-
if (typeof ceiling !== 'number')
|
|
710
|
-
return null;
|
|
711
|
-
let sum = 0;
|
|
712
|
-
for (const sc of Object.values(states)) {
|
|
713
|
-
if (typeof sc.max_concurrent === 'number')
|
|
714
|
-
sum += sc.max_concurrent;
|
|
715
|
-
}
|
|
716
|
-
if (sum > ceiling) {
|
|
717
|
-
return `sum of per-state max_concurrent caps (${sum}) exceeds agent.max_concurrent_agents (${ceiling})`;
|
|
718
|
-
}
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
// State-map validation, exposed as a string|null so it composes with the rest of
|
|
722
|
-
// `validateDispatch`. Checks declared in the same order the operator would hit
|
|
723
|
-
// them while iterating on a malformed workflow: structural (roles, uniqueness),
|
|
724
|
-
// then cross-references (allowed_transitions targets), then host-resource
|
|
725
|
-
// dependencies (adapter known + credential readable).
|
|
726
|
-
function validateStates(states) {
|
|
727
|
-
const names = Object.keys(states);
|
|
728
|
-
if (names.length === 0)
|
|
729
|
-
return 'states: at least one state must be declared';
|
|
730
|
-
let hasActive = false;
|
|
731
|
-
let hasTerminal = false;
|
|
732
|
-
let hasHolding = false;
|
|
733
|
-
for (const cfg of Object.values(states)) {
|
|
734
|
-
if (cfg.role === 'active')
|
|
735
|
-
hasActive = true;
|
|
736
|
-
else if (cfg.role === 'terminal')
|
|
737
|
-
hasTerminal = true;
|
|
738
|
-
else if (cfg.role === 'holding')
|
|
739
|
-
hasHolding = true;
|
|
740
|
-
}
|
|
741
|
-
if (!hasActive)
|
|
742
|
-
return 'states: at least one state must have role: active';
|
|
743
|
-
if (!hasTerminal)
|
|
744
|
-
return 'states: at least one state must have role: terminal';
|
|
745
|
-
// `holding` is required so `propose_issue` always has a declared landing
|
|
746
|
-
// directory; the dashboard's triage approve/discard surface also needs it.
|
|
747
|
-
if (!hasHolding)
|
|
748
|
-
return 'states: at least one state must have role: holding';
|
|
749
|
-
const seen = new Map();
|
|
750
|
-
for (const name of names) {
|
|
751
|
-
const key = name.toLowerCase();
|
|
752
|
-
const prior = seen.get(key);
|
|
753
|
-
if (prior !== undefined) {
|
|
754
|
-
return `states: duplicate state name (case-insensitive): "${prior}" and "${name}"`;
|
|
755
|
-
}
|
|
756
|
-
seen.set(key, name);
|
|
757
|
-
}
|
|
758
|
-
// PR autopilot routing (issue 139): `pr:` is only meaningful on a terminal
|
|
759
|
-
// state, `on_conflict.route_to` must name a declared state, and at most one
|
|
760
|
-
// terminal state may declare the merge (`auto_merge`) or close (`close`)
|
|
761
|
-
// behavior so `derivePrRouting`'s first-match is unambiguous.
|
|
762
|
-
let mergeStateCount = 0;
|
|
763
|
-
let closeStateCount = 0;
|
|
764
|
-
let armStateCount = 0;
|
|
765
|
-
for (const [name, cfg] of Object.entries(states)) {
|
|
766
|
-
if (cfg.allowed_transitions) {
|
|
767
|
-
for (const target of cfg.allowed_transitions) {
|
|
768
|
-
if (!seen.has(target.toLowerCase())) {
|
|
769
|
-
return `state "${name}": allowed_transitions references undeclared state "${target}"`;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
if (cfg.adapter !== undefined && !isKnownAdapter(cfg.adapter)) {
|
|
774
|
-
return `state "${name}": adapter "${cfg.adapter}" is not a known profile; use one of: claude, codex, opencode`;
|
|
775
|
-
}
|
|
776
|
-
if (cfg.pr) {
|
|
777
|
-
if (cfg.role !== 'terminal') {
|
|
778
|
-
return `state "${name}": pr: is only valid on a terminal state (got role: ${cfg.role})`;
|
|
779
|
-
}
|
|
780
|
-
if (cfg.pr.auto_merge)
|
|
781
|
-
mergeStateCount += 1;
|
|
782
|
-
if (cfg.pr.close)
|
|
783
|
-
closeStateCount += 1;
|
|
784
|
-
const routeTo = cfg.pr.on_conflict?.route_to;
|
|
785
|
-
if (routeTo && !seen.has(routeTo.toLowerCase())) {
|
|
786
|
-
return `state "${name}": pr.on_conflict.route_to references undeclared state "${routeTo}"`;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
// Auto-arm trigger (issue 140): `arm:` is only meaningful on an active state,
|
|
790
|
-
// `arm.issue` is required, `arm.from` must name a declared holding state, and
|
|
791
|
-
// at most one active state may declare arm so `deriveArmRouting`'s first-match
|
|
792
|
-
// is unambiguous. The state's own `role` is authoritative — no separate
|
|
793
|
-
// sleep-cycle role re-validator.
|
|
794
|
-
if (cfg.arm) {
|
|
795
|
-
armStateCount += 1;
|
|
796
|
-
if (cfg.role !== 'active') {
|
|
797
|
-
return `state "${name}": arm: is only valid on an active state (got role: ${cfg.role})`;
|
|
798
|
-
}
|
|
799
|
-
if (!cfg.arm.issue || cfg.arm.issue.trim().length === 0) {
|
|
800
|
-
return `state "${name}": arm.issue is required (the recurring issue to arm into this state)`;
|
|
801
|
-
}
|
|
802
|
-
const fromCanonical = seen.get(cfg.arm.from.toLowerCase());
|
|
803
|
-
if (!fromCanonical) {
|
|
804
|
-
return `state "${name}": arm.from references undeclared state "${cfg.arm.from}"`;
|
|
805
|
-
}
|
|
806
|
-
if (states[fromCanonical].role !== 'holding') {
|
|
807
|
-
return `state "${name}": arm.from "${cfg.arm.from}" must be a holding state (got role: ${states[fromCanonical].role})`;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
if (armStateCount > 1) {
|
|
812
|
-
return `states: at most one active state may declare arm (found ${armStateCount})`;
|
|
813
|
-
}
|
|
814
|
-
if (mergeStateCount > 1) {
|
|
815
|
-
return `states: at most one terminal state may declare pr.auto_merge (found ${mergeStateCount})`;
|
|
816
|
-
}
|
|
817
|
-
if (closeStateCount > 1) {
|
|
818
|
-
return `states: at most one terminal state may declare pr.close (found ${closeStateCount})`;
|
|
819
|
-
}
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
//# sourceMappingURL=workflow.js.map
|