smol-symphony 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +125 -39
- package/DESIGN.md +494 -273
- package/PRODUCT.md +2 -1
- package/README.md +259 -110
- package/SPEC.md +565 -1928
- package/WORKFLOW.minimal.yaml +34 -0
- package/WORKFLOW.template.yaml +1100 -0
- package/WORKFLOW.yaml +487 -0
- package/assets/skills/symphony-issues/SKILL.md +136 -0
- package/assets/symphony-mise.system.toml +68 -0
- package/dist/src/bin/symphony.js +30 -0
- package/dist/src/bin/symphony.js.map +1 -0
- package/dist/src/core/actions/context.js +109 -0
- package/dist/src/core/actions/context.js.map +1 -0
- package/dist/src/core/actions/parse.js +192 -0
- package/dist/src/core/actions/parse.js.map +1 -0
- package/dist/src/core/actions/plan.js +197 -0
- package/dist/src/core/actions/plan.js.map +1 -0
- package/dist/src/core/actions/predicates.js +111 -0
- package/dist/src/core/actions/predicates.js.map +1 -0
- package/dist/src/core/actions/run-fold.js +248 -0
- package/dist/src/core/actions/run-fold.js.map +1 -0
- package/dist/src/core/actions/template.js +118 -0
- package/dist/src/core/actions/template.js.map +1 -0
- package/dist/src/core/cli/args.js +116 -0
- package/dist/src/core/cli/args.js.map +1 -0
- package/dist/src/core/coerce.js +75 -0
- package/dist/src/core/coerce.js.map +1 -0
- package/dist/src/core/credential/account-id.js +20 -0
- package/dist/src/core/credential/account-id.js.map +1 -0
- package/dist/src/core/credential/adapter-config.js +136 -0
- package/dist/src/core/credential/adapter-config.js.map +1 -0
- package/dist/src/core/credential/availability.js +98 -0
- package/dist/src/core/credential/availability.js.map +1 -0
- package/dist/src/core/credential/extract.js +228 -0
- package/dist/src/core/credential/extract.js.map +1 -0
- package/dist/src/core/credential/fake-creds.js +171 -0
- package/dist/src/core/credential/fake-creds.js.map +1 -0
- package/dist/src/core/credential/identity.js +125 -0
- package/dist/src/core/credential/identity.js.map +1 -0
- package/dist/src/core/credential/shape.js +230 -0
- package/dist/src/core/credential/shape.js.map +1 -0
- package/dist/src/core/credential/strings.js +15 -0
- package/dist/src/core/credential/strings.js.map +1 -0
- package/dist/src/core/doctor/checks.js +303 -0
- package/dist/src/core/doctor/checks.js.map +1 -0
- package/dist/src/core/git/result.js +107 -0
- package/dist/src/core/git/result.js.map +1 -0
- package/dist/src/core/http/decisions.js +225 -0
- package/dist/src/core/http/decisions.js.map +1 -0
- package/dist/src/core/http/render.js +1635 -0
- package/dist/src/core/http/render.js.map +1 -0
- package/dist/src/core/http/routes.js +145 -0
- package/dist/src/core/http/routes.js.map +1 -0
- package/dist/src/core/http/views.js +181 -0
- package/dist/src/core/http/views.js.map +1 -0
- package/dist/src/core/image/managed-image.js +95 -0
- package/dist/src/core/image/managed-image.js.map +1 -0
- package/dist/src/core/issue/file.js +149 -0
- package/dist/src/core/issue/file.js.map +1 -0
- package/dist/src/core/issue/parse.js +210 -0
- package/dist/src/core/issue/parse.js.map +1 -0
- package/dist/src/core/mcp/dispatch.js +239 -0
- package/dist/src/core/mcp/dispatch.js.map +1 -0
- package/dist/src/core/mcp/post-move.js +92 -0
- package/dist/src/core/mcp/post-move.js.map +1 -0
- package/dist/src/core/mcp/protocol.js +293 -0
- package/dist/src/core/mcp/protocol.js.map +1 -0
- package/dist/src/core/mcp/url.js +162 -0
- package/dist/src/core/mcp/url.js.map +1 -0
- package/dist/src/core/path.js +63 -0
- package/dist/src/core/path.js.map +1 -0
- package/dist/src/core/reconcile/image-decide.js +48 -0
- package/dist/src/core/reconcile/image-decide.js.map +1 -0
- package/dist/src/core/reconcile/ledger.js +142 -0
- package/dist/src/core/reconcile/ledger.js.map +1 -0
- package/dist/src/core/reconcile/pr-classify.js +62 -0
- package/dist/src/core/reconcile/pr-classify.js.map +1 -0
- package/dist/src/core/reconcile/pr-decide.js +180 -0
- package/dist/src/core/reconcile/pr-decide.js.map +1 -0
- package/dist/src/core/reconcile/pr-loop.js +161 -0
- package/dist/src/core/reconcile/pr-loop.js.map +1 -0
- package/dist/src/core/reconcile/pr-notes.js +35 -0
- package/dist/src/core/reconcile/pr-notes.js.map +1 -0
- package/dist/src/core/reconcile/vm-decide.js +70 -0
- package/dist/src/core/reconcile/vm-decide.js.map +1 -0
- package/dist/src/core/reconcile/vm-reap.js +207 -0
- package/dist/src/core/reconcile/vm-reap.js.map +1 -0
- package/dist/src/core/reconcile/workspace-decide.js +162 -0
- package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/core/runlog/summary.js +231 -0
- package/dist/src/core/runlog/summary.js.map +1 -0
- package/dist/src/core/runner/dispatch-config.js +95 -0
- package/dist/src/core/runner/dispatch-config.js.map +1 -0
- package/dist/src/core/runner/injection.js +61 -0
- package/dist/src/core/runner/injection.js.map +1 -0
- package/dist/src/core/runner/mise.js +210 -0
- package/dist/src/core/runner/mise.js.map +1 -0
- package/dist/src/core/runner/prompt.js +720 -0
- package/dist/src/core/runner/prompt.js.map +1 -0
- package/dist/src/core/runner/turn.js +242 -0
- package/dist/src/core/runner/turn.js.map +1 -0
- package/dist/src/core/runner/vm-plan.js +390 -0
- package/dist/src/core/runner/vm-plan.js.map +1 -0
- package/dist/src/core/schedule/admission.js +123 -0
- package/dist/src/core/schedule/admission.js.map +1 -0
- package/dist/src/core/schedule/circuit-breaker.js +111 -0
- package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/core/schedule/eligibility.js +83 -0
- package/dist/src/core/schedule/eligibility.js.map +1 -0
- package/dist/src/core/schedule/reconcile-issue.js +82 -0
- package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
- package/dist/src/core/schedule/retry.js +96 -0
- package/dist/src/core/schedule/retry.js.map +1 -0
- package/dist/src/core/schedule/sleep-cycle.js +133 -0
- package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/core/schedule/slots.js +124 -0
- package/dist/src/core/schedule/slots.js.map +1 -0
- package/dist/src/core/schedule/tick.js +553 -0
- package/dist/src/core/schedule/tick.js.map +1 -0
- package/dist/src/core/schedule/token-fold.js +181 -0
- package/dist/src/core/schedule/token-fold.js.map +1 -0
- package/dist/src/core/state-resolve.js +86 -0
- package/dist/src/core/state-resolve.js.map +1 -0
- package/dist/src/core/vm-guards.js +278 -0
- package/dist/src/core/vm-guards.js.map +1 -0
- package/dist/src/core/workflow/derive.js +107 -0
- package/dist/src/core/workflow/derive.js.map +1 -0
- package/dist/src/core/workflow/parse.js +687 -0
- package/dist/src/core/workflow/parse.js.map +1 -0
- package/dist/src/core/workflow/prompt-probe.js +78 -0
- package/dist/src/core/workflow/prompt-probe.js.map +1 -0
- package/dist/src/core/workflow/validate.js +189 -0
- package/dist/src/core/workflow/validate.js.map +1 -0
- package/dist/src/core/workspace-key.js +19 -0
- package/dist/src/core/workspace-key.js.map +1 -0
- package/dist/src/shell/actions-runner.js +356 -0
- package/dist/src/shell/actions-runner.js.map +1 -0
- package/dist/src/shell/adapter/adapter-registry.js +45 -0
- package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
- package/dist/src/shell/adapter/clock-random.js +96 -0
- package/dist/src/shell/adapter/clock-random.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
- package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
- package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
- package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
- package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/shell/adapter/launcher-asset.js +57 -0
- package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
- package/dist/src/shell/adapter/mise-config-asset.js +65 -0
- package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
- package/dist/src/shell/adapter/workflow-loader.js +304 -0
- package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
- package/dist/src/shell/cli/doctor.js +268 -0
- package/dist/src/shell/cli/doctor.js.map +1 -0
- package/dist/src/shell/effect-interpreter-families.js +314 -0
- package/dist/src/shell/effect-interpreter-families.js.map +1 -0
- package/dist/src/shell/effect-interpreter.js +29 -0
- package/dist/src/shell/effect-interpreter.js.map +1 -0
- package/dist/src/shell/interp/acp-frame.js +137 -0
- package/dist/src/shell/interp/acp-frame.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-conn.js +320 -0
- package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
- package/dist/src/shell/interp/acp-ws-frames.js +159 -0
- package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
- package/dist/src/shell/interp/acp-ws.js +197 -0
- package/dist/src/shell/interp/acp-ws.js.map +1 -0
- package/dist/src/shell/interp/acp.js +319 -0
- package/dist/src/shell/interp/acp.js.map +1 -0
- package/dist/src/shell/interp/credential-defaults.js +128 -0
- package/dist/src/shell/interp/credential-defaults.js.map +1 -0
- package/dist/src/shell/interp/credential-hooks.js +149 -0
- package/dist/src/shell/interp/credential-hooks.js.map +1 -0
- package/dist/src/shell/interp/credential-registry.js +226 -0
- package/dist/src/shell/interp/credential-registry.js.map +1 -0
- package/dist/src/shell/interp/credential.js +103 -0
- package/dist/src/shell/interp/credential.js.map +1 -0
- package/dist/src/shell/interp/gh.js +163 -0
- package/dist/src/shell/interp/gh.js.map +1 -0
- package/dist/src/shell/interp/git.js +28 -0
- package/dist/src/shell/interp/git.js.map +1 -0
- package/dist/src/shell/interp/log.js +213 -0
- package/dist/src/shell/interp/log.js.map +1 -0
- package/dist/src/shell/interp/process.js +178 -0
- package/dist/src/shell/interp/process.js.map +1 -0
- package/dist/src/shell/interp/runlog.js +193 -0
- package/dist/src/shell/interp/runlog.js.map +1 -0
- package/dist/src/shell/interp/timer.js +64 -0
- package/dist/src/shell/interp/timer.js.map +1 -0
- package/dist/src/shell/interp/tracker-disk.js +99 -0
- package/dist/src/shell/interp/tracker-disk.js.map +1 -0
- package/dist/src/shell/interp/tracker-parse.js +71 -0
- package/dist/src/shell/interp/tracker-parse.js.map +1 -0
- package/dist/src/shell/interp/tracker-scan.js +238 -0
- package/dist/src/shell/interp/tracker-scan.js.map +1 -0
- package/dist/src/shell/interp/tracker-write.js +91 -0
- package/dist/src/shell/interp/tracker-write.js.map +1 -0
- package/dist/src/shell/interp/tracker.js +41 -0
- package/dist/src/shell/interp/tracker.js.map +1 -0
- package/dist/src/shell/interp/tty.js +48 -0
- package/dist/src/shell/interp/tty.js.map +1 -0
- package/dist/src/shell/interp/vm.js +199 -0
- package/dist/src/shell/interp/vm.js.map +1 -0
- package/dist/src/shell/interp/workspace.js +310 -0
- package/dist/src/shell/interp/workspace.js.map +1 -0
- package/dist/src/shell/main-acp.js +78 -0
- package/dist/src/shell/main-acp.js.map +1 -0
- package/dist/src/shell/main-adapters.js +222 -0
- package/dist/src/shell/main-adapters.js.map +1 -0
- package/dist/src/shell/main-credential.js +122 -0
- package/dist/src/shell/main-credential.js.map +1 -0
- package/dist/src/shell/main-doctor.js +22 -0
- package/dist/src/shell/main-doctor.js.map +1 -0
- package/dist/src/shell/main-entry.js +46 -0
- package/dist/src/shell/main-entry.js.map +1 -0
- package/dist/src/shell/main-http-csrf.js +45 -0
- package/dist/src/shell/main-http-csrf.js.map +1 -0
- package/dist/src/shell/main-http-handler.js +389 -0
- package/dist/src/shell/main-http-handler.js.map +1 -0
- package/dist/src/shell/main-http-mcp.js +122 -0
- package/dist/src/shell/main-http-mcp.js.map +1 -0
- package/dist/src/shell/main-http-views.js +253 -0
- package/dist/src/shell/main-http-views.js.map +1 -0
- package/dist/src/shell/main-http.js +76 -0
- package/dist/src/shell/main-http.js.map +1 -0
- package/dist/src/shell/main-loops.js +130 -0
- package/dist/src/shell/main-loops.js.map +1 -0
- package/dist/src/shell/main-mcp.js +129 -0
- package/dist/src/shell/main-mcp.js.map +1 -0
- package/dist/src/shell/main-orchestrator.js +120 -0
- package/dist/src/shell/main-orchestrator.js.map +1 -0
- package/dist/src/shell/main-preflight.js +43 -0
- package/dist/src/shell/main-preflight.js.map +1 -0
- package/dist/src/shell/main-reconcilers-helpers.js +244 -0
- package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
- package/dist/src/shell/main-reconcilers-pr.js +148 -0
- package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
- package/dist/src/shell/main-reconcilers.js +225 -0
- package/dist/src/shell/main-reconcilers.js.map +1 -0
- package/dist/src/shell/main-runner.js +355 -0
- package/dist/src/shell/main-runner.js.map +1 -0
- package/dist/src/shell/main-scaffold.js +116 -0
- package/dist/src/shell/main-scaffold.js.map +1 -0
- package/dist/src/shell/main-shutdown.js +115 -0
- package/dist/src/shell/main-shutdown.js.map +1 -0
- package/dist/src/shell/main-startup.js +48 -0
- package/dist/src/shell/main-startup.js.map +1 -0
- package/dist/src/shell/main-substrates.js +43 -0
- package/dist/src/shell/main-substrates.js.map +1 -0
- package/dist/src/shell/main.js +385 -0
- package/dist/src/shell/main.js.map +1 -0
- package/dist/src/shell/orchestrator-feedback.js +69 -0
- package/dist/src/shell/orchestrator-feedback.js.map +1 -0
- package/dist/src/shell/orchestrator-image.js +167 -0
- package/dist/src/shell/orchestrator-image.js.map +1 -0
- package/dist/src/shell/orchestrator-loop.js +468 -0
- package/dist/src/shell/orchestrator-loop.js.map +1 -0
- package/dist/src/shell/orchestrator-reconcile.js +36 -0
- package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
- package/dist/src/shell/reconciler-loop.js +228 -0
- package/dist/src/shell/reconciler-loop.js.map +1 -0
- package/dist/src/shell/runner-loop-turn.js +301 -0
- package/dist/src/shell/runner-loop-turn.js.map +1 -0
- package/dist/src/shell/runner-loop.js +338 -0
- package/dist/src/shell/runner-loop.js.map +1 -0
- package/dist/src/shell/server/http.js +208 -0
- package/dist/src/shell/server/http.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
- package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
- package/dist/src/shell/server/mcp-runtime.js +99 -0
- package/dist/src/shell/server/mcp-runtime.js.map +1 -0
- package/dist/src/shell/workspace-key.js +14 -0
- package/dist/src/shell/workspace-key.js.map +1 -0
- package/dist/src/types/acp.js +8 -0
- package/dist/src/types/acp.js.map +1 -0
- package/dist/src/types/actions/plan.js +6 -0
- package/dist/src/types/actions/plan.js.map +1 -0
- package/dist/src/types/actions/predicates.js +6 -0
- package/dist/src/types/actions/predicates.js.map +1 -0
- package/dist/src/types/actions/run-fold.js +8 -0
- package/dist/src/types/actions/run-fold.js.map +1 -0
- package/dist/src/types/actions.js +7 -0
- package/dist/src/types/actions.js.map +1 -0
- package/dist/src/types/adapter/clock-random.js +4 -0
- package/dist/src/types/adapter/clock-random.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
- package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
- package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
- package/dist/src/types/adapter/workflow-loader.js +4 -0
- package/dist/src/types/adapter/workflow-loader.js.map +1 -0
- package/dist/src/types/cli/args.js +8 -0
- package/dist/src/types/cli/args.js.map +1 -0
- package/dist/src/types/config.js +8 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/credential-interp.js +6 -0
- package/dist/src/types/credential-interp.js.map +1 -0
- package/dist/src/types/credentials.js +10 -0
- package/dist/src/types/credentials.js.map +1 -0
- package/dist/src/types/doctor.js +7 -0
- package/dist/src/types/doctor.js.map +1 -0
- package/dist/src/types/domain.js +7 -0
- package/dist/src/types/domain.js.map +1 -0
- package/dist/src/types/effect.js +15 -0
- package/dist/src/types/effect.js.map +1 -0
- package/dist/src/types/errors.js +39 -0
- package/dist/src/types/errors.js.map +1 -0
- package/dist/src/types/http/decisions.js +6 -0
- package/dist/src/types/http/decisions.js.map +1 -0
- package/dist/src/types/http/render.js +10 -0
- package/dist/src/types/http/render.js.map +1 -0
- package/dist/src/types/http/views.js +6 -0
- package/dist/src/types/http/views.js.map +1 -0
- package/dist/src/types/http.js +9 -0
- package/dist/src/types/http.js.map +1 -0
- package/dist/src/types/image/managed-image.js +7 -0
- package/dist/src/types/image/managed-image.js.map +1 -0
- package/dist/src/types/interp/effect-interpreter.js +8 -0
- package/dist/src/types/interp/effect-interpreter.js.map +1 -0
- package/dist/src/types/interp/tracker.js +7 -0
- package/dist/src/types/interp/tracker.js.map +1 -0
- package/dist/src/types/issue/file.js +6 -0
- package/dist/src/types/issue/file.js.map +1 -0
- package/dist/src/types/issue/parse.js +8 -0
- package/dist/src/types/issue/parse.js.map +1 -0
- package/dist/src/types/main-acp.js +13 -0
- package/dist/src/types/main-acp.js.map +1 -0
- package/dist/src/types/main-adapters.js +5 -0
- package/dist/src/types/main-adapters.js.map +1 -0
- package/dist/src/types/main-credential.js +21 -0
- package/dist/src/types/main-credential.js.map +1 -0
- package/dist/src/types/main-doctor.js +6 -0
- package/dist/src/types/main-doctor.js.map +1 -0
- package/dist/src/types/main-http-handler.js +12 -0
- package/dist/src/types/main-http-handler.js.map +1 -0
- package/dist/src/types/main-http.js +5 -0
- package/dist/src/types/main-http.js.map +1 -0
- package/dist/src/types/main-loops.js +5 -0
- package/dist/src/types/main-loops.js.map +1 -0
- package/dist/src/types/main-mcp.js +12 -0
- package/dist/src/types/main-mcp.js.map +1 -0
- package/dist/src/types/main-orchestrator.js +5 -0
- package/dist/src/types/main-orchestrator.js.map +1 -0
- package/dist/src/types/main-reconcilers.js +11 -0
- package/dist/src/types/main-reconcilers.js.map +1 -0
- package/dist/src/types/main-runner.js +13 -0
- package/dist/src/types/main-runner.js.map +1 -0
- package/dist/src/types/main-startup.js +5 -0
- package/dist/src/types/main-startup.js.map +1 -0
- package/dist/src/types/main-substrates.js +5 -0
- package/dist/src/types/main-substrates.js.map +1 -0
- package/dist/src/types/mcp/dispatch.js +4 -0
- package/dist/src/types/mcp/dispatch.js.map +1 -0
- package/dist/src/types/mcp/post-move.js +7 -0
- package/dist/src/types/mcp/post-move.js.map +1 -0
- package/dist/src/types/mcp.js +9 -0
- package/dist/src/types/mcp.js.map +1 -0
- package/dist/src/types/ports.js +12 -0
- package/dist/src/types/ports.js.map +1 -0
- package/dist/src/types/reconcile/image-decide.js +5 -0
- package/dist/src/types/reconcile/image-decide.js.map +1 -0
- package/dist/src/types/reconcile/ledger.js +7 -0
- package/dist/src/types/reconcile/ledger.js.map +1 -0
- package/dist/src/types/reconcile/pr-loop.js +8 -0
- package/dist/src/types/reconcile/pr-loop.js.map +1 -0
- package/dist/src/types/reconcile/vm-reap.js +8 -0
- package/dist/src/types/reconcile/vm-reap.js.map +1 -0
- package/dist/src/types/reconcile/workspace-decide.js +7 -0
- package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
- package/dist/src/types/reconcile.js +9 -0
- package/dist/src/types/reconcile.js.map +1 -0
- package/dist/src/types/runlog.js +7 -0
- package/dist/src/types/runlog.js.map +1 -0
- package/dist/src/types/runner/actions-runner.js +12 -0
- package/dist/src/types/runner/actions-runner.js.map +1 -0
- package/dist/src/types/runner/gondolin-dispatch.js +5 -0
- package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
- package/dist/src/types/runner/injection.js +6 -0
- package/dist/src/types/runner/injection.js.map +1 -0
- package/dist/src/types/runner/runner-loop.js +5 -0
- package/dist/src/types/runner/runner-loop.js.map +1 -0
- package/dist/src/types/runner/turn.js +4 -0
- package/dist/src/types/runner/turn.js.map +1 -0
- package/dist/src/types/runner/vm-plan.js +4 -0
- package/dist/src/types/runner/vm-plan.js.map +1 -0
- package/dist/src/types/runtime.js +9 -0
- package/dist/src/types/runtime.js.map +1 -0
- package/dist/src/types/schedule/admission.js +7 -0
- package/dist/src/types/schedule/admission.js.map +1 -0
- package/dist/src/types/schedule/circuit-breaker.js +2 -0
- package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
- package/dist/src/types/schedule/eligibility.js +9 -0
- package/dist/src/types/schedule/eligibility.js.map +1 -0
- package/dist/src/types/schedule/orchestrator-loop.js +10 -0
- package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
- package/dist/src/types/schedule/sleep-cycle.js +4 -0
- package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
- package/dist/src/types/schedule/slots.js +8 -0
- package/dist/src/types/schedule/slots.js.map +1 -0
- package/dist/src/types/schedule/tick.js +9 -0
- package/dist/src/types/schedule/tick.js.map +1 -0
- package/dist/src/types/server/mcp-runtime.js +8 -0
- package/dist/src/types/server/mcp-runtime.js.map +1 -0
- package/dist/src/types/workflow/parse.js +4 -0
- package/dist/src/types/workflow/parse.js.map +1 -0
- package/dist/tests/core/account-id.test.js +35 -0
- package/dist/tests/core/account-id.test.js.map +1 -0
- package/dist/tests/core/actions-parse.test.js +176 -0
- package/dist/tests/core/actions-parse.test.js.map +1 -0
- package/dist/tests/core/adapter-config.test.js +133 -0
- package/dist/tests/core/adapter-config.test.js.map +1 -0
- package/dist/tests/core/admission.test.js +215 -0
- package/dist/tests/core/admission.test.js.map +1 -0
- package/dist/tests/core/args.test.js +132 -0
- package/dist/tests/core/args.test.js.map +1 -0
- package/dist/tests/core/availability.test.js +62 -0
- package/dist/tests/core/availability.test.js.map +1 -0
- package/dist/tests/core/checks.test.js +395 -0
- package/dist/tests/core/checks.test.js.map +1 -0
- package/dist/tests/core/circuit-breaker.test.js +172 -0
- package/dist/tests/core/circuit-breaker.test.js.map +1 -0
- package/dist/tests/core/coerce.test.js +87 -0
- package/dist/tests/core/coerce.test.js.map +1 -0
- package/dist/tests/core/context.test.js +228 -0
- package/dist/tests/core/context.test.js.map +1 -0
- package/dist/tests/core/decisions.test.js +310 -0
- package/dist/tests/core/decisions.test.js.map +1 -0
- package/dist/tests/core/derive.test.js +205 -0
- package/dist/tests/core/derive.test.js.map +1 -0
- package/dist/tests/core/dispatch-config.test.js +164 -0
- package/dist/tests/core/dispatch-config.test.js.map +1 -0
- package/dist/tests/core/dispatch.test.js +302 -0
- package/dist/tests/core/dispatch.test.js.map +1 -0
- package/dist/tests/core/eligibility.test.js +163 -0
- package/dist/tests/core/eligibility.test.js.map +1 -0
- package/dist/tests/core/extract.test.js +139 -0
- package/dist/tests/core/extract.test.js.map +1 -0
- package/dist/tests/core/fake-creds.test.js +134 -0
- package/dist/tests/core/fake-creds.test.js.map +1 -0
- package/dist/tests/core/file.test.js +197 -0
- package/dist/tests/core/file.test.js.map +1 -0
- package/dist/tests/core/git-result.test.js +113 -0
- package/dist/tests/core/git-result.test.js.map +1 -0
- package/dist/tests/core/identity.test.js +180 -0
- package/dist/tests/core/identity.test.js.map +1 -0
- package/dist/tests/core/image-decide.test.js +59 -0
- package/dist/tests/core/image-decide.test.js.map +1 -0
- package/dist/tests/core/injection.test.js +163 -0
- package/dist/tests/core/injection.test.js.map +1 -0
- package/dist/tests/core/ledger.test.js +218 -0
- package/dist/tests/core/ledger.test.js.map +1 -0
- package/dist/tests/core/managed-image.test.js +68 -0
- package/dist/tests/core/managed-image.test.js.map +1 -0
- package/dist/tests/core/mise.test.js +138 -0
- package/dist/tests/core/mise.test.js.map +1 -0
- package/dist/tests/core/parse.test.js +174 -0
- package/dist/tests/core/parse.test.js.map +1 -0
- package/dist/tests/core/path.test.js +50 -0
- package/dist/tests/core/path.test.js.map +1 -0
- package/dist/tests/core/plan.test.js +218 -0
- package/dist/tests/core/plan.test.js.map +1 -0
- package/dist/tests/core/post-move.test.js +162 -0
- package/dist/tests/core/post-move.test.js.map +1 -0
- package/dist/tests/core/pr-classify.test.js +117 -0
- package/dist/tests/core/pr-classify.test.js.map +1 -0
- package/dist/tests/core/pr-decide.test.js +298 -0
- package/dist/tests/core/pr-decide.test.js.map +1 -0
- package/dist/tests/core/pr-loop.test.js +301 -0
- package/dist/tests/core/pr-loop.test.js.map +1 -0
- package/dist/tests/core/pr-notes.test.js +165 -0
- package/dist/tests/core/pr-notes.test.js.map +1 -0
- package/dist/tests/core/predicates.test.js +154 -0
- package/dist/tests/core/predicates.test.js.map +1 -0
- package/dist/tests/core/prompt.test.js +189 -0
- package/dist/tests/core/prompt.test.js.map +1 -0
- package/dist/tests/core/protocol.test.js +195 -0
- package/dist/tests/core/protocol.test.js.map +1 -0
- package/dist/tests/core/reconcile-issue.test.js +116 -0
- package/dist/tests/core/reconcile-issue.test.js.map +1 -0
- package/dist/tests/core/render.test.js +549 -0
- package/dist/tests/core/render.test.js.map +1 -0
- package/dist/tests/core/retry.test.js +186 -0
- package/dist/tests/core/retry.test.js.map +1 -0
- package/dist/tests/core/routes.test.js +247 -0
- package/dist/tests/core/routes.test.js.map +1 -0
- package/dist/tests/core/run-fold.test.js +299 -0
- package/dist/tests/core/run-fold.test.js.map +1 -0
- package/dist/tests/core/shape.test.js +185 -0
- package/dist/tests/core/shape.test.js.map +1 -0
- package/dist/tests/core/sleep-cycle.test.js +150 -0
- package/dist/tests/core/sleep-cycle.test.js.map +1 -0
- package/dist/tests/core/slots.test.js +201 -0
- package/dist/tests/core/slots.test.js.map +1 -0
- package/dist/tests/core/state-resolve.test.js +80 -0
- package/dist/tests/core/state-resolve.test.js.map +1 -0
- package/dist/tests/core/summary.test.js +200 -0
- package/dist/tests/core/summary.test.js.map +1 -0
- package/dist/tests/core/template.test.js +116 -0
- package/dist/tests/core/template.test.js.map +1 -0
- package/dist/tests/core/tick.test.js +558 -0
- package/dist/tests/core/tick.test.js.map +1 -0
- package/dist/tests/core/token-fold.test.js +176 -0
- package/dist/tests/core/token-fold.test.js.map +1 -0
- package/dist/tests/core/turn.test.js +388 -0
- package/dist/tests/core/turn.test.js.map +1 -0
- package/dist/tests/core/url.test.js +118 -0
- package/dist/tests/core/url.test.js.map +1 -0
- package/dist/tests/core/validate.test.js +247 -0
- package/dist/tests/core/validate.test.js.map +1 -0
- package/dist/tests/core/views.test.js +252 -0
- package/dist/tests/core/views.test.js.map +1 -0
- package/dist/tests/core/vm-decide.test.js +110 -0
- package/dist/tests/core/vm-decide.test.js.map +1 -0
- package/dist/tests/core/vm-guards.test.js +153 -0
- package/dist/tests/core/vm-guards.test.js.map +1 -0
- package/dist/tests/core/vm-plan.test.js +332 -0
- package/dist/tests/core/vm-plan.test.js.map +1 -0
- package/dist/tests/core/vm-reap.test.js +196 -0
- package/dist/tests/core/vm-reap.test.js.map +1 -0
- package/dist/tests/core/workflow-parse.test.js +493 -0
- package/dist/tests/core/workflow-parse.test.js.map +1 -0
- package/dist/tests/core/workspace-decide.test.js +236 -0
- package/dist/tests/core/workspace-decide.test.js.map +1 -0
- package/dist/tests/helpers/fixtures.js +167 -0
- package/dist/tests/helpers/fixtures.js.map +1 -0
- package/dist/tests/shell/acp-substrate.test.js +101 -0
- package/dist/tests/shell/acp-substrate.test.js.map +1 -0
- package/dist/tests/shell/actions-runner-push.test.js +203 -0
- package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
- package/dist/tests/shell/credential-hooks.test.js +36 -0
- package/dist/tests/shell/credential-hooks.test.js.map +1 -0
- package/dist/tests/shell/credential-registry.test.js +165 -0
- package/dist/tests/shell/credential-registry.test.js.map +1 -0
- package/dist/tests/shell/credential-substrate.test.js +179 -0
- package/dist/tests/shell/credential-substrate.test.js.map +1 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
- package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
- package/dist/tests/shell/doctor.test.js +101 -0
- package/dist/tests/shell/doctor.test.js.map +1 -0
- package/dist/tests/shell/effect-vm-create.test.js +52 -0
- package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
- package/dist/tests/shell/gh-port.test.js +63 -0
- package/dist/tests/shell/gh-port.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
- package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
- package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
- package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
- package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
- package/dist/tests/shell/http-handler.test.js +608 -0
- package/dist/tests/shell/http-handler.test.js.map +1 -0
- package/dist/tests/shell/http-server.test.js +53 -0
- package/dist/tests/shell/http-server.test.js.map +1 -0
- package/dist/tests/shell/mcp-runtime.test.js +366 -0
- package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
- package/dist/tests/shell/mise-config-asset.test.js +87 -0
- package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
- package/dist/tests/shell/orchestrator-loop.test.js +583 -0
- package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
- package/dist/tests/shell/reconciler-passes.test.js +314 -0
- package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
- package/dist/tests/shell/runner-loop-turn.test.js +97 -0
- package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
- package/dist/tests/shell/runner-slice.test.js +536 -0
- package/dist/tests/shell/runner-slice.test.js.map +1 -0
- package/dist/tests/shell/scaffold.test.js +65 -0
- package/dist/tests/shell/scaffold.test.js.map +1 -0
- package/dist/tests/shell/tick-config.test.js +83 -0
- package/dist/tests/shell/tick-config.test.js.map +1 -0
- package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
- package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
- package/dist/tests/shell/tracker-write-issue.test.js +154 -0
- package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
- package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
- package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
- package/dist/tests/shell/workspace-live-config.test.js +140 -0
- package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
- package/package.json +31 -11
- 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 +433 -0
- package/WORKFLOW.md +0 -269
- package/WORKFLOW.template.md +0 -307
- package/dist/agent/acp.js +0 -304
- package/dist/agent/acp.js.map +0 -1
- package/dist/agent/adapters.js +0 -275
- package/dist/agent/adapters.js.map +0 -1
- package/dist/agent/codex.js +0 -439
- package/dist/agent/codex.js.map +0 -1
- package/dist/agent/runner.js +0 -394
- package/dist/agent/runner.js.map +0 -1
- package/dist/agent/smolvm.js +0 -174
- package/dist/agent/smolvm.js.map +0 -1
- package/dist/bin/symphony.js +0 -205
- package/dist/bin/symphony.js.map +0 -1
- package/dist/http.js +0 -1189
- package/dist/http.js.map +0 -1
- package/dist/logging.js +0 -45
- package/dist/logging.js.map +0 -1
- package/dist/mcp.js +0 -478
- package/dist/mcp.js.map +0 -1
- package/dist/orchestrator.js +0 -683
- package/dist/orchestrator.js.map +0 -1
- package/dist/prompt.js +0 -65
- package/dist/prompt.js.map +0 -1
- package/dist/trackers/local.js +0 -344
- 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/workflow.js +0 -385
- package/dist/workflow.js.map +0 -1
- package/dist/workspace.js +0 -196
- package/dist/workspace.js.map +0 -1
- package/scripts/build-vm.sh +0 -67
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# WORKFLOW.template.yaml — annotated reference for symphony workflow files.
|
|
3
|
+
#
|
|
4
|
+
# A workflow file is pure YAML config. The orchestrator parses it into
|
|
5
|
+
# ServiceConfig (see src/types/config.ts) and, per dispatched issue, renders the
|
|
6
|
+
# active state's prompt — its `prompt_file` wrapped in the shared
|
|
7
|
+
# `prompt.preamble_file` / `prompt.footer_file` — with the Liquid context
|
|
8
|
+
# `{ issue, attempt }`.
|
|
9
|
+
#
|
|
10
|
+
# This document lists every supported section, every option within it, the
|
|
11
|
+
# parser default, and a small example. For a complete worked example, see
|
|
12
|
+
# WORKFLOW.yaml in this repo. For the smallest thing that runs — what
|
|
13
|
+
# `npx smol-symphony` scaffolds by default — see WORKFLOW.minimal.yaml; reach for
|
|
14
|
+
# the options below only when you want a power feature it omits.
|
|
15
|
+
#
|
|
16
|
+
# Notation:
|
|
17
|
+
# • Required keys are marked (required).
|
|
18
|
+
# • Types: `string`, `int`, `bool`, `path` (string resolved relative to the
|
|
19
|
+
# workflow file's directory unless absolute), `string[]`, `map<K, V>`.
|
|
20
|
+
# • Defaults are what the parser writes when the key is absent.
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
# tracker — where issues come from.
|
|
24
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
tracker:
|
|
26
|
+
# kind (required): currently the only supported value is 'local' (markdown
|
|
27
|
+
# files under `root`, one per issue, organized by state subdirectory).
|
|
28
|
+
kind: local
|
|
29
|
+
|
|
30
|
+
# root (path): directory containing `<state>/<id>.md` files. Optional.
|
|
31
|
+
# An explicit value resolves absolute as-is, `~`-expanded against $HOME, or
|
|
32
|
+
# (relative) against the workflow file's directory.
|
|
33
|
+
# Default (unset): ~/.symphony/trackers/<project> — durable tracker state lives
|
|
34
|
+
# OUTSIDE the repo working tree by default, so it survives a delete / re-clone
|
|
35
|
+
# of the repo. <project> is the `workspace.github_repo` slug's repo name
|
|
36
|
+
# (owner/repo → repo), falling back to this workflow file's directory basename;
|
|
37
|
+
# sanitized so two different projects on one machine never collide.
|
|
38
|
+
# root: ~/.symphony/trackers/my-project
|
|
39
|
+
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
# states — per-state configuration map. REQUIRED. Every workflow must declare
|
|
42
|
+
# at least one `active`, one `terminal`, and one `holding` state; a workflow
|
|
43
|
+
# missing the `states:` block (or missing any of those roles) is rejected at
|
|
44
|
+
# parse time. This map is the only place state names and roles are configured;
|
|
45
|
+
# there are no separate active/terminal lists to keep in sync.
|
|
46
|
+
#
|
|
47
|
+
# Keys are state names; values are config objects with these fields:
|
|
48
|
+
# role (required, enum):
|
|
49
|
+
# active — orchestrator dispatches issues in this state.
|
|
50
|
+
# terminal — orchestrator treats issues in this state as complete; the
|
|
51
|
+
# workspace is removed after the run unwinds.
|
|
52
|
+
# holding — directory exists on disk, but the orchestrator never
|
|
53
|
+
# dispatches issues from it. Triage is the canonical example
|
|
54
|
+
# and the landing directory for `symphony.propose_issue`.
|
|
55
|
+
# adapter (string, optional): override the workflow-level `acp.adapter` for
|
|
56
|
+
# agents dispatched in this state. Must be a known profile (claude,
|
|
57
|
+
# codex). Both use host-side credential substitution at Gondolin
|
|
58
|
+
# egress and are startup-probed so a missing credential fails fast.
|
|
59
|
+
# claude has a single host credential file
|
|
60
|
+
# (~/.claude/.credentials.json) that is probed for readability; codex
|
|
61
|
+
# passes when either ~/.codex/auth.json holds a token (ChatGPT-OAuth
|
|
62
|
+
# tokens.access_token or a top-level OPENAI_API_KEY) or the host
|
|
63
|
+
# OPENAI_API_KEY env var is set.
|
|
64
|
+
# prompt_file (path): this state's prompt template, in its own file. The shell
|
|
65
|
+
# loader reads it and assembles `prompt.preamble_file` + this file +
|
|
66
|
+
# `prompt.footer_file` (see the top-level `prompt:` block below) into
|
|
67
|
+
# the template rendered for issues in this state. Every active
|
|
68
|
+
# (dispatched) state should declare one; a state without `prompt_file`
|
|
69
|
+
# renders the generic fallback prompt. Resolved relative to the
|
|
70
|
+
# workflow file. A missing/unparseable file fails at startup (and
|
|
71
|
+
# `symphony doctor`), not mid-dispatch.
|
|
72
|
+
# model (string, optional): override `acp.model` for this state.
|
|
73
|
+
# Blank or whitespace-only values normalize to "use the adapter
|
|
74
|
+
# default" (same as the workflow-level acp.model semantics).
|
|
75
|
+
# effort (string, optional): override `acp.effort` for this state. Same
|
|
76
|
+
# undefined-vs-null semantics as `model`: omit to inherit
|
|
77
|
+
# `acp.effort`; blank/whitespace normalizes to null ("use the
|
|
78
|
+
# adapter default for this state"). Valid values are adapter- and
|
|
79
|
+
# model-specific (see `acp.effort`).
|
|
80
|
+
# max_turns (int, optional): override `agent.max_turns` for this state.
|
|
81
|
+
# max_concurrent (int, optional): cap on agents the orchestrator runs
|
|
82
|
+
# simultaneously for issues in THIS state. Symmetric with
|
|
83
|
+
# `max_turns` — concurrency and turn budget both live on the state.
|
|
84
|
+
# Omit for "no per-state cap; only the global
|
|
85
|
+
# `agent.max_concurrent_agents` ceiling applies". The sum of every
|
|
86
|
+
# state's `max_concurrent` must not exceed that global ceiling
|
|
87
|
+
# (validated at startup).
|
|
88
|
+
# allowed_transitions (string[]|null, optional): when set, restricts which
|
|
89
|
+
# states agents in this state may transition to via the MCP
|
|
90
|
+
# `transition` tool. Each entry must be a declared state. Omit (or
|
|
91
|
+
# explicitly set to null) for "any declared state is reachable".
|
|
92
|
+
# An empty list (`allowed_transitions: []`) means "no transitions
|
|
93
|
+
# allowed out of this state" — the agent's `transition` calls will
|
|
94
|
+
# always be rejected with `transition_not_allowed`. Useful for
|
|
95
|
+
# review-style states that should pause until a human re-routes.
|
|
96
|
+
# eval_mode (bool, optional): when true, the runner adds two extra read-only
|
|
97
|
+
# bind mounts to every per-issue VM dispatched in this state so an
|
|
98
|
+
# in-VM agent can inspect symphony's own state for evaluation /
|
|
99
|
+
# debugging:
|
|
100
|
+
# • `tracker.root` → `/symphony/issues` (every issue file across
|
|
101
|
+
# every state directory)
|
|
102
|
+
# • `logs.root` → `/symphony/logs` (per-issue JSONL run-log
|
|
103
|
+
# transcripts captured by RunLog — ACP frames, stderr,
|
|
104
|
+
# typed-action output, system events — plus the compact
|
|
105
|
+
# `<key>.summary.json` outcome records the reflector reads; see
|
|
106
|
+
# the `logs:` block below)
|
|
107
|
+
# Either mount is skipped silently if the corresponding root is
|
|
108
|
+
# unset. Each VFS mount has a cost, so this is opt-in per state rather
|
|
109
|
+
# than a workflow-wide default — flip it on for a dedicated eval
|
|
110
|
+
# state, not for the routine implement/review flow. Default: false.
|
|
111
|
+
# The canonical consumer is the "sleep cycle" reflection pattern —
|
|
112
|
+
# see the SLEEP CYCLE section below the states block.
|
|
113
|
+
# actions (list, optional): typed action DAG (issue 36, reconciler v2).
|
|
114
|
+
# The single glue mechanism attached to a state — there is no shell
|
|
115
|
+
# `hooks:` surface. When set on a `terminal` state, this list runs
|
|
116
|
+
# on transition INTO the state (the canonical Done-state pair is
|
|
117
|
+
# push_branch + create_pr_if_missing). For arbitrary in-sandbox
|
|
118
|
+
# commands use a `run_in_vm` action; per-VM tooling belongs in the
|
|
119
|
+
# agent image (`images/agents/`); first-creation workspace setup is
|
|
120
|
+
# owned by the orchestrator's `setupWorkspaceDir`.
|
|
121
|
+
# Each entry is a closed-kind record:
|
|
122
|
+
#
|
|
123
|
+
# - kind: push_branch
|
|
124
|
+
# remote: origin
|
|
125
|
+
# ref: $branch
|
|
126
|
+
# if: $repo
|
|
127
|
+
#
|
|
128
|
+
# Recognized kinds:
|
|
129
|
+
# push_branch { remote, ref }
|
|
130
|
+
# create_pr_if_missing { base, head, title_from, body_from }
|
|
131
|
+
# run_in_vm { name, cmd: [...], env?, timeout? }
|
|
132
|
+
#
|
|
133
|
+
# Templating: `$varname` references the fixed ActionContext
|
|
134
|
+
# namespace ($identifier, $workspace, $branch, $base_branch,
|
|
135
|
+
# $issue_title, $issue_body, $repo, $pr_title, $pr_body_file).
|
|
136
|
+
# Unknown $vars throw at run-time (no silent "" expansion).
|
|
137
|
+
#
|
|
138
|
+
# Conditional: optional `if:` field supports three predicates
|
|
139
|
+
# - `if: $repo` (env-var-truthy)
|
|
140
|
+
# - `if: { branch_exists: <ref> }` (workspace branch)
|
|
141
|
+
# - `if: { file_present: <path> }` (workspace file)
|
|
142
|
+
#
|
|
143
|
+
# Retry: optional `on_error.retry: { count, backoff_ms }`. Default
|
|
144
|
+
# policy is 3 retries with exponential backoff starting at 1s,
|
|
145
|
+
# then abort. `on_error.then: { route_to: <state> }` reroutes the
|
|
146
|
+
# issue OUT of the action's state into <state> (which must be
|
|
147
|
+
# declared — validated at parse time) instead of aborting; the
|
|
148
|
+
# runner moves the tracker file there with the failure reason
|
|
149
|
+
# appended as notes. The canonical use is the Done handoff (issue
|
|
150
|
+
# 235): a `push_branch` / `create_pr_if_missing` that fails after
|
|
151
|
+
# its retries reroutes into a `HandoffFailed` holding state so a
|
|
152
|
+
# stranded handoff (no branch on the remote, no PR) is operator-
|
|
153
|
+
# visible instead of dead-ending silently in terminal Done. A
|
|
154
|
+
# rerouted handoff also RETAINS the per-issue workspace (the
|
|
155
|
+
# handoff-pending marker, issue 234) so the unpushed commit
|
|
156
|
+
# survives for a manual push. A `.github/workflows/`-touching push
|
|
157
|
+
# is the common trigger: the shared fine-grained PAT is not granted
|
|
158
|
+
# Workflows:write, so GitHub server-side-declines it and the routed
|
|
159
|
+
# reason is rewritten to an actionable "manual SSH push required".
|
|
160
|
+
#
|
|
161
|
+
# run_in_vm runs uncached: every dispatch into the state re-execs
|
|
162
|
+
# the command on the live per-issue VM. `name` labels its run-log
|
|
163
|
+
# output (no caching layer; the action is the plain escape hatch).
|
|
164
|
+
# pr (map, optional): PR autopilot routing for this state (issue 139).
|
|
165
|
+
# Valid only on a `terminal` state, and only acts when the
|
|
166
|
+
# top-level `pr:` engine block (below) has `enabled: true`. This is
|
|
167
|
+
# where the routing the engine used to name by string now lives —
|
|
168
|
+
# on the state it describes. Two shapes:
|
|
169
|
+
# • Merge state: pr: { auto_merge: squash|merge|rebase,
|
|
170
|
+
# on_conflict: { route_to: <active state> } }
|
|
171
|
+
# A MERGEABLE PR for an issue in this state has GitHub
|
|
172
|
+
# auto-merge armed with `auto_merge`; a CONFLICTING one is
|
|
173
|
+
# routed back to `on_conflict.route_to` (defaults to the first
|
|
174
|
+
# declared active state when omitted) for the dispatched agent
|
|
175
|
+
# to rebase. While the engine is enabled, transitions INTO this
|
|
176
|
+
# state do not fire the standard terminal workspace cleanup —
|
|
177
|
+
# the pr resource owns the workspace until the PR merges/closes.
|
|
178
|
+
# • Close state: pr: { close: true }
|
|
179
|
+
# An open PR for an issue in this state is closed without merge
|
|
180
|
+
# and its remote branch is best-effort-deleted. Standard
|
|
181
|
+
# terminal cleanup still runs (the close path needs no workspace).
|
|
182
|
+
# The merge/close/route targets are DERIVED by scanning states for
|
|
183
|
+
# this field — there is no named-string sibling block. At most one
|
|
184
|
+
# terminal state may declare `auto_merge`, and at most one may
|
|
185
|
+
# declare `close`; an `on_conflict.route_to` naming an undeclared
|
|
186
|
+
# state is rejected at parse time.
|
|
187
|
+
# spawn (map, optional): recurring-spawn trigger for this state (the sleep
|
|
188
|
+
# cycle as a recurring STATE). Valid only on an `active` state. When
|
|
189
|
+
# a trigger fires the orchestrator MINTS a FRESH ephemeral issue into
|
|
190
|
+
# this state (it runs once and terminates in a dedicated terminal),
|
|
191
|
+
# instead of re-arming one immortal parked ticket. Fields:
|
|
192
|
+
# • on_idle (bool): spawn when the orchestrator is idle AND >=1
|
|
193
|
+
# issue reached a terminal state since the last cycle (the ">=1
|
|
194
|
+
# since last cycle" gate is load-bearing — without it an idle
|
|
195
|
+
# orchestrator re-spawns in a tight loop with nothing new to
|
|
196
|
+
# mine). Default false.
|
|
197
|
+
# • after_terminal (int): a backstop for busy stretches that never
|
|
198
|
+
# go idle — spawn once this many issues have reached a terminal
|
|
199
|
+
# state (Done/Cancelled/the spawn's terminal) since the last
|
|
200
|
+
# cycle. 0 disables the count trigger. Default 0.
|
|
201
|
+
# • title (string): title template for the minted issue;
|
|
202
|
+
# `{{ stamp }}` → the mint timestamp (e.g. "2026-06-03 12:00").
|
|
203
|
+
# Default "Reflection {{ stamp }}".
|
|
204
|
+
# • body (string, optional): lead-in line for the GENERATED
|
|
205
|
+
# provenance body (the trigger + counter + timestamp the static
|
|
206
|
+
# prompt can't know are always appended). Default: a generic
|
|
207
|
+
# "Auto-spawned reflection cycle." lead-in.
|
|
208
|
+
# • max_in_flight (int): never mint a second issue while this many
|
|
209
|
+
# are already live in the spawn state. Load-bearing — it replaces
|
|
210
|
+
# the immortal ticket's built-in "one location = one reflection"
|
|
211
|
+
# mutex, so a busy-stretch after_terminal trigger can't pile up
|
|
212
|
+
# cycles. Default 1.
|
|
213
|
+
# The terminal-transition counter resets to 0 on a successful mint
|
|
214
|
+
# and is held in orchestrator memory only (a restart resets it). At
|
|
215
|
+
# most one active state may declare `spawn` (so the spawn state is
|
|
216
|
+
# unambiguous). The spawn's terminal must be a DEDICATED terminal
|
|
217
|
+
# with no `pr:` / `actions:` — a minted issue runs on an empty branch,
|
|
218
|
+
# so a Done-style PR autopilot would try to open a PR off nothing.
|
|
219
|
+
# See the SLEEP CYCLE section below for the full pattern.
|
|
220
|
+
#
|
|
221
|
+
# Declaration order matters: role-filtered listings (active states, terminal
|
|
222
|
+
# states) follow it, and the dashboard renders state columns in the same order.
|
|
223
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
states:
|
|
225
|
+
Todo:
|
|
226
|
+
role: active
|
|
227
|
+
prompt_file: prompts/Todo.md # this state's prompt template (see `prompt:` below)
|
|
228
|
+
adapter: claude
|
|
229
|
+
model: claude-opus-4-7
|
|
230
|
+
effort: xhigh
|
|
231
|
+
max_turns: 10
|
|
232
|
+
max_concurrent: 1 # at most one implementer agent at a time
|
|
233
|
+
Review:
|
|
234
|
+
role: active
|
|
235
|
+
prompt_file: prompts/Review.md
|
|
236
|
+
adapter: codex
|
|
237
|
+
model: gpt-5-codex
|
|
238
|
+
max_turns: 4
|
|
239
|
+
allowed_transitions: [Todo, Done]
|
|
240
|
+
Done:
|
|
241
|
+
role: terminal
|
|
242
|
+
# PR autopilot merge state (issue 139). Acts only when the top-level `pr:`
|
|
243
|
+
# engine block (below) is enabled. Derived as the merge state because it
|
|
244
|
+
# declares `auto_merge`.
|
|
245
|
+
# pr:
|
|
246
|
+
# auto_merge: squash
|
|
247
|
+
# on_conflict: { route_to: Todo }
|
|
248
|
+
# Terminal-state handoff via typed actions (issue 36). On transition into
|
|
249
|
+
# Done, push the branch (if SYMPHONY_REPO is set → $repo non-empty) and
|
|
250
|
+
# open a PR if one does not already exist. Templates resolve against the
|
|
251
|
+
# fixed ActionContext namespace; the orchestrator stages $pr_title and
|
|
252
|
+
# $pr_body_file from the issue file before firing.
|
|
253
|
+
# A handoff that fails after its retries reroutes the issue OUT of Done into
|
|
254
|
+
# the HandoffFailed holding state (issue 235) via `on_error.then.route_to`,
|
|
255
|
+
# so a stranded handoff is operator-visible instead of silently terminal-Done.
|
|
256
|
+
# actions:
|
|
257
|
+
# - kind: push_branch
|
|
258
|
+
# remote: origin
|
|
259
|
+
# ref: $branch
|
|
260
|
+
# if: $repo
|
|
261
|
+
# on_error: { then: { route_to: HandoffFailed } }
|
|
262
|
+
# - kind: create_pr_if_missing
|
|
263
|
+
# base: $base_branch
|
|
264
|
+
# head: $branch
|
|
265
|
+
# title_from: $pr_title
|
|
266
|
+
# body_from: $pr_body_file
|
|
267
|
+
# if: $repo
|
|
268
|
+
# on_error: { then: { route_to: HandoffFailed } }
|
|
269
|
+
Cancelled:
|
|
270
|
+
role: terminal
|
|
271
|
+
# PR autopilot close state (issue 139). Derived as the close state because
|
|
272
|
+
# it declares `close: true` — an open PR is closed without merge.
|
|
273
|
+
# pr:
|
|
274
|
+
# close: true
|
|
275
|
+
Triage:
|
|
276
|
+
role: holding
|
|
277
|
+
HandoffFailed:
|
|
278
|
+
# Landing state for a Done handoff that FAILED (issue 235): the failing
|
|
279
|
+
# action's `on_error.then.route_to` (above) reroutes the issue here with the
|
|
280
|
+
# reason appended. Holding → never dispatched; it parks for an operator to
|
|
281
|
+
# push the branch by hand (the workspace is retained by issue 234's marker)
|
|
282
|
+
# and move the issue back to Done, or to Cancelled. Declared AFTER Triage so
|
|
283
|
+
# Triage stays the `propose_issue` landing (the first declared holding state).
|
|
284
|
+
role: holding
|
|
285
|
+
|
|
286
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
# prompt — shared prompt-assembly files (per-state prompt split). OPTIONAL.
|
|
288
|
+
#
|
|
289
|
+
# When an active state declares a `prompt_file` (see the states block above), the
|
|
290
|
+
# shell loader builds that state's prompt by concatenating, in order:
|
|
291
|
+
#
|
|
292
|
+
# prompt.preamble_file (shared header)
|
|
293
|
+
# + the state's prompt_file (the state-specific body)
|
|
294
|
+
# + prompt.footer_file (shared footer)
|
|
295
|
+
#
|
|
296
|
+
# and renders the result — so the config reads as the shape of the workflow and
|
|
297
|
+
# each state's prompt lives in its own file under `prompts/`. The Liquid context
|
|
298
|
+
# is `{ issue, attempt }`; each file is an ordinary Liquid template fragment.
|
|
299
|
+
#
|
|
300
|
+
# Both keys are paths resolved relative to the workflow file, and both are
|
|
301
|
+
# optional: omit `preamble_file`/`footer_file` to wrap a state body in nothing.
|
|
302
|
+
# A workflow file is pure YAML config — there is no inline prompt body. Every
|
|
303
|
+
# active state names its own `prompt_file`; a state without one renders the
|
|
304
|
+
# generic fallback prompt, so give each dispatched state a `prompt_file`. See
|
|
305
|
+
# WORKFLOW.minimal.yaml for the smallest example and WORKFLOW.yaml in this repo
|
|
306
|
+
# for a full worked example.
|
|
307
|
+
#
|
|
308
|
+
# prompt:
|
|
309
|
+
# preamble_file: prompts/_preamble.md
|
|
310
|
+
# footer_file: prompts/_footer.md
|
|
311
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
314
|
+
# SLEEP CYCLE — a recurring reflection STATE that mines finished work for harness
|
|
315
|
+
# improvements (issue 122). Optional, opt-in pattern; layered on top of the
|
|
316
|
+
# states block above. The shipped smol-symphony WORKFLOW.yaml wires it for the
|
|
317
|
+
# dogfooding (symphony-on-symphony) setup.
|
|
318
|
+
#
|
|
319
|
+
# The idea: every dispatch starts from the same static prompt + config, no
|
|
320
|
+
# matter what the last 100 issues taught us about where agents stall, get
|
|
321
|
+
# rejected, burn their turn budget, or fight the harness. A periodic
|
|
322
|
+
# "reflection" turn closes that feedback loop — it reads completed-task history
|
|
323
|
+
# (the read-only mounts `eval_mode` exposes), distils *recurring* friction, and
|
|
324
|
+
# files improvement proposals against the HARNESS (this WORKFLOW.yaml's prompt
|
|
325
|
+
# branches and per-state model/max_turns/allowed_transitions/effort/actions, the
|
|
326
|
+
# gondolin image config, acceptance criteria, timeouts) — never the product code under
|
|
327
|
+
# review. Proposals land in Triage via `propose_issue`, so a human stays the
|
|
328
|
+
# gate. This is the "self-improving agent" pattern aimed at the harness rather
|
|
329
|
+
# than the product.
|
|
330
|
+
#
|
|
331
|
+
# SHAPE: recurring STATE, NOT an immortal ticket. The cycle is modelled as a
|
|
332
|
+
# recurring state that SPAWNS ephemeral reflection issues. There is no parked
|
|
333
|
+
# "Sleep cycle" ticket that ping-pongs between two states forever: when a trigger
|
|
334
|
+
# fires the orchestrator MINTS a fresh issue into the spawn state; it runs once
|
|
335
|
+
# and terminates in a dedicated terminal, like every other ticket. Two states
|
|
336
|
+
# implement it:
|
|
337
|
+
#
|
|
338
|
+
# Reflect (role: active, eval_mode: true, spawn: {...}):
|
|
339
|
+
# - eval_mode binds /symphony/issues (all state dirs, incl. the Done/*.md
|
|
340
|
+
# handoff transcripts) + /symphony/logs (per-issue JSONL run logs)
|
|
341
|
+
# read-only into the VM. No extra mount plumbing — it reuses the existing
|
|
342
|
+
# eval_mode mounts.
|
|
343
|
+
# - Give it a capable adapter/model (large context helps: a reflection turn
|
|
344
|
+
# reads many transcripts + logs) and a higher max_turns than your
|
|
345
|
+
# implement/review states.
|
|
346
|
+
# - allowed_transitions: [Reflected] — the reflector may ONLY terminate in
|
|
347
|
+
# its dedicated terminal. It cannot route itself into the
|
|
348
|
+
# implement/review/done flow. Filing improvements goes through
|
|
349
|
+
# propose_issue (→ Triage), which is independent of allowed_transitions.
|
|
350
|
+
# - The `spawn:` block (see the states-field docs above) declares the
|
|
351
|
+
# triggers (on_idle / after_terminal), the minted-issue title template, and
|
|
352
|
+
# max_in_flight. This is what makes the cycle recurring — no cron needed.
|
|
353
|
+
# - The Reflect state's `prompt_file` encodes the
|
|
354
|
+
# read → distil → propose loop and the GUARDRAILS below.
|
|
355
|
+
#
|
|
356
|
+
# Reflected (role: terminal):
|
|
357
|
+
# - DEDICATED terminal for a completed reflection — NOT Done. Done carries the
|
|
358
|
+
# PR autopilot `actions:` (push_branch + create_pr_if_missing); a minted
|
|
359
|
+
# reflection runs on an empty branch (it only files propose_issue), so
|
|
360
|
+
# terminating in Done would try to open a PR off nothing. So `Reflected`
|
|
361
|
+
# declares NO `pr:` and NO `actions:`. Each cycle lands here as a browsable,
|
|
362
|
+
# per-run audit trail — one issue per cycle — instead of one ever-growing
|
|
363
|
+
# immortal ticket body.
|
|
364
|
+
#
|
|
365
|
+
# GUARDRAILS (this is a self-modifying loop — keep the human in it):
|
|
366
|
+
# - Output is proposals into Triage (holding, never auto-dispatched). The
|
|
367
|
+
# operator approves/discards. Do not bypass this gate.
|
|
368
|
+
# - Constrain the proposal surface to harness config. Forbid any proposal that
|
|
369
|
+
# weakens the Review state, the test/lint gates, or the Triage gate itself.
|
|
370
|
+
# - Each proposal must cite the issue ids that motivated it, so the operator
|
|
371
|
+
# checks the lesson against the evidence rather than the reflector's summary.
|
|
372
|
+
#
|
|
373
|
+
# CADENCE: the orchestrator auto-spawns a fresh reflection issue from the
|
|
374
|
+
# Reflect state's `spawn:` block — `on_idle` (idle with >=1 issue finished since
|
|
375
|
+
# the last cycle) or `after_terminal` (a backstop once N issues have reached a
|
|
376
|
+
# terminal state since the last cycle). The counter resets on a successful mint;
|
|
377
|
+
# `max_in_flight: 1` prevents a second cycle while one is live. An operator can
|
|
378
|
+
# also mint a cycle by hand by creating a Reflect issue (a real dashboard
|
|
379
|
+
# "reflect now" button or a `symphony reflect` verb is a natural fit) — a clean
|
|
380
|
+
# replacement for the old "re-arm by mv/cron" path.
|
|
381
|
+
#
|
|
382
|
+
# Example states to add (names are yours to choose):
|
|
383
|
+
#
|
|
384
|
+
# states:
|
|
385
|
+
# # ... your active/terminal states ...
|
|
386
|
+
# Reflect:
|
|
387
|
+
# role: active
|
|
388
|
+
# adapter: claude
|
|
389
|
+
# model: claude-opus-4-8[1m] # large context for reading transcripts
|
|
390
|
+
# max_turns: 20 # higher than implement/review
|
|
391
|
+
# eval_mode: true
|
|
392
|
+
# allowed_transitions: [Reflected]
|
|
393
|
+
# spawn:
|
|
394
|
+
# on_idle: true
|
|
395
|
+
# after_terminal: 10 # backstop for busy stretches (0 disables)
|
|
396
|
+
# title: "Reflection {{ stamp }}"
|
|
397
|
+
# max_in_flight: 1 # never two reflections live at once
|
|
398
|
+
# Triage:
|
|
399
|
+
# role: holding
|
|
400
|
+
# Reflected:
|
|
401
|
+
# role: terminal # dedicated terminal — NO pr:/actions:
|
|
402
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
405
|
+
# pr — PR autopilot engine toggle: arm GitHub auto-merge when a terminal-state
|
|
406
|
+
# PR is mergeable; route non-mergeable PRs back to the implementing state.
|
|
407
|
+
#
|
|
408
|
+
# Optional. This is the SLIM host-global half only — the on/off switch and the
|
|
409
|
+
# per-PR `gh pr view` cache TTL. The merge/close/route targets and the
|
|
410
|
+
# auto-merge strategy live ON the terminal states they describe, as
|
|
411
|
+
# `states.<name>.pr` (see the `pr:` field doc in the states block above):
|
|
412
|
+
# • the MERGE state declares `pr: { auto_merge: <strategy>,
|
|
413
|
+
# on_conflict: { route_to: <active state> } }`
|
|
414
|
+
# • the CLOSE state declares `pr: { close: true }`
|
|
415
|
+
# The reconciler derives the targets by scanning states for that field — there
|
|
416
|
+
# is no named-string sibling block here.
|
|
417
|
+
#
|
|
418
|
+
# When `enabled: true` the reconciler grows a `pr` resource that, on every tick,
|
|
419
|
+
# looks up each issue in the merge state via `gh pr list --head agent/<id>`,
|
|
420
|
+
# fetches its detail with `gh pr view`, and:
|
|
421
|
+
#
|
|
422
|
+
# • Arms GitHub's auto-merge when the PR is `mergeable: MERGEABLE`
|
|
423
|
+
# (`gh pr merge --auto --<strategy> --delete-branch`, where `<strategy>` is
|
|
424
|
+
# the merge state's `pr.auto_merge`). GitHub merges as soon as required
|
|
425
|
+
# checks pass and review requirements are satisfied.
|
|
426
|
+
# • When the PR is `mergeable: CONFLICTING`, appends a structured notes block
|
|
427
|
+
# to the issue file and routes the issue from the merge state back to that
|
|
428
|
+
# state's `pr.on_conflict.route_to` (default: the first declared
|
|
429
|
+
# `role: active` state). The workspace + `agent/<id>` branch are preserved.
|
|
430
|
+
# Before the next dispatch symphony runs `git fetch origin <base>` so
|
|
431
|
+
# `origin/<base>` is current, and the Todo prompt's first step is
|
|
432
|
+
# `git rebase origin/<base>` — resolving the conflict is the agent's normal
|
|
433
|
+
# flow, not an out-of-band autopilot operation.
|
|
434
|
+
# • For an issue in the close state with an open PR, closes the PR without
|
|
435
|
+
# merge and best-effort-deletes the remote branch.
|
|
436
|
+
#
|
|
437
|
+
# Requires `gh` authenticated on the host (`gh auth status` clean). The token
|
|
438
|
+
# never enters the VM. Auto-merge ALSO requires at least one branch protection
|
|
439
|
+
# rule on the base branch, or `gh pr merge --auto` will error — set one in
|
|
440
|
+
# the repo's GitHub settings before flipping `enabled: true`.
|
|
441
|
+
#
|
|
442
|
+
# When `enabled: false` (or the block is absent) the autopilot is fully inert:
|
|
443
|
+
# the resource is never constructed and the orchestrator's existing Done-state
|
|
444
|
+
# behavior (workspace cleanup + the Done-state `actions:` block that pushes the
|
|
445
|
+
# branch and opens the PR + operator-merge) is unchanged.
|
|
446
|
+
#
|
|
447
|
+
# Workspace lifecycle gotcha: when `enabled: true`, transitions into the merge
|
|
448
|
+
# state no longer fire the standard terminal workspace cleanup. The pr resource
|
|
449
|
+
# owns the workspace from that point on and removes it once the PR has merged
|
|
450
|
+
# (or been closed). Transitions into the close state (and any other terminal
|
|
451
|
+
# state) keep the standard cleanup-on-transition behavior.
|
|
452
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
453
|
+
pr:
|
|
454
|
+
# enabled (bool): master switch. Default false.
|
|
455
|
+
enabled: false
|
|
456
|
+
|
|
457
|
+
# poll_interval_ms (int): per-PR GitHub view cache TTL, milliseconds. The
|
|
458
|
+
# reconciler may tick more often than this; a single PR view is reused
|
|
459
|
+
# within the window. Default 30000.
|
|
460
|
+
poll_interval_ms: 30000
|
|
461
|
+
# ----------------------------------------------------------------------------
|
|
462
|
+
# SLEEP CYCLE (recurring spawn) — the recurrence lives in the `spawn:` block on
|
|
463
|
+
# the active reflection state (see the states-field docs + the SLEEP CYCLE
|
|
464
|
+
# section above). There is NO top-level `sleep_cycle:` block and NO `arm:` block;
|
|
465
|
+
# instead of re-arming one immortal parked ticket, the orchestrator MINTS a fresh
|
|
466
|
+
# ephemeral reflection issue when a trigger fires:
|
|
467
|
+
#
|
|
468
|
+
# states:
|
|
469
|
+
# Reflect:
|
|
470
|
+
# role: active
|
|
471
|
+
# allowed_transitions: [Reflected]
|
|
472
|
+
# spawn:
|
|
473
|
+
# on_idle: true # spawn when idle with >=1 terminal since last cycle
|
|
474
|
+
# after_terminal: 10 # spawn after N terminal transitions (0 disables)
|
|
475
|
+
# title: "Reflection {{ stamp }}"
|
|
476
|
+
# max_in_flight: 1 # never two reflections live at once
|
|
477
|
+
# Reflected:
|
|
478
|
+
# role: terminal # dedicated terminal — NO pr:/actions:
|
|
479
|
+
#
|
|
480
|
+
# SEMANTICS:
|
|
481
|
+
# - on_idle: spawn when the orchestrator is idle (nothing running, claimed, or
|
|
482
|
+
# pending retry, and no active candidate this poll) AND >=1 issue has reached
|
|
483
|
+
# a terminal state since the last cycle. The ">=1 since last cycle" gate is
|
|
484
|
+
# load-bearing: without it an idle orchestrator re-spawns in a tight loop with
|
|
485
|
+
# nothing new to mine.
|
|
486
|
+
# - after_terminal: a backstop for busy stretches that never go idle — spawn
|
|
487
|
+
# once this many issues have reached a terminal state since the last cycle.
|
|
488
|
+
# 0 disables the count trigger.
|
|
489
|
+
# - max_in_flight: never mint a second issue while this many are already live in
|
|
490
|
+
# the spawn state (replaces the immortal ticket's built-in one-at-a-time
|
|
491
|
+
# mutex). Default 1.
|
|
492
|
+
# The terminal-transition counter resets to 0 on a successful mint (and is
|
|
493
|
+
# restored if the mint fails) and is held in orchestrator memory only (a restart
|
|
494
|
+
# resets it). A finished reflection's OWN terminal move (out of the spawn state)
|
|
495
|
+
# is excluded from the counter, so a completed cycle never counts toward spawning
|
|
496
|
+
# the next.
|
|
497
|
+
#
|
|
498
|
+
# VALIDATION (structural, from each state's own role — no dedicated re-validator):
|
|
499
|
+
# `spawn:` is only valid on an `active` state, and at most one active state may
|
|
500
|
+
# declare `spawn`. Unlike the retired `arm:` block it needs no parked issue and
|
|
501
|
+
# no from-state.
|
|
502
|
+
#
|
|
503
|
+
# GUARDRAILS: spawning ONLY mints the reflection issue. The proposals the
|
|
504
|
+
# reflector files still land in Triage and still require human approve/discard —
|
|
505
|
+
# spawning does not bypass the human gate.
|
|
506
|
+
# ----------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
509
|
+
# polling — how often to poll the tracker.
|
|
510
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
511
|
+
polling:
|
|
512
|
+
# interval_ms (int): tick interval, milliseconds.
|
|
513
|
+
# Default: 30000
|
|
514
|
+
interval_ms: 5000
|
|
515
|
+
|
|
516
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
517
|
+
# workspace — per-issue working directory.
|
|
518
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
519
|
+
workspace:
|
|
520
|
+
# root (path): parent directory holding `<issue-id>/` working trees.
|
|
521
|
+
# An explicit value resolves absolute as-is, `~`-expanded against $HOME, or
|
|
522
|
+
# (relative) against the workflow file's directory.
|
|
523
|
+
# Default (unset): ~/.symphony/workspaces/<project> (same <project> derivation
|
|
524
|
+
# as tracker.root above).
|
|
525
|
+
# root: ~/.symphony/workspaces/my-project
|
|
526
|
+
|
|
527
|
+
# github_repo (string | null): the GitHub `owner/repo` slug symphony pushes the
|
|
528
|
+
# per-issue `agent/<id>` branch to and opens a PR against on the Done-state
|
|
529
|
+
# transition. This is the config-file home for what was previously the
|
|
530
|
+
# `SYMPHONY_REPO` env var only.
|
|
531
|
+
# • unset / null / "none" → LOCAL-ONLY mode: no `origin` is restored, nothing
|
|
532
|
+
# is pushed, and the branch is left in the workspace for `git log agent/<id>`.
|
|
533
|
+
# • "owner/repo" → PR mode: the canonical workspace setup restores an
|
|
534
|
+
# `origin` at https://github.com/<owner>/<repo>.git and the Done-state
|
|
535
|
+
# actions push + `gh pr create`. Requires `gh auth status` clean on the host
|
|
536
|
+
# (the token never enters the VM).
|
|
537
|
+
# GitHub-only by construction (the origin URL and `gh` are hardwired). There is
|
|
538
|
+
# NO auto-detection — set the literal slug or leave it local-only. A malformed
|
|
539
|
+
# value (a URL, a bare name, extra slashes/whitespace) is REJECTED at parse time
|
|
540
|
+
# rather than silently falling back to local-only.
|
|
541
|
+
#
|
|
542
|
+
# Precedence: the `SYMPHONY_REPO` env var, when set non-empty, OVERRIDES this
|
|
543
|
+
# field (so existing exports keep working); otherwise this value is used.
|
|
544
|
+
# Default: null.
|
|
545
|
+
# github_repo: owner/repo
|
|
546
|
+
|
|
547
|
+
# base_branch (string): the branch the per-issue workspace clones from and
|
|
548
|
+
# (in PR mode) targets as the PR base. The source repo's local
|
|
549
|
+
# `<base_branch>` is the canonical base ref — symphony does NOT implicitly
|
|
550
|
+
# fetch `origin/<base_branch>` at setup time; update the source repo before
|
|
551
|
+
# the next dispatch to move the base.
|
|
552
|
+
# Precedence: the `SYMPHONY_BASE_BRANCH` env var, when set non-empty,
|
|
553
|
+
# OVERRIDES this field; otherwise this value is used.
|
|
554
|
+
# Default: main.
|
|
555
|
+
base_branch: main
|
|
556
|
+
|
|
557
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
558
|
+
# logs — per-issue JSONL run logs (everything to/from the VM, plus typed-action
|
|
559
|
+
# output) AND the orchestrator-side text log mirrored to disk for offline debugging.
|
|
560
|
+
#
|
|
561
|
+
# Per-issue: one file per issue at `<root>/<sanitized-identifier>.jsonl`,
|
|
562
|
+
# appended across attempts AND across symphony process restarts. Each line is
|
|
563
|
+
# a self-describing JSON object with `ts`, `issue_id`, `attempt`, and a
|
|
564
|
+
# `channel` discriminator:
|
|
565
|
+
#
|
|
566
|
+
# channel: "acp" — JSON-RPC frame between host and the in-VM adapter.
|
|
567
|
+
# `direction` ("host_to_vm" | "vm_to_host") and `frame`
|
|
568
|
+
# (parsed JSON) — or `kind: "unparseable"` + `raw`.
|
|
569
|
+
# channel: "stderr" — raw byte chunk from the adapter / VM stderr.
|
|
570
|
+
# channel: "hook" — stdout/stderr chunk from a terminal-state `actions:`
|
|
571
|
+
# run, plus a final `kind: "result"` line (exit_code,
|
|
572
|
+
# signal, timed_out). The `hook` field names the action
|
|
573
|
+
# group (e.g. `actions`). (The channel name is a holdover
|
|
574
|
+
# from the retired workflow-hook surface.)
|
|
575
|
+
# channel: "system" — orchestrator lifecycle events (attempt_started — which
|
|
576
|
+
# also carries the per-state `max_turns` budget,
|
|
577
|
+
# attempt_ended, transition, reconciliation_terminating,
|
|
578
|
+
# etc.). The `transition` event records each state move
|
|
579
|
+
# (from_state, to_state, notes, actor, terminal,
|
|
580
|
+
# rerouted) so the trajectory is reconstructable.
|
|
581
|
+
#
|
|
582
|
+
# Per-issue run summary (for the sleep-cycle reflector): alongside each
|
|
583
|
+
# `<root>/<key>.jsonl`, the orchestrator writes a compact, versioned
|
|
584
|
+
# `<root>/<key>.summary.json` at the issue's terminal unwind. It is a pure
|
|
585
|
+
# REDUCTION over the lifecycle (`system`) events already in the JSONL — no extra
|
|
586
|
+
# hot-path logging — so a reflection turn can read dozens of summaries without
|
|
587
|
+
# re-parsing multi-MB frame logs. Fields (schema_version 1):
|
|
588
|
+
# • state_path — distinct states visited, terminal appended
|
|
589
|
+
# (e.g. ["Todo","Review","Todo","Review","Done"]);
|
|
590
|
+
# • attempts — total dispatched attempts;
|
|
591
|
+
# • per_state[] — {state, attempts, turns_used, max_turns,
|
|
592
|
+
# budget_exhausted, wall_clock_ms};
|
|
593
|
+
# • review_rejections + rejection_notes[] — count and each reviewer kick-back's
|
|
594
|
+
# notes (a non-reroute transition back to the INITIAL
|
|
595
|
+
# implementing state, i.e. a Review→Todo rework);
|
|
596
|
+
# • turn_budget_exhausted, timeouts[] (stall / prompt_timeout / transport);
|
|
597
|
+
# • conflict_routes[] — PR-autopilot / action reroutes (rebase churn);
|
|
598
|
+
# • terminal_state + terminal_outcome (completed | cancelled | incomplete);
|
|
599
|
+
# • pr_number / pr_url (best-effort, scraped from the Done-state actions
|
|
600
|
+
# stdout; null when unavailable);
|
|
601
|
+
# • first/last_event_at, wall_clock_ms_total, generated_at.
|
|
602
|
+
# Graceful absence / backfill: the summary is best-effort. Issues that closed
|
|
603
|
+
# BEFORE this feature shipped have no `*.summary.json`; a write failure or a
|
|
604
|
+
# mid-issue process restart (the in-memory accumulator only sees post-restart
|
|
605
|
+
# attempts) can leave it missing or partial. The reflector MUST treat an absent
|
|
606
|
+
# or partial summary as "no signal for this issue" and fall back to the raw
|
|
607
|
+
# JSONL (or skip the issue) — never as an error. No backfill job is run; old
|
|
608
|
+
# issues simply carry no summary.
|
|
609
|
+
#
|
|
610
|
+
# Orchestrator-side: a single `<root>/symphony.log` (created on demand) gets
|
|
611
|
+
# every structured log line symphony emits — workflow loads, dispatch
|
|
612
|
+
# decisions, action results, reconciler ticks, shutdown — in `key=value` text
|
|
613
|
+
# format. Lets an agent reviewing a finished run (typically with
|
|
614
|
+
# `.symphony/logs/` mounted into a VM) replay orchestrator-side events
|
|
615
|
+
# alongside the per-issue JSONL traces in the same directory. Set the
|
|
616
|
+
# `SYMPHONY_LOG_FILE` env var to override the path; set it to the empty
|
|
617
|
+
# string to disable the file sink entirely (stderr remains).
|
|
618
|
+
#
|
|
619
|
+
# Console routing: while the file sink is active (the default), the structured
|
|
620
|
+
# stream goes to the file ONLY — the console shows just the startup banner
|
|
621
|
+
# (workflow, tracker root, dashboard URL, MCP endpoint, ACP `/acp` WebSocket
|
|
622
|
+
# endpoint, log-file path). The dashboard/MCP/ACP all share the one HTTP port,
|
|
623
|
+
# which defaults to ephemeral, so the banner is how the operator learns which
|
|
624
|
+
# port got bound. `tail -f` the
|
|
625
|
+
# log file to follow the detail. Pass `--verbose` (alias `--foreground`) to mirror
|
|
626
|
+
# the structured stream back onto the console for interactive debugging. With
|
|
627
|
+
# no file sink configured, the structured stream stays on stderr.
|
|
628
|
+
#
|
|
629
|
+
# Intended for later evaluation — typically by another agent running inside a VM
|
|
630
|
+
# — so the schema is verbose on purpose. Writes are best-effort: a failure to
|
|
631
|
+
# write a log line never crashes the orchestrator.
|
|
632
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
633
|
+
logs:
|
|
634
|
+
# root (path): directory holding per-issue JSONL files and symphony.log.
|
|
635
|
+
# An explicit value resolves absolute as-is, `~`-expanded against $HOME, or
|
|
636
|
+
# (relative) against the workflow file's directory.
|
|
637
|
+
# Default (unset): ~/.symphony/logs/<project> (same <project> derivation as
|
|
638
|
+
# tracker.root above) — durable run logs live OUTSIDE the repo tree by default.
|
|
639
|
+
# root: ~/.symphony/logs/my-project
|
|
640
|
+
|
|
641
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
642
|
+
# workspace lifecycle — no shell `hooks:` surface.
|
|
643
|
+
#
|
|
644
|
+
# Symphony has no `hooks:` block (workflow-level or per-state). The behaviors
|
|
645
|
+
# the old `after_create` / `before_run` / `after_run` / `before_remove` hooks
|
|
646
|
+
# covered now live in typed, testable homes:
|
|
647
|
+
#
|
|
648
|
+
# • First-creation workspace setup (clone + base checkout + `agent/<id>`
|
|
649
|
+
# branch cut + origin/identity) is owned by the orchestrator's TypeScript
|
|
650
|
+
# `setupWorkspaceDir`. The workspace arrives at the agent with: a hardlinked
|
|
651
|
+
# `git clone --local --no-tags` of the source repo (`SYMPHONY_SOURCE_REPO`,
|
|
652
|
+
# default: the dir containing WORKFLOW.yaml) on the base branch
|
|
653
|
+
# (`SYMPHONY_BASE_BRANCH`, default `main`); all network remotes stripped; an
|
|
654
|
+
# `origin` restored to the canonical HTTPS URL when `workspace.github_repo`
|
|
655
|
+
# (or the `SYMPHONY_REPO` env override) is set (so the Done-state push can
|
|
656
|
+
# reach a remote — `gh auth setup-git` runs host-side; the token never
|
|
657
|
+
# enters the VM); pinned `--local` git identity;
|
|
658
|
+
# and `agent/<id>` checked out off the base SHA.
|
|
659
|
+
# • Per-VM tooling (extra CLIs, language runtimes, dependency bootstrap) is
|
|
660
|
+
# baked into the agent image — see `gondolin.image` and `images/agents/`.
|
|
661
|
+
# • Arbitrary in-sandbox commands run from a state's `actions:` via a
|
|
662
|
+
# `run_in_vm` action (executes inside the per-issue VM, not on the host).
|
|
663
|
+
# • The post-attempt handoff (push branch, open PR) is the Done state's
|
|
664
|
+
# `actions:` block — see `states.Done.actions` above for the canonical pair
|
|
665
|
+
# (push_branch + create_pr_if_missing). The action executor exposes
|
|
666
|
+
# `$branch`, `$base_branch`, `$pr_title`, `$pr_body_file`, `$repo`.
|
|
667
|
+
#
|
|
668
|
+
# Workspace removal is a plain best-effort `rm -rf` of the per-issue dir once
|
|
669
|
+
# the run unwinds; there is no pre-removal artifact-rescue hook (rescue what you
|
|
670
|
+
# need via a terminal-state action before transitioning).
|
|
671
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
674
|
+
# agent — concurrency and turn budget.
|
|
675
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
676
|
+
agent:
|
|
677
|
+
# max_concurrent_agents (int): GLOBAL host ceiling on simultaneously-running
|
|
678
|
+
# agents across the whole workflow. This is the cross-state RAM bound that
|
|
679
|
+
# memory admission clamps (see memory_admission_enabled below); the sum of
|
|
680
|
+
# every state's `max_concurrent` is validated against it at startup. It stays
|
|
681
|
+
# top-level — not on a state — because it bounds total host memory across all
|
|
682
|
+
# VMs at once. Default: 10
|
|
683
|
+
max_concurrent_agents: 2
|
|
684
|
+
|
|
685
|
+
# max_turns (int): hard ceiling on autonomous turns per issue. Steering-reply
|
|
686
|
+
# turns are free; only autonomous turns count. Default: 20
|
|
687
|
+
max_turns: 6
|
|
688
|
+
|
|
689
|
+
# max_retry_backoff_ms (int): exponential backoff cap for retried dispatches
|
|
690
|
+
# after recoverable failures. Default: 300000
|
|
691
|
+
max_retry_backoff_ms: 120000
|
|
692
|
+
|
|
693
|
+
# memory_admission_enabled (bool): when true, before each dispatch the
|
|
694
|
+
# orchestrator reads `/proc/meminfo` (MemAvailable) and clamps the effective
|
|
695
|
+
# concurrency cap to what currently fits at `gondolin.mem_mib` per VM after
|
|
696
|
+
# subtracting `host_memory_reserve_mib`. This is a defense-in-depth backstop
|
|
697
|
+
# for hosts where the static `max_concurrent_agents` is set generously: when
|
|
698
|
+
# MemAvailable drops, new dispatches are gated so a misconfigured cap can't
|
|
699
|
+
# walk the host into OOM (issue 27). On hosts without `/proc/meminfo`
|
|
700
|
+
# (macOS, BSD) the probe degrades gracefully and the static cap is used
|
|
701
|
+
# unchanged. Default: true.
|
|
702
|
+
memory_admission_enabled: true
|
|
703
|
+
|
|
704
|
+
# host_memory_reserve_mib (int): headroom (MiB) the memory admission cap
|
|
705
|
+
# keeps for the orchestrator process itself, the per-VM Gondolin runners, and
|
|
706
|
+
# the kernel's own working set. Only consulted when
|
|
707
|
+
# `memory_admission_enabled` is true. Raise on hosts with heavy non-symphony
|
|
708
|
+
# workloads; lower on dedicated worker hosts. Default: 2048.
|
|
709
|
+
host_memory_reserve_mib: 2048
|
|
710
|
+
|
|
711
|
+
# circuit_breaker_threshold (int): after this many CONSECUTIVE dispatch
|
|
712
|
+
# attempts fail with the *same* (normalized) reason, the orchestrator stops
|
|
713
|
+
# retrying the issue and routes it to a holding state (the first declared
|
|
714
|
+
# `role: holding` state) for a human to inspect, instead of looping forever
|
|
715
|
+
# on a deterministically-failing dispatch (issue 128 — a persistent
|
|
716
|
+
# `401 invalid_api_key` once looped ~324 attempts over ~13h). The streak
|
|
717
|
+
# resets the moment an attempt fails with a different reason or exits
|
|
718
|
+
# cleanly, so transient/varied failures still retry under the normal backoff
|
|
719
|
+
# (`max_retry_backoff_ms`). The tripped issue's body gets a diagnostic note
|
|
720
|
+
# explaining the trip so the dashboard shows "stuck on identical failure"
|
|
721
|
+
# rather than a silent loop. Set to 0 to disable; must otherwise be >= 2
|
|
722
|
+
# (1 would trip on the first failure, never retrying). Default: 5.
|
|
723
|
+
circuit_breaker_threshold: 5
|
|
724
|
+
|
|
725
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
726
|
+
# acp — Agent Client Protocol adapter selection.
|
|
727
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
728
|
+
acp:
|
|
729
|
+
# adapter (string): one of symphony's known profiles. Default: 'claude'.
|
|
730
|
+
# claude — claude-agent-acp. The guest holds a placeholder bearer; the host
|
|
731
|
+
# substitutes the real Anthropic OAuth token at Gondolin egress.
|
|
732
|
+
# No real credential enters the VM.
|
|
733
|
+
# codex — codex-acp (issue 116). Same model: the guest holds a placeholder
|
|
734
|
+
# bearer (in a fake ~/.codex/auth.json); the host substitutes the
|
|
735
|
+
# real OpenAI/ChatGPT token at egress. No real credential — and no
|
|
736
|
+
# real OPENAI_API_KEY — enters the VM.
|
|
737
|
+
adapter: claude
|
|
738
|
+
|
|
739
|
+
# Credentials never enter the VM (issue 113; codex generalized in 116). The
|
|
740
|
+
# guest holds only a token-shaped placeholder; on every outbound request the
|
|
741
|
+
# host substitutes the real upstream credential at Gondolin egress (TLS-MITM,
|
|
742
|
+
# see src/agent/credential-secrets.ts). The real refresh/durable token always
|
|
743
|
+
# stays host-side.
|
|
744
|
+
#
|
|
745
|
+
# For claude: the host reads the live access token from
|
|
746
|
+
# ~/.claude/.credentials.json (refreshing host-side via `claude -p "ok"` under
|
|
747
|
+
# flock when the cache is stale) and injects it at egress to api.anthropic.com.
|
|
748
|
+
# A minimal ~/.claude.json is staged for identity only — NO refreshToken, NO
|
|
749
|
+
# accessToken on the VM.
|
|
750
|
+
#
|
|
751
|
+
# For codex: the host reads the live credential (`tokens.access_token` from
|
|
752
|
+
# ~/.codex/auth.json, with an OPENAI_API_KEY env fallback — NEVER the refresh
|
|
753
|
+
# token) and injects it at egress to chatgpt.com / api.openai.com. A COMPLETE
|
|
754
|
+
# fake ~/.codex/auth.json is staged (JWT-shaped placeholder tokens + the
|
|
755
|
+
# non-secret account_id/auth_mode/last_refresh codex's completeness check needs),
|
|
756
|
+
# so codex-acp runs in its native mode without an in-VM OAuth handshake or
|
|
757
|
+
# refresh (both stay host-side). Every credential-bearing var is stripped from
|
|
758
|
+
# the forwarded VM boot env.
|
|
759
|
+
|
|
760
|
+
# model (string | null): optional model selector forwarded to the chosen adapter.
|
|
761
|
+
# Each adapter profile knows how to surface it natively:
|
|
762
|
+
# claude — exported as ANTHROPIC_MODEL on the adapter process. Accepts anything
|
|
763
|
+
# claude-agent-acp would (aliases like "opus", "sonnet", or full IDs
|
|
764
|
+
# like "claude-opus-4-7").
|
|
765
|
+
# codex — passed as `-c model="<value>"` argv to codex-acp (parsed as TOML).
|
|
766
|
+
# Leave unset / null to use the adapter's own default model. Default: null.
|
|
767
|
+
# model: claude-opus-4-7
|
|
768
|
+
|
|
769
|
+
# effort (string | null): optional reasoning-effort lever forwarded to the chosen
|
|
770
|
+
# adapter. Profile-specific surface:
|
|
771
|
+
# claude — written into a staged `settings.json` ({"effortLevel": "<value>"})
|
|
772
|
+
# copied to /root/.claude/settings.json in the VM before claude-agent-acp
|
|
773
|
+
# starts. Valid values are `low|medium|high|xhigh|max`, gated per-model
|
|
774
|
+
# by claude-agent-acp's `supportedEffortLevels` (Opus supports `xhigh` and
|
|
775
|
+
# `max`; Haiku does not). Symphony does not validate the value — the
|
|
776
|
+
# adapter rejects unsupported choices at startup, which keeps symphony
|
|
777
|
+
# from drifting from the adapter's own supported list.
|
|
778
|
+
# codex — not wired (codex-acp has no first-class effort knob on the wrapper);
|
|
779
|
+
# setting `acp.effort` for a codex-backed state is a no-op.
|
|
780
|
+
# Leave unset / null for the adapter's own default. Default: null.
|
|
781
|
+
# effort: xhigh
|
|
782
|
+
|
|
783
|
+
# NOTE: the launch shape is fixed (an in-VM agent dials back the `/acp` WebSocket
|
|
784
|
+
# and spawns the chosen adapter). Customizing what the agent spawns requires
|
|
785
|
+
# forking that agent and rebuilding the VM image with the fork in place.
|
|
786
|
+
|
|
787
|
+
# shell (string): shell used to run the ACP launch command. Default: 'bash'.
|
|
788
|
+
shell: bash
|
|
789
|
+
|
|
790
|
+
# prompt_timeout_ms (int): max wall time for a single ACP `session/prompt`
|
|
791
|
+
# call (one symphony turn). Default: 3600000 (1 hour).
|
|
792
|
+
prompt_timeout_ms: 1800000
|
|
793
|
+
|
|
794
|
+
# read_timeout_ms (int): max time between bytes on the ACP stdio. Bumped from
|
|
795
|
+
# a small default because VM cold-boot + adapter startup can take ~10s on
|
|
796
|
+
# first use. Default: 30000
|
|
797
|
+
read_timeout_ms: 30000
|
|
798
|
+
|
|
799
|
+
# stall_timeout_ms (int): max time the adapter can be idle (no events) before
|
|
800
|
+
# the turn is killed and retried. Default: 300000
|
|
801
|
+
stall_timeout_ms: 300000
|
|
802
|
+
|
|
803
|
+
# ACP transport — the in-VM agent dials back a `/acp` WebSocket on symphony's unified
|
|
804
|
+
# HTTP server (the SAME listener that serves the dashboard + MCP), reached through the
|
|
805
|
+
# SAME `tcp.hosts` tunnel MCP uses (the guest dials `ws://symphony-mcp:7001/acp`). It
|
|
806
|
+
# authenticates with a per-dispatch bearer sent as the first WebSocket message, then ACP
|
|
807
|
+
# JSON-RPC frames flow as WebSocket frames; the in-VM agent (`/opt/symphony/vm-agent.mjs`)
|
|
808
|
+
# spawns the adapter via `child_process.spawn` with kernel pipes and pumps the frames to
|
|
809
|
+
# the adapter's stdio. There is NO separate ACP listener / bind port — the raw-TCP bridge
|
|
810
|
+
# (`acp.bridge.*`: bind_host/bind_port/reach_host/reach_url) was retired; those keys are
|
|
811
|
+
# ignored if present.
|
|
812
|
+
|
|
813
|
+
# connect_timeout_ms (int): how long to wait for the in-VM agent to dial back the `/acp`
|
|
814
|
+
# WebSocket after the sandbox is launched, before failing the attempt. Default: 30000
|
|
815
|
+
connect_timeout_ms: 30000
|
|
816
|
+
|
|
817
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
818
|
+
# credentials — host credential lifecycle (issue 113). The host substitutes the
|
|
819
|
+
# real OAuth access token into each VM's outbound request at Gondolin egress; the
|
|
820
|
+
# ticker keeps the host's cached access token warm by periodically running
|
|
821
|
+
# `claude -p "ok"` — Claude Code's own OAuth path detects the stale token,
|
|
822
|
+
# refreshes against Anthropic, and atomically writes the rotated tuple back to
|
|
823
|
+
# `~/.claude/.credentials.json`. Symphony never implements OAuth; Anthropic's own
|
|
824
|
+
# client does.
|
|
825
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
826
|
+
credentials:
|
|
827
|
+
# ticker_interval_ms (int): how often the host ticker spawns `claude -p "ok"`
|
|
828
|
+
# to refresh the OAuth cache. Each live VM also refreshes proactively before its
|
|
829
|
+
# cached token expires, so the ticker is belt-to-the-braces for idle periods.
|
|
830
|
+
# Set to 0 to disable the in-symphony ticker entirely (operator runs their own
|
|
831
|
+
# systemd timer instead). Default: 21600000 (6 hours).
|
|
832
|
+
ticker_interval_ms: 21600000
|
|
833
|
+
|
|
834
|
+
# refresh_margin_ms (int): how far ahead of a token's expiry the host refreshes it.
|
|
835
|
+
# Drives BOTH the per-VM proactive tick (scheduled at `expiresAt - margin`) AND the
|
|
836
|
+
# egress request-side lazy refresh (a request bound for a credential host within this
|
|
837
|
+
# margin of expiry awaits a fresh token before going out). Must comfortably cover the
|
|
838
|
+
# refresh round-trip (`claude -p ok` rotate + re-read + fan-out) plus any in-flight long
|
|
839
|
+
# request, or a token expiring mid-dispatch loses the refresh/request race and 401s the
|
|
840
|
+
# egress (issue 214). Default: 300000 (5 minutes).
|
|
841
|
+
refresh_margin_ms: 300000
|
|
842
|
+
|
|
843
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
844
|
+
# gondolin — microVM execution environment (Gondolin substrate).
|
|
845
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
846
|
+
gondolin:
|
|
847
|
+
# image: the agent rootfs source. THREE forms, in precedence order
|
|
848
|
+
# (image > oci_image > managed):
|
|
849
|
+
#
|
|
850
|
+
# 1. `managed` (the DEFAULT when neither `image` nor `oci_image` is set, and what a
|
|
851
|
+
# fresh scaffold emits): FETCH a prebuilt Gondolin asset published to symphony's
|
|
852
|
+
# OWN GitHub releases for the running symphony version + host arch (issue 224).
|
|
853
|
+
# No docker, no `npm run build:image`, no local convert — onboarding is
|
|
854
|
+
# `scaffold → run`. On the first poll the orchestrator downloads + verifies (against
|
|
855
|
+
# the published `.sha256`) + imports the asset ONCE (the dashboard shows the
|
|
856
|
+
# one-time fetch) and HOLDS VM dispatch until it's ready, then boots the cached
|
|
857
|
+
# asset. Published for x86_64 and arm64/aarch64 (the `release.yml` agent-image
|
|
858
|
+
# matrix); any OTHER arch makes the managed source inert and the doctor FAILs
|
|
859
|
+
# with an unsupported-arch message.
|
|
860
|
+
# Override `SYMPHONY_RELEASE_REPO` to fetch from a fork/staging release.
|
|
861
|
+
#
|
|
862
|
+
# 2. a PRE-CONVERTED asset selector — the low-level escape hatch. Build it ONCE with
|
|
863
|
+
# `npm run build:image` (see images/agents/); the build prints a content-addressed
|
|
864
|
+
# build id (a digest) — pin that here for an immutable reference. A `name:tag` ref
|
|
865
|
+
# (e.g. `symphony-agents:latest`) or a path to an exported asset directory also
|
|
866
|
+
# work. WINS over `oci_image` and the managed source when set.
|
|
867
|
+
#
|
|
868
|
+
# 3. `oci_image` (below): a normal OCI ref symphony auto-converts on first reconcile.
|
|
869
|
+
#
|
|
870
|
+
# The image ships only `mise`; node + the ACP CLIs (claude-agent-acp, codex-acp)
|
|
871
|
+
# are mise-installed at dispatch and the in-VM launcher is staged at
|
|
872
|
+
# /opt/symphony/vm-agent.mjs — so dispatch needs no runtime mounts. Default: managed.
|
|
873
|
+
image: managed
|
|
874
|
+
|
|
875
|
+
# oci_image (string | null): a NORMAL OCI image reference (a docker/podman repo
|
|
876
|
+
# string, optionally `:tag` or `@sha256:…`) that symphony auto-converts + caches into
|
|
877
|
+
# a Gondolin asset as part of the reconcile loop (issue 206) — no manual
|
|
878
|
+
# `npm run build:image` step. On the first poll where the asset isn't cached, the
|
|
879
|
+
# orchestrator converts it ONCE (the dashboard shows a "building microVM image
|
|
880
|
+
# (one-time setup)" banner) and HOLDS VM dispatch until it's ready; the cache key is
|
|
881
|
+
# the resolved OCI digest, re-checked cheaply each reconcile tick (a read-only LOCAL
|
|
882
|
+
# resolve, no pull), so a moving tag whose locally-resolved digest changes (e.g. after
|
|
883
|
+
# a re-pull) reconverts before dispatch resumes — readiness tracks the live digest, not
|
|
884
|
+
# a ref string that converted once. `oci_pull_policy` governs the one-time conversion
|
|
885
|
+
# pull, not this readiness check. Once converted, dispatch boots the cached asset
|
|
886
|
+
# automatically — set ONLY
|
|
887
|
+
# this key and go; you do NOT also need to set `image`. The MVP targets the "golden"
|
|
888
|
+
# path — a published ref that already bakes the guest contract (Node ≥ 21 + the ACP
|
|
889
|
+
# CLIs on PATH; the launcher is staged at dispatch). `image` stays the escape hatch
|
|
890
|
+
# for an asset you converted yourself, and WINS when both are set: when `image` is
|
|
891
|
+
# set the reconcile gate boots IT and the `oci_image` auto-convert path is bypassed
|
|
892
|
+
# entirely — dispatch is never held and no one-time build runs. Default: null.
|
|
893
|
+
# oci_image: ghcr.io/your-org/symphony-agents:1.0.0
|
|
894
|
+
|
|
895
|
+
# oci_pull_policy (if-not-present | always | never): how the OCI image is pulled
|
|
896
|
+
# while resolving its digest for the one-time conversion. `if-not-present` pulls
|
|
897
|
+
# only when the ref isn't already in the local container store; `always` re-pulls
|
|
898
|
+
# even when it IS present locally, so a moving tag (`repo:latest`) picks up its
|
|
899
|
+
# fresh registry digest (and reconverts when that digest changed); `never` never
|
|
900
|
+
# pulls and requires the image to already be in the local store. The cache key and
|
|
901
|
+
# the converted asset are always keyed on the POST-pull digest, so `always` can't
|
|
902
|
+
# cache against a stale local one. Only consulted when `oci_image` is set (and not
|
|
903
|
+
# by the per-tick readiness probe, which is always pull-free). Default: if-not-present.
|
|
904
|
+
# oci_pull_policy: if-not-present
|
|
905
|
+
|
|
906
|
+
# oci_runtime (docker | podman | null): container runtime used for the conversion
|
|
907
|
+
# pull/export. null = auto-detect (prefers docker, falls back to podman). Only
|
|
908
|
+
# consulted when `oci_image` is set. Default: null (auto-detect).
|
|
909
|
+
# oci_runtime: docker
|
|
910
|
+
|
|
911
|
+
# cpus (int): vCPU count per VM. Default: 2.
|
|
912
|
+
cpus: 2
|
|
913
|
+
|
|
914
|
+
# mem_mib (int): RAM per VM in MiB. Default: 2048.
|
|
915
|
+
mem_mib: 4096
|
|
916
|
+
|
|
917
|
+
# rootfs_size (string): guest rootfs virtual-disk size (qemu size syntax, e.g. "3G"),
|
|
918
|
+
# passed to the VM substrate as the MINIMUM rootfs size. The agent toolchain installs
|
|
919
|
+
# onto the guest ROOTFS each dispatch (chmod works there; the sandboxfs bind mount
|
|
920
|
+
# silently drops it — issue 222), so the rootfs must be big enough for the ephemeral
|
|
921
|
+
# install: node alone is ~123 MB plus the agent CLIs, and the default ~593 MB rootfs
|
|
922
|
+
# leaves too little headroom. Default: "3G".
|
|
923
|
+
# rootfs_size: 3G
|
|
924
|
+
|
|
925
|
+
# volumes (list): additional host:guest VFS mounts beyond the auto-mounted
|
|
926
|
+
# workspace. Gondolin's VFS is programmable (no hard per-VM mount cap), but keep
|
|
927
|
+
# this lean — if ANY state sets `eval_mode: true` it adds two read-only mounts
|
|
928
|
+
# (/symphony/issues + /symphony/logs) on top of the workspace. Prefer baking
|
|
929
|
+
# static tooling into the image over a runtime mount. Each entry:
|
|
930
|
+
# { host: path, guest: path, readonly?: bool }. Default: [].
|
|
931
|
+
volumes:
|
|
932
|
+
- host: ~/.cache/npm
|
|
933
|
+
guest: /root/.npm
|
|
934
|
+
readonly: false
|
|
935
|
+
|
|
936
|
+
# forward_env (string[]): host env vars forwarded into the VM exec.
|
|
937
|
+
# Default: [OPENAI_API_KEY, ANTHROPIC_API_KEY]
|
|
938
|
+
# NOTE: the runner strips EVERY credential-bearing var from the forwarded boot
|
|
939
|
+
# env before launch — the guest holds only a token-shaped placeholder that
|
|
940
|
+
# Gondolin substitutes with the real token at egress — so listing a credential
|
|
941
|
+
# var here does NOT plant the real key in the VM's PID-1 env.
|
|
942
|
+
forward_env:
|
|
943
|
+
- OPENAI_API_KEY
|
|
944
|
+
- ANTHROPIC_API_KEY
|
|
945
|
+
|
|
946
|
+
# mise (jdx/mise) toolchain provisioning. This is ALWAYS ON and is the ONLY toolchain
|
|
947
|
+
# path (issue 233) — there is no enable/disable toggle. The agent image ships ONLY the
|
|
948
|
+
# `mise` binary (+ glibc essentials); node + the agent CLIs (claude-code, codex,
|
|
949
|
+
# claude-agent-acp, codex-acp) come from a mise SYSTEM config symphony
|
|
950
|
+
# STAGES into the guest at /etc/mise/config.toml at dispatch, and a consuming repo's
|
|
951
|
+
# own `mise.toml` / `.tool-versions` (higher precedence) ADDS project toolchains
|
|
952
|
+
# (rust, go, kotlin+gradle, a different node, …) on top. `mise install` runs once
|
|
953
|
+
# per dispatch, installing the toolchain onto the guest ROOTFS at /opt/symphony/mise-data
|
|
954
|
+
# (EPHEMERAL per VM — see below); the agent runtime node is resolved system-scoped via
|
|
955
|
+
# `mise which node` (so a project mise.toml can't downgrade the agent below the Node ≥ 21
|
|
956
|
+
# the launcher needs). Bumping an agent CLI is a one-line edit to
|
|
957
|
+
# assets/symphony-mise.system.toml + a restart — NO image rebuild + digest repin.
|
|
958
|
+
#
|
|
959
|
+
# WHY the rootfs, not a bind mount (issue 222): the gondolin sandboxfs bind mount has no
|
|
960
|
+
# chmod opcode (chmod is a silent no-op there), so mise's create-then-chmod extractor +
|
|
961
|
+
# npm postinstall would produce NON-executable binaries on it; the guest rootfs (ext4)
|
|
962
|
+
# honors chmod, so installs land there executable. Grow it via `gondolin.rootfs_size`
|
|
963
|
+
# (default 3G) so the per-VM install fits. Only the pure downloads (node tarballs + the
|
|
964
|
+
# npm package cache) persist across dispatches, on the bind-mounted `data_dir` below.
|
|
965
|
+
#
|
|
966
|
+
# The only tunable here is `data_dir` (the download cache location).
|
|
967
|
+
mise:
|
|
968
|
+
# data_dir (string): host directory bind-mounted RW → /opt/symphony/mise-cache as the
|
|
969
|
+
# persistent mise DOWNLOAD CACHE (node tarballs + the npm package cache) — NOT the
|
|
970
|
+
# install dir (issue 222): installs need chmod, which the sandboxfs bind mount silently
|
|
971
|
+
# drops, so the toolchain installs onto the ephemeral guest rootfs and only the pure
|
|
972
|
+
# downloads live here (so a cold install isn't re-downloaded every dispatch). SHARED
|
|
973
|
+
# across every VM; host-isolated from your own ~/.local/share/mise. Absolute as-is,
|
|
974
|
+
# `~`/`$VAR`-expanded, relative → resolved against the workflow-file dir.
|
|
975
|
+
# Default: ~/.symphony/mise.
|
|
976
|
+
# data_dir: ~/.symphony/mise
|
|
977
|
+
|
|
978
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
979
|
+
# egress — general dev-tooling firewall for the in-VM agent.
|
|
980
|
+
#
|
|
981
|
+
# Gondolin denies guest egress to non-allowlisted hosts by default. The agent can
|
|
982
|
+
# always reach its own inference host (that is handled by the credential layer,
|
|
983
|
+
# which substitutes the real upstream token at egress). This block additionally
|
|
984
|
+
# opens the dev-tooling hosts the agent needs so gates can run inside the VM —
|
|
985
|
+
# `npm install`, git-based dependencies, release-binary downloads.
|
|
986
|
+
#
|
|
987
|
+
# SECURITY: this is the firewall ONLY. No credential is ever substituted for a host
|
|
988
|
+
# listed here — the real token substitutes solely on each adapter's inference host
|
|
989
|
+
# (see src/agent/credential-secrets.ts `substitutionHosts`). The effective
|
|
990
|
+
# per-adapter allowlist handed to Gondolin is THIS list UNION that adapter's
|
|
991
|
+
# substitution host(s). Listing a host therefore grants plain network egress, never
|
|
992
|
+
# a token. Keep the list tight — every entry widens the network surface of
|
|
993
|
+
# semi-trusted in-VM code.
|
|
994
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
995
|
+
egress:
|
|
996
|
+
# allowed_hosts (string[]): hostnames the in-VM agent may reach for dev tooling.
|
|
997
|
+
# Default: the mise provisioning hosts [nodejs.org, registry.npmjs.org, mise.jdx.dev,
|
|
998
|
+
# mise-versions.jdx.dev] — ALWAYS, since mise provisioning is unconditional (issue 233)
|
|
999
|
+
# — so `mise install` of the staged SYSTEM config isn't firewall-blocked. Bare
|
|
1000
|
+
# hostnames ONLY — no scheme, port, or path (`github.com`, not
|
|
1001
|
+
# `https://github.com/...`). A malformed entry fails safe (the host simply stays
|
|
1002
|
+
# blocked, never opened). Each entry is matched against the request host exactly.
|
|
1003
|
+
#
|
|
1004
|
+
# An explicit list REPLACES the default — so if you set `allowed_hosts`, re-list all
|
|
1005
|
+
# four mise hosts here alongside your own (omitting mise-versions.jdx.dev re-hits the
|
|
1006
|
+
# cold-cache node@<N> resolution failure issue 223 documents). PROJECT toolchains pull
|
|
1007
|
+
# from their own hosts; add those too (rust → static.rust-lang.org, gradle →
|
|
1008
|
+
# services.gradle.org, JDK → api.adoptium.net, …).
|
|
1009
|
+
allowed_hosts:
|
|
1010
|
+
- nodejs.org # mise node prebuilts
|
|
1011
|
+
- registry.npmjs.org # npm install + mise npm: backend (agent CLIs)
|
|
1012
|
+
- mise.jdx.dev # mise registry redirect
|
|
1013
|
+
- mise-versions.jdx.dev # mise precompiled tool version lists (node@N, etc.)
|
|
1014
|
+
- github.com # git-based deps / release pages
|
|
1015
|
+
- codeload.github.com # GitHub tarball fetch
|
|
1016
|
+
- objects.githubusercontent.com # release-binary downloads
|
|
1017
|
+
|
|
1018
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1019
|
+
# server — HTTP dashboard + MCP endpoint listener.
|
|
1020
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1021
|
+
server:
|
|
1022
|
+
# port (int | null): HTTP listener port. `--port <n>` on the CLI overrides
|
|
1023
|
+
# this. When both are unset symphony binds an ephemeral port (0 → the kernel
|
|
1024
|
+
# picks a free port), so it "just works" with no config and two concurrent
|
|
1025
|
+
# instances never collide; the actually-bound port + dashboard URL are printed
|
|
1026
|
+
# to stdout at startup. Set a value here to pin a fixed port. Default: null
|
|
1027
|
+
# (ephemeral).
|
|
1028
|
+
# port: 8787
|
|
1029
|
+
|
|
1030
|
+
# host (string): bind address. Default: '127.0.0.1'. Bind to '0.0.0.0' only
|
|
1031
|
+
# inside a trusted network boundary; the dashboard has no built-in auth.
|
|
1032
|
+
host: 0.0.0.0
|
|
1033
|
+
|
|
1034
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1035
|
+
# mcp — Model Context Protocol server exposed to in-VM agents.
|
|
1036
|
+
#
|
|
1037
|
+
# The orchestrator runs a JSON-RPC endpoint scoped to each active issue at
|
|
1038
|
+
# /api/v1/issues/<id>/mcp, gated by a per-dispatch bearer token. Three tools
|
|
1039
|
+
# live there:
|
|
1040
|
+
#
|
|
1041
|
+
# • symphony.transition({ to_state, notes? })
|
|
1042
|
+
# — canonical (and only) exit verb. Moves the issue into another declared
|
|
1043
|
+
# state, optionally appending `notes` (markdown) to the issue body before
|
|
1044
|
+
# the move so the next agent (in `to_state`) reads them as part of
|
|
1045
|
+
# `issue.description`. Terminal targets clean the workspace; active and
|
|
1046
|
+
# holding targets preserve it so the same `agent/<id>` git branch
|
|
1047
|
+
# survives the handoff. Rejected transitions return MCP tool-result
|
|
1048
|
+
# errors (isError:true) the agent can read and retry.
|
|
1049
|
+
# • symphony.request_human_steering({ question, context? })
|
|
1050
|
+
# • symphony.propose_issue({ title, description?, labels?, priority? })
|
|
1051
|
+
# — drops a new issue into the first declared `role: holding` state
|
|
1052
|
+
# directory (literal Triage if none declared). The orchestrator does NOT
|
|
1053
|
+
# dispatch it; the operator approves or discards from the dashboard. The
|
|
1054
|
+
# calling issue is recorded as proposed_by in the new file's
|
|
1055
|
+
# front-matter.
|
|
1056
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1057
|
+
mcp:
|
|
1058
|
+
# enabled (bool): when false, the orchestrator refuses to dispatch (MCP is
|
|
1059
|
+
# required for completion signaling). Default: true.
|
|
1060
|
+
enabled: true
|
|
1061
|
+
|
|
1062
|
+
# host (string): hostname or IP the agent uses to reach the orchestrator
|
|
1063
|
+
# from inside the VM. The port is resolved at runtime from the
|
|
1064
|
+
# actually-bound HTTP server. Default: '127.0.0.1' (Gondolin maps VM
|
|
1065
|
+
# loopback to host loopback; verified empirically).
|
|
1066
|
+
host: 127.0.0.1
|
|
1067
|
+
|
|
1068
|
+
# host_url (string | null): full-URL override. When set, used verbatim and
|
|
1069
|
+
# `host` + bound port are ignored. Use only when the VM cannot reach the
|
|
1070
|
+
# orchestrator through the host gateway (e.g. bridge networking with a
|
|
1071
|
+
# fixed reverse-proxy URL). Default: null.
|
|
1072
|
+
host_url: null
|
|
1073
|
+
|
|
1074
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1075
|
+
# prompts — per-state files (there is NO inline prompt body).
|
|
1076
|
+
#
|
|
1077
|
+
# A workflow file is pure YAML config; it carries no prompt body. Each active
|
|
1078
|
+
# state's prompt is its `states.<name>.prompt_file` (a Liquid template) wrapped
|
|
1079
|
+
# in the shared `prompt.preamble_file` / `prompt.footer_file` (see the `prompt:`
|
|
1080
|
+
# and `states:` sections above). The runner renders the matching state's
|
|
1081
|
+
# assembled template fresh on every dispatch, so each state's agent sees only its
|
|
1082
|
+
# own instructions plus the shared preamble / footer.
|
|
1083
|
+
#
|
|
1084
|
+
# Liquid context available to every prompt file ({ issue, attempt }):
|
|
1085
|
+
# issue.identifier — the issue's external id (e.g. "DEMO-42").
|
|
1086
|
+
# issue.title — issue title (string).
|
|
1087
|
+
# issue.state — current state (string, matches a key in `states:`).
|
|
1088
|
+
# issue.description — body text (string or empty). `symphony.transition`
|
|
1089
|
+
# appends its `notes` block here before the file moves, so
|
|
1090
|
+
# the next state's agent reads the previous state's handoff
|
|
1091
|
+
# message verbatim.
|
|
1092
|
+
# issue.priority — number or null.
|
|
1093
|
+
# issue.labels — list of strings (lowercased).
|
|
1094
|
+
# attempt — int, 1-based attempt counter; absent on first attempt.
|
|
1095
|
+
#
|
|
1096
|
+
# Available Liquid filters: standard Shopify Liquid plus `escape_once`.
|
|
1097
|
+
#
|
|
1098
|
+
# See prompts/ alongside WORKFLOW.yaml in this repo for worked prompt files, and
|
|
1099
|
+
# WORKFLOW.minimal.yaml for the smallest single-state example.
|
|
1100
|
+
# ─────────────────────────────────────────────────────────────────────────────
|