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
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// FCIS rewrite — PURE workflow (WORKFLOW.yaml) parser + typed ServiceConfig builder.
|
|
2
|
+
//
|
|
3
|
+
// Ports the original `src/workflow.ts` parse path (parseWorkflow, expandVar,
|
|
4
|
+
// buildServiceConfig) into the functional
|
|
5
|
+
// core. 100% synchronous, zero IO, imports ONLY from src/types. The legacy
|
|
6
|
+
// fenced front-matter + inline Markdown body form was removed: a workflow file
|
|
7
|
+
// is now pure YAML config (prompts live in per-state `prompt_file`s).
|
|
8
|
+
//
|
|
9
|
+
// How the original's impure dependencies are made pure here:
|
|
10
|
+
// * YAML decode (the `yaml` package in the original's parseFrontMatter) is
|
|
11
|
+
// INJECTED as `deps.parseYaml`. The shell wires in `yaml.parse`; the core
|
|
12
|
+
// never imports the library.
|
|
13
|
+
// * `node:path` / `node:os` (path.resolve/dirname/join/isAbsolute, os.homedir)
|
|
14
|
+
// are replaced with pure POSIX helpers below; the home dir comes from the
|
|
15
|
+
// injected WorkflowEnv (HOME), so the parser reads no real environment.
|
|
16
|
+
// * The `actions:` sub-block parser lives in the sibling core module
|
|
17
|
+
// src/core/actions/parse.ts. It is INJECTABLE as `deps.parseActions` (so the
|
|
18
|
+
// shell can curry it) but defaults to that module's `parseActionsBlock`.
|
|
19
|
+
import { WorkflowError } from '../../types/errors.js';
|
|
20
|
+
import { parseActionsBlock } from '../actions/parse.js';
|
|
21
|
+
import { MISE_EGRESS_HOSTS } from '../runner/mise.js';
|
|
22
|
+
import { asString, asIntOr, asStringListOr } from '../coerce.js';
|
|
23
|
+
import { sanitizeWorkspaceKey } from '../workspace-key.js';
|
|
24
|
+
// ─── pure POSIX path helpers (replace node:path; no IO) ──────────────────────
|
|
25
|
+
//
|
|
26
|
+
// The original used node:path. These are deliberately POSIX-only — the
|
|
27
|
+
// orchestrator runs on Linux/macOS hosts — and operate purely on strings. They
|
|
28
|
+
// faithfully reproduce path.{isAbsolute,join,resolve,dirname} for the inputs the
|
|
29
|
+
// parser sees (absolute workflow paths + relative config values). POSIX-only is
|
|
30
|
+
// intentional and matches the original's runtime targets.
|
|
31
|
+
function isAbsolute(p) {
|
|
32
|
+
return p.startsWith('/');
|
|
33
|
+
}
|
|
34
|
+
function normalizeSegments(segments, allowLeadingDotDot) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const seg of segments) {
|
|
37
|
+
if (seg === '' || seg === '.')
|
|
38
|
+
continue;
|
|
39
|
+
if (seg === '..') {
|
|
40
|
+
if (out.length > 0 && out[out.length - 1] !== '..')
|
|
41
|
+
out.pop();
|
|
42
|
+
else if (allowLeadingDotDot)
|
|
43
|
+
out.push('..');
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out.push(seg);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function joinPath(...parts) {
|
|
51
|
+
const joined = parts.filter((p) => p.length > 0).join('/');
|
|
52
|
+
if (joined === '')
|
|
53
|
+
return '.';
|
|
54
|
+
const absolute = joined.startsWith('/');
|
|
55
|
+
const segs = normalizeSegments(joined.split('/'), !absolute);
|
|
56
|
+
const body = segs.join('/');
|
|
57
|
+
if (absolute)
|
|
58
|
+
return '/' + body;
|
|
59
|
+
return body === '' ? '.' : body;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve `to` against `from` (defaults to a cwd derived from the env). Faithful
|
|
63
|
+
* to path.resolve for the parser's use: an absolute input is normalized; a
|
|
64
|
+
* relative input is joined onto its base. Unlike node:path.resolve we never read
|
|
65
|
+
* the real cwd — a relative base resolves against the injected `cwd`.
|
|
66
|
+
*/
|
|
67
|
+
function resolvePath(cwd, ...parts) {
|
|
68
|
+
let resolved = '';
|
|
69
|
+
let isAbs = false;
|
|
70
|
+
// Walk right-to-left like path.resolve, prepending until we hit an absolute.
|
|
71
|
+
for (let i = parts.length - 1; i >= 0 && !isAbs; i--) {
|
|
72
|
+
const part = parts[i];
|
|
73
|
+
if (part.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
resolved = resolved.length === 0 ? part : part + '/' + resolved;
|
|
76
|
+
isAbs = part.startsWith('/');
|
|
77
|
+
}
|
|
78
|
+
if (!isAbs) {
|
|
79
|
+
resolved = resolved.length === 0 ? cwd : cwd + '/' + resolved;
|
|
80
|
+
}
|
|
81
|
+
const absolute = resolved.startsWith('/');
|
|
82
|
+
const segs = normalizeSegments(resolved.split('/'), false);
|
|
83
|
+
const body = segs.join('/');
|
|
84
|
+
if (absolute)
|
|
85
|
+
return '/' + body;
|
|
86
|
+
return body === '' ? '.' : body;
|
|
87
|
+
}
|
|
88
|
+
function dirname(p) {
|
|
89
|
+
const norm = p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p;
|
|
90
|
+
const idx = norm.lastIndexOf('/');
|
|
91
|
+
if (idx < 0)
|
|
92
|
+
return '.';
|
|
93
|
+
if (idx === 0)
|
|
94
|
+
return '/';
|
|
95
|
+
return norm.slice(0, idx);
|
|
96
|
+
}
|
|
97
|
+
/** Last path segment (replaces node:path.basename for the project-namespace fallback). */
|
|
98
|
+
function basename(p) {
|
|
99
|
+
const norm = p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p;
|
|
100
|
+
const idx = norm.lastIndexOf('/');
|
|
101
|
+
return idx < 0 ? norm : norm.slice(idx + 1);
|
|
102
|
+
}
|
|
103
|
+
/** Home directory from the injected env (replaces os.homedir(); no IO). */
|
|
104
|
+
function homeDir(env) {
|
|
105
|
+
const h = env['HOME'] ?? env['USERPROFILE'];
|
|
106
|
+
return h && h.length > 0 ? h : '/root';
|
|
107
|
+
}
|
|
108
|
+
/** The base cwd a relative workflow path resolves against (env PWD, else "/"). */
|
|
109
|
+
function cwdOf(env) {
|
|
110
|
+
const pwd = env['PWD'];
|
|
111
|
+
return pwd && isAbsolute(pwd) ? pwd : '/';
|
|
112
|
+
}
|
|
113
|
+
// ─── home-based path defaults (issue 210) ────────────────────────────────────
|
|
114
|
+
//
|
|
115
|
+
// When a `*.root` is UNSET, durable operator state defaults under a per-project
|
|
116
|
+
// home base (~/.symphony/<kind>/<project>) instead of inside the repo working
|
|
117
|
+
// tree or tmpdir — so it survives a `rm -rf`/re-clone of the repo (or a reboot,
|
|
118
|
+
// for the old tmpdir workspaces). An EXPLICIT root is unaffected: it still
|
|
119
|
+
// resolves exactly as before (absolute as-is, `~`-expanded, relative →
|
|
120
|
+
// workflowDir). Only the unset-default branch routes through here.
|
|
121
|
+
/**
|
|
122
|
+
* The `<project>` namespace for the home-based path defaults. Derived from the
|
|
123
|
+
* `workspace.github_repo` slug (owner/repo → repo), falling back to the
|
|
124
|
+
* workflow-dir basename. Sanitized with the canonical workspace-key sanitizer so
|
|
125
|
+
* two different projects on one machine never collide under the shared
|
|
126
|
+
* `~/.symphony/<kind>/` base (the supported topology is one instance per project,
|
|
127
|
+
* many across DIFFERENT projects). An empty result falls back to "symphony".
|
|
128
|
+
*/
|
|
129
|
+
function deriveProjectNamespace(githubRepo, workflowDir) {
|
|
130
|
+
const raw = githubRepo ? (githubRepo.split('/').pop() ?? '') : basename(workflowDir);
|
|
131
|
+
const sanitized = sanitizeWorkspaceKey(raw.trim());
|
|
132
|
+
return sanitized.length > 0 ? sanitized : 'symphony';
|
|
133
|
+
}
|
|
134
|
+
/** Home-based default root for an unset `*.root`: ~/.symphony/<kind>/<project>. */
|
|
135
|
+
function homeDefaultRoot(env, kind, project) {
|
|
136
|
+
return joinPath(homeDir(env), '.symphony', kind, project);
|
|
137
|
+
}
|
|
138
|
+
// ─── workflow YAML decode (§4.2) ──────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Decode a workflow file (pure YAML) to a config map. A workflow file is pure
|
|
141
|
+
* YAML config — the whole file decodes to the config map and prompts come from
|
|
142
|
+
* per-state `prompt_file`s (+ the shared `prompt.preamble_file`/`footer_file`).
|
|
143
|
+
* Empty/whitespace decodes to `{}`; a non-map (list, scalar) throws
|
|
144
|
+
* `workflow_front_matter_not_a_map`; any other decode failure throws
|
|
145
|
+
* `workflow_parse_error`.
|
|
146
|
+
*/
|
|
147
|
+
export function decodeYamlMap(text, parseYaml) {
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = text.trim().length === 0 ? {} : parseYaml(text);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw new WorkflowError('workflow_parse_error', `invalid workflow YAML: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
if (parsed === null || parsed === undefined)
|
|
156
|
+
parsed = {};
|
|
157
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
158
|
+
throw new WorkflowError('workflow_front_matter_not_a_map', 'workflow YAML must decode to a map');
|
|
159
|
+
}
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Pure entry point: decode the workflow YAML, build the typed view, and return
|
|
164
|
+
* both shapes. `env` supplies the `$VAR`/`~` variable map (the shell loader
|
|
165
|
+
* passes process.env; tests pass an explicit shape). `deps.parseYaml` decodes
|
|
166
|
+
* the config; `deps.parseActions` parses per-state `actions:` blocks.
|
|
167
|
+
*/
|
|
168
|
+
export function parseWorkflow(text, workflowPath, deps, env = {}) {
|
|
169
|
+
// A workflow file is pure YAML config: the whole file decodes to the config
|
|
170
|
+
// map and each active state renders its own `prompt_file` (wrapped in the
|
|
171
|
+
// shared `prompt.preamble_file`/`footer_file`). The legacy fenced
|
|
172
|
+
// front-matter + inline Markdown body form was removed (no backward compat).
|
|
173
|
+
const raw = decodeYamlMap(text, deps.parseYaml);
|
|
174
|
+
const definition = { config: raw };
|
|
175
|
+
const built = buildServiceConfig(raw, workflowPath, deps, env);
|
|
176
|
+
return { definition, config: built.config };
|
|
177
|
+
}
|
|
178
|
+
// ─── $VAR / ~ expansion (pure) ────────────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* `$VAR` / `~` expansion for path/command fields. `~`/`~/x` expand against the
|
|
181
|
+
* injected env's HOME; a bare `$NAME` resolves from `env` (empty string when
|
|
182
|
+
* unset, matching the original). Other strings pass through unchanged.
|
|
183
|
+
*/
|
|
184
|
+
export function expandVar(value, env = {}) {
|
|
185
|
+
if (typeof value !== 'string')
|
|
186
|
+
return value;
|
|
187
|
+
let s = value;
|
|
188
|
+
if (s.startsWith('~/') || s === '~') {
|
|
189
|
+
s = s === '~' ? homeDir(env) : joinPath(homeDir(env), s.slice(2));
|
|
190
|
+
}
|
|
191
|
+
const m = s.match(/^\$([A-Z_][A-Z0-9_]*)$/);
|
|
192
|
+
if (m) {
|
|
193
|
+
const envVal = env[m[1]];
|
|
194
|
+
return envVal ?? '';
|
|
195
|
+
}
|
|
196
|
+
return s;
|
|
197
|
+
}
|
|
198
|
+
// ─── scalar coercion helpers ─────────────────────────────────────────────────
|
|
199
|
+
// asString / asInt / asStringList are the canonical coercers from core/coerce.ts
|
|
200
|
+
// (asString imported directly; the fallback-taking config forms are local
|
|
201
|
+
// aliases over asIntOr / asStringListOr so the 20+ call sites are unchanged).
|
|
202
|
+
const asInt = asIntOr;
|
|
203
|
+
const asStringList = asStringListOr;
|
|
204
|
+
/** A non-empty (post-trim) string, or the fallback (so a blank config value defaults). */
|
|
205
|
+
function nonEmptyOr(v, fallback) {
|
|
206
|
+
if (typeof v === 'string' && v.trim().length > 0)
|
|
207
|
+
return v.trim();
|
|
208
|
+
return fallback;
|
|
209
|
+
}
|
|
210
|
+
function getObject(parent, key) {
|
|
211
|
+
const v = parent[key];
|
|
212
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
213
|
+
return v;
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
// workspace.github_repo validation: a literal `owner/repo` slug, or null. See the
|
|
217
|
+
// original for the rationale (reject whole-URL/SSH/bare names at parse time).
|
|
218
|
+
function parseGithubRepo(input) {
|
|
219
|
+
if (input === undefined || input === null)
|
|
220
|
+
return null;
|
|
221
|
+
if (typeof input !== 'string') {
|
|
222
|
+
throw new WorkflowError('workflow_parse_error', `workspace.github_repo must be a string "owner/repo" slug or "none" (got ${typeof input})`);
|
|
223
|
+
}
|
|
224
|
+
const trimmed = input.trim();
|
|
225
|
+
if (trimmed === '' || trimmed.toLowerCase() === 'none')
|
|
226
|
+
return null;
|
|
227
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
228
|
+
throw new WorkflowError('workflow_parse_error', `workspace.github_repo must be a GitHub "owner/repo" slug or "none" (got: ${input})`);
|
|
229
|
+
}
|
|
230
|
+
return trimmed;
|
|
231
|
+
}
|
|
232
|
+
// gondolin.oci_pull_policy: the OCI pull behavior before conversion (issue 206).
|
|
233
|
+
// Defaults to `if-not-present`; rejects any other string at parse time so a typo
|
|
234
|
+
// surfaces here, not at the first reconcile build.
|
|
235
|
+
function parseOciPullPolicy(input) {
|
|
236
|
+
if (input === undefined || input === null)
|
|
237
|
+
return 'if-not-present';
|
|
238
|
+
const s = asString(input);
|
|
239
|
+
if (s === 'if-not-present' || s === 'always' || s === 'never')
|
|
240
|
+
return s;
|
|
241
|
+
throw new WorkflowError('workflow_parse_error', `gondolin.oci_pull_policy must be one of if-not-present|always|never (got: ${String(input)})`);
|
|
242
|
+
}
|
|
243
|
+
// gondolin.oci_runtime: the container runtime for the conversion pull/export
|
|
244
|
+
// (issue 206). Null/unset = auto-detect; rejects any value other than docker/podman.
|
|
245
|
+
function parseOciRuntime(input) {
|
|
246
|
+
if (input === undefined || input === null)
|
|
247
|
+
return null;
|
|
248
|
+
const s = asString(input);
|
|
249
|
+
if (s === null || s.trim().length === 0)
|
|
250
|
+
return null;
|
|
251
|
+
const trimmed = s.trim();
|
|
252
|
+
if (trimmed === 'docker' || trimmed === 'podman')
|
|
253
|
+
return trimmed;
|
|
254
|
+
throw new WorkflowError('workflow_parse_error', `gondolin.oci_runtime must be docker or podman (got: ${String(input)})`);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Build a fully typed ServiceConfig from a parsed workflow YAML config map. Pure. The
|
|
258
|
+
* `env` supplies the `$VAR`/`~` variable map; `deps.parseActions` parses per-state
|
|
259
|
+
* `actions:` blocks.
|
|
260
|
+
*/
|
|
261
|
+
export function buildServiceConfig(raw, workflowPath, deps, env = {}) {
|
|
262
|
+
const cwd = cwdOf(env);
|
|
263
|
+
const parseActions = deps.parseActions ?? parseActionsBlock;
|
|
264
|
+
const workflowAbs = resolvePath(cwd, workflowPath);
|
|
265
|
+
const workflowDir = dirname(workflowAbs);
|
|
266
|
+
// Home-default project namespace (issue 210): parse workspace.github_repo once
|
|
267
|
+
// here so the unset-root defaults of tracker / workspaces / logs can land under
|
|
268
|
+
// ~/.symphony/<kind>/<project>. (workspaceRaw + githubRepo are reused by the
|
|
269
|
+
// workspace block below.)
|
|
270
|
+
const workspaceRaw = getObject(raw, 'workspace');
|
|
271
|
+
const githubRepo = parseGithubRepo(workspaceRaw['github_repo']);
|
|
272
|
+
const projectNamespace = deriveProjectNamespace(githubRepo, workflowDir);
|
|
273
|
+
// tracker (§4.3.1)
|
|
274
|
+
const trackerRaw = getObject(raw, 'tracker');
|
|
275
|
+
const trackerKind = (asString(trackerRaw['kind']) ?? '').trim();
|
|
276
|
+
const trackerRootRaw = asString(trackerRaw['root']);
|
|
277
|
+
let trackerRoot = null;
|
|
278
|
+
if (trackerRootRaw) {
|
|
279
|
+
const expanded = expandVar(trackerRootRaw, env);
|
|
280
|
+
if (expanded === '') {
|
|
281
|
+
throw new WorkflowError('workflow_parse_error', `tracker.root references an unset variable: ${trackerRootRaw}`);
|
|
282
|
+
}
|
|
283
|
+
trackerRoot = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
|
|
284
|
+
}
|
|
285
|
+
else if (trackerKind === 'local') {
|
|
286
|
+
trackerRoot = homeDefaultRoot(env, 'trackers', projectNamespace);
|
|
287
|
+
}
|
|
288
|
+
const states = parseStatesBlock(raw['states'], parseActions);
|
|
289
|
+
const tracker = { kind: trackerKind, states, root: trackerRoot };
|
|
290
|
+
// prompt files (per-state prompt split). Resolve a workflow-relative or
|
|
291
|
+
// `$VAR`/`~` path to an absolute path the shell loader reads off disk. Shared
|
|
292
|
+
// by the per-state `prompt_file` and the `prompt.{preamble,footer}_file`.
|
|
293
|
+
const resolvePromptPath = (value, label) => {
|
|
294
|
+
const expanded = expandVar(value, env);
|
|
295
|
+
if (expanded === '') {
|
|
296
|
+
throw new WorkflowError('workflow_parse_error', `${label} references an unset variable: ${value}`);
|
|
297
|
+
}
|
|
298
|
+
return isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
|
|
299
|
+
};
|
|
300
|
+
for (const [name, sc] of Object.entries(states)) {
|
|
301
|
+
if (sc.prompt_file)
|
|
302
|
+
sc.prompt_file = resolvePromptPath(sc.prompt_file, `state "${name}": prompt_file`);
|
|
303
|
+
}
|
|
304
|
+
const promptRaw = getObject(raw, 'prompt');
|
|
305
|
+
const preambleFileRaw = asString(promptRaw['preamble_file']);
|
|
306
|
+
const footerFileRaw = asString(promptRaw['footer_file']);
|
|
307
|
+
const prompt = {
|
|
308
|
+
preamble_file: preambleFileRaw && preambleFileRaw.trim().length > 0
|
|
309
|
+
? resolvePromptPath(preambleFileRaw.trim(), 'prompt.preamble_file')
|
|
310
|
+
: null,
|
|
311
|
+
footer_file: footerFileRaw && footerFileRaw.trim().length > 0
|
|
312
|
+
? resolvePromptPath(footerFileRaw.trim(), 'prompt.footer_file')
|
|
313
|
+
: null,
|
|
314
|
+
};
|
|
315
|
+
// polling (§4.3.2)
|
|
316
|
+
const pollingRaw = getObject(raw, 'polling');
|
|
317
|
+
const polling = { interval_ms: asInt(pollingRaw['interval_ms'], 30_000) };
|
|
318
|
+
// workspace (§4.3.3). workspaceRaw + githubRepo were parsed above (the home
|
|
319
|
+
// project namespace needs github_repo before the tracker default).
|
|
320
|
+
const wsRootInput = asString(workspaceRaw['root']);
|
|
321
|
+
let workspaceRoot;
|
|
322
|
+
if (wsRootInput) {
|
|
323
|
+
const expanded = expandVar(wsRootInput, env);
|
|
324
|
+
if (expanded === '') {
|
|
325
|
+
throw new WorkflowError('workflow_parse_error', `workspace.root references an unset variable: ${wsRootInput}`);
|
|
326
|
+
}
|
|
327
|
+
workspaceRoot = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
workspaceRoot = homeDefaultRoot(env, 'workspaces', projectNamespace);
|
|
331
|
+
}
|
|
332
|
+
const baseBranchInput = asString(workspaceRaw['base_branch']);
|
|
333
|
+
const baseBranch = baseBranchInput && baseBranchInput.trim().length > 0 ? baseBranchInput.trim() : 'main';
|
|
334
|
+
const workspace = {
|
|
335
|
+
root: resolvePath(cwd, workspaceRoot),
|
|
336
|
+
github_repo: githubRepo,
|
|
337
|
+
base_branch: baseBranch,
|
|
338
|
+
};
|
|
339
|
+
// logs (symphony extension): per-issue JSONL run logs.
|
|
340
|
+
const logsRaw = getObject(raw, 'logs');
|
|
341
|
+
const logsRootInput = asString(logsRaw['root']);
|
|
342
|
+
let logsRoot;
|
|
343
|
+
if (logsRootInput) {
|
|
344
|
+
const expanded = expandVar(logsRootInput, env);
|
|
345
|
+
if (expanded === '') {
|
|
346
|
+
throw new WorkflowError('workflow_parse_error', `logs.root references an unset variable: ${logsRootInput}`);
|
|
347
|
+
}
|
|
348
|
+
logsRoot = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
logsRoot = homeDefaultRoot(env, 'logs', projectNamespace);
|
|
352
|
+
}
|
|
353
|
+
const logs = { root: resolvePath(cwd, logsRoot) };
|
|
354
|
+
// agent (§4.3.4)
|
|
355
|
+
const agentRaw = getObject(raw, 'agent');
|
|
356
|
+
const maxTurns = asInt(agentRaw['max_turns'], 20);
|
|
357
|
+
if (maxTurns <= 0) {
|
|
358
|
+
throw new WorkflowError('workflow_parse_error', 'agent.max_turns must be positive');
|
|
359
|
+
}
|
|
360
|
+
const memAdmissionEnabledRaw = agentRaw['memory_admission_enabled'];
|
|
361
|
+
const memoryAdmissionEnabled = memAdmissionEnabledRaw === undefined ? true : memAdmissionEnabledRaw !== false;
|
|
362
|
+
const hostMemoryReserveMib = asInt(agentRaw['host_memory_reserve_mib'], 2048);
|
|
363
|
+
if (hostMemoryReserveMib < 0) {
|
|
364
|
+
throw new WorkflowError('workflow_parse_error', 'agent.host_memory_reserve_mib must be a non-negative integer');
|
|
365
|
+
}
|
|
366
|
+
const circuitBreakerThreshold = asInt(agentRaw['circuit_breaker_threshold'], 5);
|
|
367
|
+
if (circuitBreakerThreshold < 0 || circuitBreakerThreshold === 1) {
|
|
368
|
+
throw new WorkflowError('workflow_parse_error', 'agent.circuit_breaker_threshold must be 0 (disabled) or an integer >= 2');
|
|
369
|
+
}
|
|
370
|
+
const agent = {
|
|
371
|
+
max_concurrent_agents: asInt(agentRaw['max_concurrent_agents'], 10),
|
|
372
|
+
max_turns: maxTurns,
|
|
373
|
+
max_retry_backoff_ms: asInt(agentRaw['max_retry_backoff_ms'], 300_000),
|
|
374
|
+
memory_admission_enabled: memoryAdmissionEnabled,
|
|
375
|
+
host_memory_reserve_mib: hostMemoryReserveMib,
|
|
376
|
+
circuit_breaker_threshold: circuitBreakerThreshold,
|
|
377
|
+
};
|
|
378
|
+
// acp (Symphony extension; see §4.3.5)
|
|
379
|
+
const acpRaw = getObject(raw, 'acp');
|
|
380
|
+
const modelRaw = asString(acpRaw['model']);
|
|
381
|
+
const modelTrimmed = modelRaw === null ? null : modelRaw.trim();
|
|
382
|
+
const effortRaw = asString(acpRaw['effort']);
|
|
383
|
+
const effortTrimmed = effortRaw === null ? null : effortRaw.trim();
|
|
384
|
+
const acp = {
|
|
385
|
+
adapter: asString(acpRaw['adapter']) ?? 'claude',
|
|
386
|
+
model: modelTrimmed && modelTrimmed.length > 0 ? modelTrimmed : null,
|
|
387
|
+
effort: effortTrimmed && effortTrimmed.length > 0 ? effortTrimmed : null,
|
|
388
|
+
shell: asString(acpRaw['shell']) ?? 'bash',
|
|
389
|
+
prompt_timeout_ms: asInt(acpRaw['prompt_timeout_ms'], 3_600_000),
|
|
390
|
+
read_timeout_ms: asInt(acpRaw['read_timeout_ms'], 30_000),
|
|
391
|
+
stall_timeout_ms: asInt(acpRaw['stall_timeout_ms'], 300_000),
|
|
392
|
+
connect_timeout_ms: asInt(acpRaw['connect_timeout_ms'], 30_000),
|
|
393
|
+
heartbeat_interval_ms: asInt(acpRaw['heartbeat_interval_ms'], 15_000),
|
|
394
|
+
heartbeat_timeout_ms: asInt(acpRaw['heartbeat_timeout_ms'], 45_000),
|
|
395
|
+
};
|
|
396
|
+
// credentials extension (issue 113)
|
|
397
|
+
const credentialsRaw = getObject(raw, 'credentials');
|
|
398
|
+
const credentials = {
|
|
399
|
+
ticker_interval_ms: asInt(credentialsRaw['ticker_interval_ms'], 6 * 60 * 60 * 1000),
|
|
400
|
+
refresh_margin_ms: asInt(credentialsRaw['refresh_margin_ms'], 5 * 60 * 1000),
|
|
401
|
+
};
|
|
402
|
+
// gondolin VM extension
|
|
403
|
+
const gondolinRaw = getObject(raw, 'gondolin');
|
|
404
|
+
const volumesRaw = gondolinRaw['volumes'];
|
|
405
|
+
const volumes = Array.isArray(volumesRaw)
|
|
406
|
+
? volumesRaw.flatMap((v) => {
|
|
407
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
408
|
+
return [];
|
|
409
|
+
const m = v;
|
|
410
|
+
const hostRaw = asString(m['host']);
|
|
411
|
+
const guest = asString(m['guest']);
|
|
412
|
+
if (!hostRaw || !guest)
|
|
413
|
+
return [];
|
|
414
|
+
const expandedHost = expandVar(hostRaw, env);
|
|
415
|
+
if (expandedHost === '')
|
|
416
|
+
return [];
|
|
417
|
+
const host = isAbsolute(expandedHost)
|
|
418
|
+
? expandedHost
|
|
419
|
+
: resolvePath(workflowDir, expandedHost);
|
|
420
|
+
const readonly = m['readonly'] === true;
|
|
421
|
+
return [{ host, guest, readonly }];
|
|
422
|
+
})
|
|
423
|
+
: [];
|
|
424
|
+
const ociImageRaw = asString(gondolinRaw['oci_image']);
|
|
425
|
+
// gondolin.mise (issue 209): mise provisioning is UNCONDITIONAL (issue 233) — the
|
|
426
|
+
// base image ships only `mise`, so this is the sole toolchain path; only `data_dir`
|
|
427
|
+
// is configurable. data_dir defaults to a flat, home-based SHARED store
|
|
428
|
+
// ~/.symphony/mise (NOT per-project — mise dedups installs by tool@version across
|
|
429
|
+
// VMs). An explicit data_dir resolves like any other root (absolute as-is,
|
|
430
|
+
// `~`/`$VAR`-expanded, relative → workflowDir). A stale `enabled:` key is simply
|
|
431
|
+
// ignored (consistent with the parser's unknown-key tolerance), never a hard error.
|
|
432
|
+
const miseRaw = getObject(gondolinRaw, 'mise');
|
|
433
|
+
const miseDataDirInput = asString(miseRaw['data_dir']);
|
|
434
|
+
let miseDataDir;
|
|
435
|
+
if (miseDataDirInput && miseDataDirInput.trim().length > 0) {
|
|
436
|
+
const expanded = expandVar(miseDataDirInput.trim(), env);
|
|
437
|
+
if (expanded === '') {
|
|
438
|
+
throw new WorkflowError('workflow_parse_error', `gondolin.mise.data_dir references an unset variable: ${miseDataDirInput}`);
|
|
439
|
+
}
|
|
440
|
+
miseDataDir = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
miseDataDir = joinPath(homeDir(env), '.symphony', 'mise');
|
|
444
|
+
}
|
|
445
|
+
// gondolin image source precedence (issue 224): explicit `image` > `oci_image` >
|
|
446
|
+
// managed. `image: managed` is a SENTINEL that selects the managed prebuilt-asset
|
|
447
|
+
// source (parsed to `managed_image: true` + `image: null` so the sentinel never
|
|
448
|
+
// reaches `resolveImagePath`). An unset image+oci_image DEFAULTS to managed so a
|
|
449
|
+
// fresh scaffold is `scaffold → run`. An explicit `image` (escape hatch) or
|
|
450
|
+
// `oci_image` (auto-convert) keeps `managed_image` false.
|
|
451
|
+
const rawImage = asString(gondolinRaw['image']);
|
|
452
|
+
const trimmedImage = rawImage && rawImage.trim().length > 0 ? rawImage.trim() : null;
|
|
453
|
+
const ociImage = ociImageRaw && ociImageRaw.trim().length > 0 ? ociImageRaw.trim() : null;
|
|
454
|
+
const managedSentinel = trimmedImage !== null && trimmedImage.toLowerCase() === 'managed';
|
|
455
|
+
const escapeHatchImage = managedSentinel ? null : trimmedImage;
|
|
456
|
+
const managedImage = managedSentinel || (escapeHatchImage === null && ociImage === null);
|
|
457
|
+
const gondolin = {
|
|
458
|
+
image: escapeHatchImage,
|
|
459
|
+
managed_image: managedImage,
|
|
460
|
+
oci_image: ociImage,
|
|
461
|
+
oci_pull_policy: parseOciPullPolicy(gondolinRaw['oci_pull_policy']),
|
|
462
|
+
oci_runtime: parseOciRuntime(gondolinRaw['oci_runtime']),
|
|
463
|
+
cpus: asInt(gondolinRaw['cpus'], 2),
|
|
464
|
+
mem_mib: asInt(gondolinRaw['mem_mib'], 2048),
|
|
465
|
+
// rootfs_size (issue 222): the ephemeral mise install lives on the guest rootfs, which
|
|
466
|
+
// the ~593 MB default is too small for — grow it (qemu size syntax). Default 3G.
|
|
467
|
+
rootfs_size: nonEmptyOr(asString(gondolinRaw['rootfs_size']), '3G'),
|
|
468
|
+
volumes,
|
|
469
|
+
forward_env: asStringList(gondolinRaw['forward_env'], ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']),
|
|
470
|
+
guest_agent_path: nonEmptyOr(asString(gondolinRaw['guest_agent_path']), '/opt/symphony/vm-agent.mjs'),
|
|
471
|
+
node_bin: nonEmptyOr(asString(gondolinRaw['node_bin']), 'node'),
|
|
472
|
+
mise: { data_dir: miseDataDir },
|
|
473
|
+
};
|
|
474
|
+
// egress firewall. mise provisioning is unconditional (issue 233), so when the
|
|
475
|
+
// workflow declares NO explicit allowed_hosts the default is ALWAYS the mise
|
|
476
|
+
// provisioning hosts (nodejs.org / npm registry / mise registry) so `mise install`
|
|
477
|
+
// of the staged SYSTEM config isn't silently firewall-blocked (issue 209). An
|
|
478
|
+
// explicit `allowed_hosts` REPLACES this default — a consuming repo adding
|
|
479
|
+
// project-toolchain hosts must re-list the mise ones.
|
|
480
|
+
const egressRaw = getObject(raw, 'egress');
|
|
481
|
+
const egress = {
|
|
482
|
+
allowed_hosts: asStringList(egressRaw['allowed_hosts'], [...MISE_EGRESS_HOSTS]),
|
|
483
|
+
};
|
|
484
|
+
// server extension (§9.5)
|
|
485
|
+
const serverRaw = getObject(raw, 'server');
|
|
486
|
+
const server = {
|
|
487
|
+
port: typeof serverRaw['port'] === 'number' ? serverRaw['port'] : null,
|
|
488
|
+
host: asString(serverRaw['host']) ?? '127.0.0.1',
|
|
489
|
+
};
|
|
490
|
+
// mcp extension
|
|
491
|
+
const mcpRaw = getObject(raw, 'mcp');
|
|
492
|
+
const mcpEnabledRaw = mcpRaw['enabled'];
|
|
493
|
+
const mcpEnabled = mcpEnabledRaw === undefined ? true : mcpEnabledRaw !== false;
|
|
494
|
+
const mcp = {
|
|
495
|
+
enabled: mcpEnabled,
|
|
496
|
+
host: asString(mcpRaw['host']) ?? '127.0.0.1',
|
|
497
|
+
explicit_host_url: asString(mcpRaw['host_url']),
|
|
498
|
+
};
|
|
499
|
+
// pr (issue 38, slimmed in issue 139)
|
|
500
|
+
const prRaw = getObject(raw, 'pr');
|
|
501
|
+
const pr = {
|
|
502
|
+
enabled: prRaw['enabled'] === true,
|
|
503
|
+
poll_interval_ms: asInt(prRaw['poll_interval_ms'], 30_000),
|
|
504
|
+
};
|
|
505
|
+
if (pr.poll_interval_ms < 0) {
|
|
506
|
+
throw new WorkflowError('workflow_parse_error', 'pr.poll_interval_ms must be non-negative');
|
|
507
|
+
}
|
|
508
|
+
const config = {
|
|
509
|
+
workflow_path: workflowAbs,
|
|
510
|
+
workflow_dir: workflowDir,
|
|
511
|
+
tracker,
|
|
512
|
+
prompt,
|
|
513
|
+
polling,
|
|
514
|
+
workspace,
|
|
515
|
+
logs,
|
|
516
|
+
agent,
|
|
517
|
+
acp,
|
|
518
|
+
gondolin,
|
|
519
|
+
egress,
|
|
520
|
+
server,
|
|
521
|
+
mcp,
|
|
522
|
+
pr,
|
|
523
|
+
credentials,
|
|
524
|
+
states,
|
|
525
|
+
};
|
|
526
|
+
return { config };
|
|
527
|
+
}
|
|
528
|
+
// ─── per-state spawn block (recurring reflection spawn) ───────────────────────
|
|
529
|
+
function parseStateSpawnBlock(stateName, raw) {
|
|
530
|
+
if (raw === undefined || raw === null)
|
|
531
|
+
return undefined;
|
|
532
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
533
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": spawn must be a map (on_idle / after_terminal / title / body / max_in_flight)`);
|
|
534
|
+
}
|
|
535
|
+
const m = raw;
|
|
536
|
+
const afterTerminal = asInt(m['after_terminal'], 0);
|
|
537
|
+
if (afterTerminal < 0) {
|
|
538
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": spawn.after_terminal must be a non-negative integer (0 disables the terminal-count trigger)`);
|
|
539
|
+
}
|
|
540
|
+
const maxInFlight = asInt(m['max_in_flight'], 1);
|
|
541
|
+
if (maxInFlight < 1) {
|
|
542
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": spawn.max_in_flight must be a positive integer (>= 1)`);
|
|
543
|
+
}
|
|
544
|
+
const titleRaw = asString(m['title']);
|
|
545
|
+
const title = titleRaw && titleRaw.trim().length > 0 ? titleRaw.trim() : 'Reflection {{ stamp }}';
|
|
546
|
+
const bodyRaw = asString(m['body']);
|
|
547
|
+
const body = bodyRaw && bodyRaw.trim().length > 0 ? bodyRaw.trim() : null;
|
|
548
|
+
return {
|
|
549
|
+
on_idle: m['on_idle'] === true,
|
|
550
|
+
after_terminal: afterTerminal,
|
|
551
|
+
title,
|
|
552
|
+
body,
|
|
553
|
+
max_in_flight: maxInFlight,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// ─── states block (§4.3, mandatory) ──────────────────────────────────────────
|
|
557
|
+
function parseStatesBlock(raw, parseActions) {
|
|
558
|
+
if (raw === undefined || raw === null) {
|
|
559
|
+
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.yaml for the schema.');
|
|
560
|
+
}
|
|
561
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
562
|
+
throw new WorkflowError('workflow_parse_error', 'states: must be a map of name → config');
|
|
563
|
+
}
|
|
564
|
+
const entries = Object.entries(raw);
|
|
565
|
+
if (entries.length === 0) {
|
|
566
|
+
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.yaml for the schema.');
|
|
567
|
+
}
|
|
568
|
+
const out = {};
|
|
569
|
+
for (const [name, value] of entries) {
|
|
570
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
571
|
+
throw new WorkflowError('workflow_parse_error', `state "${name}": value must be a map`);
|
|
572
|
+
}
|
|
573
|
+
const m = value;
|
|
574
|
+
const roleRaw = asString(m['role']);
|
|
575
|
+
if (roleRaw !== 'active' && roleRaw !== 'terminal' && roleRaw !== 'holding') {
|
|
576
|
+
throw new WorkflowError('workflow_parse_error', `state "${name}": role must be one of active|terminal|holding (got: ${String(m['role'])})`);
|
|
577
|
+
}
|
|
578
|
+
const adapter = asString(m['adapter']);
|
|
579
|
+
const modelRaw = asString(m['model']);
|
|
580
|
+
const modelTrimmed = modelRaw === null ? undefined : modelRaw.trim();
|
|
581
|
+
const model = modelTrimmed === undefined ? undefined : modelTrimmed.length > 0 ? modelTrimmed : null;
|
|
582
|
+
const effortRaw = asString(m['effort']);
|
|
583
|
+
const effortTrimmed = effortRaw === null ? undefined : effortRaw.trim();
|
|
584
|
+
const effort = effortTrimmed === undefined ? undefined : effortTrimmed.length > 0 ? effortTrimmed : null;
|
|
585
|
+
let maxTurns;
|
|
586
|
+
if (m['max_turns'] !== undefined) {
|
|
587
|
+
const n = asInt(m['max_turns'], -1);
|
|
588
|
+
if (n <= 0) {
|
|
589
|
+
throw new WorkflowError('workflow_parse_error', `state "${name}": max_turns must be a positive integer`);
|
|
590
|
+
}
|
|
591
|
+
maxTurns = n;
|
|
592
|
+
}
|
|
593
|
+
let maxConcurrent;
|
|
594
|
+
if (m['max_concurrent'] !== undefined) {
|
|
595
|
+
const n = asInt(m['max_concurrent'], -1);
|
|
596
|
+
if (n <= 0) {
|
|
597
|
+
throw new WorkflowError('workflow_parse_error', `state "${name}": max_concurrent must be a positive integer`);
|
|
598
|
+
}
|
|
599
|
+
maxConcurrent = n;
|
|
600
|
+
}
|
|
601
|
+
let allowed;
|
|
602
|
+
if (m['allowed_transitions'] === undefined) {
|
|
603
|
+
allowed = undefined;
|
|
604
|
+
}
|
|
605
|
+
else if (m['allowed_transitions'] === null) {
|
|
606
|
+
allowed = null;
|
|
607
|
+
}
|
|
608
|
+
else if (Array.isArray(m['allowed_transitions'])) {
|
|
609
|
+
allowed = m['allowed_transitions'].filter((x) => typeof x === 'string');
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
throw new WorkflowError('workflow_parse_error', `state "${name}": allowed_transitions must be a list of state names (or null/omitted)`);
|
|
613
|
+
}
|
|
614
|
+
const stateActions = parseActions(name, m['actions']);
|
|
615
|
+
const evalModeRaw = m['eval_mode'];
|
|
616
|
+
if (evalModeRaw !== undefined && typeof evalModeRaw !== 'boolean') {
|
|
617
|
+
throw new WorkflowError('workflow_parse_error', `state "${name}": eval_mode must be a boolean (true/false)`);
|
|
618
|
+
}
|
|
619
|
+
const statePr = parseStatePrBlock(name, m['pr']);
|
|
620
|
+
const stateSpawn = parseStateSpawnBlock(name, m['spawn']);
|
|
621
|
+
const promptFileRaw = asString(m['prompt_file']);
|
|
622
|
+
const sc = { role: roleRaw };
|
|
623
|
+
if (adapter !== null)
|
|
624
|
+
sc.adapter = adapter;
|
|
625
|
+
// Stored as the raw (trimmed) string here; buildServiceConfig resolves it to
|
|
626
|
+
// an absolute path against the workflow dir (it owns the env + path helpers).
|
|
627
|
+
if (promptFileRaw && promptFileRaw.trim().length > 0)
|
|
628
|
+
sc.prompt_file = promptFileRaw.trim();
|
|
629
|
+
if (model !== undefined)
|
|
630
|
+
sc.model = model;
|
|
631
|
+
if (effort !== undefined)
|
|
632
|
+
sc.effort = effort;
|
|
633
|
+
if (maxTurns !== undefined)
|
|
634
|
+
sc.max_turns = maxTurns;
|
|
635
|
+
if (maxConcurrent !== undefined)
|
|
636
|
+
sc.max_concurrent = maxConcurrent;
|
|
637
|
+
if (allowed !== undefined)
|
|
638
|
+
sc.allowed_transitions = allowed;
|
|
639
|
+
if (stateActions !== undefined)
|
|
640
|
+
sc.actions = stateActions;
|
|
641
|
+
if (evalModeRaw === true)
|
|
642
|
+
sc.eval_mode = true;
|
|
643
|
+
if (statePr !== undefined)
|
|
644
|
+
sc.pr = statePr;
|
|
645
|
+
if (stateSpawn !== undefined)
|
|
646
|
+
sc.spawn = stateSpawn;
|
|
647
|
+
out[name] = sc;
|
|
648
|
+
}
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
// ─── per-state pr block (issue 139) ────────────────────────────────────────────
|
|
652
|
+
function parseStatePrBlock(stateName, raw) {
|
|
653
|
+
if (raw === undefined || raw === null)
|
|
654
|
+
return undefined;
|
|
655
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
656
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr must be a map (auto_merge / on_conflict / close)`);
|
|
657
|
+
}
|
|
658
|
+
const m = raw;
|
|
659
|
+
const out = {};
|
|
660
|
+
if (m['auto_merge'] !== undefined && m['auto_merge'] !== null) {
|
|
661
|
+
const s = asString(m['auto_merge']);
|
|
662
|
+
if (s !== 'squash' && s !== 'merge' && s !== 'rebase') {
|
|
663
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.auto_merge must be one of squash|merge|rebase`);
|
|
664
|
+
}
|
|
665
|
+
out.auto_merge = s;
|
|
666
|
+
}
|
|
667
|
+
if (m['on_conflict'] !== undefined && m['on_conflict'] !== null) {
|
|
668
|
+
const oc = m['on_conflict'];
|
|
669
|
+
if (typeof oc !== 'object' || Array.isArray(oc)) {
|
|
670
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.on_conflict must be a map with a route_to field`);
|
|
671
|
+
}
|
|
672
|
+
const routeTo = asString(oc['route_to']);
|
|
673
|
+
if (!routeTo || routeTo.trim().length === 0) {
|
|
674
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.on_conflict.route_to must be a non-empty state name`);
|
|
675
|
+
}
|
|
676
|
+
out.on_conflict = { route_to: routeTo.trim() };
|
|
677
|
+
}
|
|
678
|
+
if (m['close'] !== undefined && m['close'] !== null) {
|
|
679
|
+
if (typeof m['close'] !== 'boolean') {
|
|
680
|
+
throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.close must be a boolean`);
|
|
681
|
+
}
|
|
682
|
+
if (m['close'] === true)
|
|
683
|
+
out.close = true;
|
|
684
|
+
}
|
|
685
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
686
|
+
}
|
|
687
|
+
//# sourceMappingURL=parse.js.map
|