smol-symphony 0.1.0 → 0.2.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 +105 -38
- package/PRODUCT.md +2 -1
- package/README.md +195 -98
- package/SPEC.md +543 -1915
- package/WORKFLOW.md +654 -179
- package/WORKFLOW.template.md +761 -121
- package/dist/acp-bridge.js +324 -0
- package/dist/acp-bridge.js.map +1 -0
- package/dist/actions/cache.js +191 -0
- package/dist/actions/cache.js.map +1 -0
- package/dist/actions/effects.js +41 -0
- package/dist/actions/effects.js.map +1 -0
- package/dist/actions/executor.js +570 -0
- package/dist/actions/executor.js.map +1 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/parsing.js +273 -0
- package/dist/actions/parsing.js.map +1 -0
- package/dist/actions/predicate-env.js +27 -0
- package/dist/actions/predicate-env.js.map +1 -0
- package/dist/actions/predicates.js +49 -0
- package/dist/actions/predicates.js.map +1 -0
- package/dist/actions/templating.js +66 -0
- package/dist/actions/templating.js.map +1 -0
- package/dist/actions/types.js +15 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/agent/acp.js +232 -63
- package/dist/agent/acp.js.map +1 -1
- package/dist/agent/adapter-names.js +159 -0
- package/dist/agent/adapter-names.js.map +1 -0
- package/dist/agent/adapters.js +338 -102
- package/dist/agent/adapters.js.map +1 -1
- package/dist/agent/credential-extractors.js +342 -0
- package/dist/agent/credential-extractors.js.map +1 -0
- package/dist/agent/credential-secrets.js +628 -0
- package/dist/agent/credential-secrets.js.map +1 -0
- package/dist/agent/credential-ticker.js +57 -0
- package/dist/agent/credential-ticker.js.map +1 -0
- package/dist/agent/gondolin-creds-staging.js +356 -0
- package/dist/agent/gondolin-creds-staging.js.map +1 -0
- package/dist/agent/gondolin-dispatch.js +375 -0
- package/dist/agent/gondolin-dispatch.js.map +1 -0
- package/dist/agent/gondolin.js +124 -0
- package/dist/agent/gondolin.js.map +1 -0
- package/dist/agent/runner-decisions.js +134 -0
- package/dist/agent/runner-decisions.js.map +1 -0
- package/dist/agent/runner.js +1352 -290
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tool-call-summary.js +102 -0
- package/dist/agent/tool-call-summary.js.map +1 -0
- package/dist/agent/vm-acp-mapping.js +73 -0
- package/dist/agent/vm-acp-mapping.js.map +1 -0
- package/dist/agent/vm-guards.js +262 -0
- package/dist/agent/vm-guards.js.map +1 -0
- package/dist/agent/vm-port.js +22 -0
- package/dist/agent/vm-port.js.map +1 -0
- package/dist/agent/vm-process-registry.js +79 -0
- package/dist/agent/vm-process-registry.js.map +1 -0
- package/dist/bin/cli-args.js +105 -0
- package/dist/bin/cli-args.js.map +1 -0
- package/dist/bin/symphony.js +719 -130
- package/dist/bin/symphony.js.map +1 -1
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-disk.js +135 -0
- package/dist/http-disk.js.map +1 -0
- package/dist/http-handlers.js +180 -0
- package/dist/http-handlers.js.map +1 -0
- package/dist/http.js +1476 -764
- package/dist/http.js.map +1 -1
- package/dist/issues.js +178 -0
- package/dist/issues.js.map +1 -0
- package/dist/logging.js +163 -5
- package/dist/logging.js.map +1 -1
- package/dist/mcp.js +391 -163
- package/dist/mcp.js.map +1 -1
- package/dist/memory.js +85 -0
- package/dist/memory.js.map +1 -0
- package/dist/orchestrator-decisions.js +331 -0
- package/dist/orchestrator-decisions.js.map +1 -0
- package/dist/orchestrator.js +1189 -303
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompt.js +5 -5
- package/dist/prompt.js.map +1 -1
- package/dist/reconciler/cache.js +65 -0
- package/dist/reconciler/cache.js.map +1 -0
- package/dist/reconciler/index.js +448 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/reconciler/ledger.js +131 -0
- package/dist/reconciler/ledger.js.map +1 -0
- package/dist/reconciler/pr-adapters.js +174 -0
- package/dist/reconciler/pr-adapters.js.map +1 -0
- package/dist/reconciler/pr-decide.js +167 -0
- package/dist/reconciler/pr-decide.js.map +1 -0
- package/dist/reconciler/pr.js +422 -0
- package/dist/reconciler/pr.js.map +1 -0
- package/dist/reconciler/types.js +12 -0
- package/dist/reconciler/types.js.map +1 -0
- package/dist/reconciler/vm.js +243 -0
- package/dist/reconciler/vm.js.map +1 -0
- package/dist/reconciler/workspace-defaults.js +83 -0
- package/dist/reconciler/workspace-defaults.js.map +1 -0
- package/dist/reconciler/workspace.js +272 -0
- package/dist/reconciler/workspace.js.map +1 -0
- package/dist/runlog.js +403 -0
- package/dist/runlog.js.map +1 -0
- package/dist/scaffold.js +165 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/trackers/local.js +234 -133
- package/dist/trackers/local.js.map +1 -1
- package/dist/trackers/types.js +1 -1
- package/dist/trackers/types.js.map +1 -1
- package/dist/types.js +1 -1
- package/dist/util/clock.js +12 -0
- package/dist/util/clock.js.map +1 -0
- package/dist/util/crypto.js +25 -0
- package/dist/util/crypto.js.map +1 -0
- package/dist/util/frontmatter.js +70 -0
- package/dist/util/frontmatter.js.map +1 -0
- package/dist/util/fs-issues.js +22 -0
- package/dist/util/fs-issues.js.map +1 -0
- package/dist/util/process.js +152 -0
- package/dist/util/process.js.map +1 -0
- package/dist/util/workspace-key.js +10 -0
- package/dist/util/workspace-key.js.map +1 -0
- package/dist/workflow-loader.js +147 -0
- package/dist/workflow-loader.js.map +1 -0
- package/dist/workflow.js +656 -219
- package/dist/workflow.js.map +1 -1
- package/dist/workspace-types.js +8 -0
- package/dist/workspace-types.js.map +1 -0
- package/dist/workspace.js +367 -120
- package/dist/workspace.js.map +1 -1
- package/package.json +14 -6
- package/scripts/vm-agent.mjs +211 -0
- package/dist/agent/codex.js +0 -439
- package/dist/agent/codex.js.map +0 -1
- package/dist/agent/smolvm.js +0 -174
- package/dist/agent/smolvm.js.map +0 -1
- package/scripts/build-vm.sh +0 -67
package/WORKFLOW.template.md
CHANGED
|
@@ -22,39 +22,385 @@ Notation:
|
|
|
22
22
|
# tracker — where issues come from.
|
|
23
23
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
24
|
tracker:
|
|
25
|
-
# kind (required): 'local'
|
|
26
|
-
#
|
|
27
|
-
# linear — Linear API (workspace + project_slug).
|
|
25
|
+
# kind (required): currently the only supported value is 'local' (markdown
|
|
26
|
+
# files under `root`, one per issue, organized by state subdirectory).
|
|
28
27
|
kind: local
|
|
29
28
|
|
|
30
|
-
# root (path):
|
|
31
|
-
#
|
|
32
|
-
# if not absolute.
|
|
29
|
+
# root (path): directory containing `<state>/<id>.md` files. Required.
|
|
30
|
+
# Resolved relative to the workflow file if not absolute.
|
|
33
31
|
root: ./issues
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
# states — per-state configuration map. REQUIRED. Every workflow must declare
|
|
35
|
+
# at least one `active`, one `terminal`, and one `holding` state; a workflow
|
|
36
|
+
# missing the `states:` block (or missing any of those roles) is rejected at
|
|
37
|
+
# parse time. This map is the only place state names and roles are configured;
|
|
38
|
+
# there are no separate active/terminal lists to keep in sync.
|
|
39
|
+
#
|
|
40
|
+
# Keys are state names; values are config objects with these fields:
|
|
41
|
+
# role (required, enum):
|
|
42
|
+
# active — orchestrator dispatches issues in this state.
|
|
43
|
+
# terminal — orchestrator treats issues in this state as complete; the
|
|
44
|
+
# workspace is removed after the run unwinds.
|
|
45
|
+
# holding — directory exists on disk, but the orchestrator never
|
|
46
|
+
# dispatches issues from it. Triage is the canonical example
|
|
47
|
+
# and the landing directory for `symphony.propose_issue`.
|
|
48
|
+
# adapter (string, optional): override the workflow-level `acp.adapter` for
|
|
49
|
+
# agents dispatched in this state. Must be a known profile (claude,
|
|
50
|
+
# codex, opencode). All use host-side credential substitution at
|
|
51
|
+
# Gondolin egress and are startup-probed so a missing credential
|
|
52
|
+
# fails fast. claude has a
|
|
53
|
+
# single host credential file (~/.claude/.credentials.json) that is
|
|
54
|
+
# probed for readability; codex passes when either ~/.codex/auth.json
|
|
55
|
+
# holds a token (ChatGPT-OAuth tokens.access_token or a top-level
|
|
56
|
+
# OPENAI_API_KEY) or the host OPENAI_API_KEY env var is set; opencode
|
|
57
|
+
# passes when either ~/.local/share/opencode/auth.json holds a
|
|
58
|
+
# github-copilot token (run `opencode auth login` -> GitHub Copilot
|
|
59
|
+
# on the host) or a COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN env
|
|
60
|
+
# var is set.
|
|
61
|
+
# model (string, optional): override `acp.model` for this state.
|
|
62
|
+
# Blank or whitespace-only values normalize to "use the adapter
|
|
63
|
+
# default" (same as the workflow-level acp.model semantics).
|
|
64
|
+
# effort (string, optional): override `acp.effort` for this state. Same
|
|
65
|
+
# undefined-vs-null semantics as `model`: omit to inherit
|
|
66
|
+
# `acp.effort`; blank/whitespace normalizes to null ("use the
|
|
67
|
+
# adapter default for this state"). Valid values are adapter- and
|
|
68
|
+
# model-specific (see `acp.effort`).
|
|
69
|
+
# max_turns (int, optional): override `agent.max_turns` for this state.
|
|
70
|
+
# max_concurrent (int, optional): cap on agents the orchestrator runs
|
|
71
|
+
# simultaneously for issues in THIS state. Symmetric with
|
|
72
|
+
# `max_turns` — concurrency and turn budget both live on the state.
|
|
73
|
+
# Omit for "no per-state cap; only the global
|
|
74
|
+
# `agent.max_concurrent_agents` ceiling applies". The sum of every
|
|
75
|
+
# state's `max_concurrent` must not exceed that global ceiling
|
|
76
|
+
# (validated at startup).
|
|
77
|
+
# allowed_transitions (string[]|null, optional): when set, restricts which
|
|
78
|
+
# states agents in this state may transition to via the MCP
|
|
79
|
+
# `transition` tool. Each entry must be a declared state. Omit (or
|
|
80
|
+
# explicitly set to null) for "any declared state is reachable".
|
|
81
|
+
# An empty list (`allowed_transitions: []`) means "no transitions
|
|
82
|
+
# allowed out of this state" — the agent's `transition` calls will
|
|
83
|
+
# always be rejected with `transition_not_allowed`. Useful for
|
|
84
|
+
# review-style states that should pause until a human re-routes.
|
|
85
|
+
# eval_mode (bool, optional): when true, the runner adds two extra read-only
|
|
86
|
+
# bind mounts to every per-issue VM dispatched in this state so an
|
|
87
|
+
# in-VM agent can inspect symphony's own state for evaluation /
|
|
88
|
+
# debugging:
|
|
89
|
+
# • `tracker.root` → `/symphony/issues` (every issue file across
|
|
90
|
+
# every state directory)
|
|
91
|
+
# • `logs.root` → `/symphony/logs` (per-issue JSONL run-log
|
|
92
|
+
# transcripts captured by RunLog — ACP frames, stderr,
|
|
93
|
+
# typed-action output, system events — plus the compact
|
|
94
|
+
# `<key>.summary.json` outcome records the reflector reads; see
|
|
95
|
+
# the `logs:` block below)
|
|
96
|
+
# Either mount is skipped silently if the corresponding root is
|
|
97
|
+
# unset. Each VFS mount has a cost, so this is opt-in per state rather
|
|
98
|
+
# than a workflow-wide default — flip it on for a dedicated eval
|
|
99
|
+
# state, not for the routine implement/review flow. Default: false.
|
|
100
|
+
# The canonical consumer is the "sleep cycle" reflection pattern —
|
|
101
|
+
# see the SLEEP CYCLE section below the states block.
|
|
102
|
+
# actions (list, optional): typed action DAG (issue 36, reconciler v2).
|
|
103
|
+
# The single glue mechanism attached to a state — there is no shell
|
|
104
|
+
# `hooks:` surface. When set on a `terminal` state, this list runs
|
|
105
|
+
# on transition INTO the state (the canonical Done-state pair is
|
|
106
|
+
# push_branch + create_pr_if_missing). For arbitrary in-sandbox
|
|
107
|
+
# commands use a `run_in_vm` action; per-VM tooling belongs in the
|
|
108
|
+
# agent image (`images/agents/`); first-creation workspace setup is
|
|
109
|
+
# owned by the orchestrator's `setupWorkspaceDir`.
|
|
110
|
+
# Each entry is a closed-kind record:
|
|
111
|
+
#
|
|
112
|
+
# - kind: push_branch
|
|
113
|
+
# remote: origin
|
|
114
|
+
# ref: $branch
|
|
115
|
+
# if: $repo
|
|
116
|
+
#
|
|
117
|
+
# Recognized kinds:
|
|
118
|
+
# push_branch { remote, ref }
|
|
119
|
+
# create_pr_if_missing { base, head, title_from, body_from }
|
|
120
|
+
# ensure_branch { name, seed_from? }
|
|
121
|
+
# checkout { ref }
|
|
122
|
+
# merge { source, target, on_conflict }
|
|
123
|
+
# delete_branch { name, scope: local|remote|both, remote? }
|
|
124
|
+
# run_in_vm { name, cmd: [...], env?, timeout? }
|
|
125
|
+
# propose_followup { title, body?, labels?, priority? }
|
|
126
|
+
#
|
|
127
|
+
# Templating: `$varname` references the fixed ActionContext
|
|
128
|
+
# namespace ($identifier, $workspace, $branch, $base_branch,
|
|
129
|
+
# $issue_title, $issue_body, $repo, $pr_title, $pr_body_file).
|
|
130
|
+
# Unknown $vars throw at run-time (no silent "" expansion).
|
|
131
|
+
#
|
|
132
|
+
# Conditional: optional `if:` field supports three predicates
|
|
133
|
+
# - `if: $repo` (env-var-truthy)
|
|
134
|
+
# - `if: { branch_exists: <ref> }` (workspace branch)
|
|
135
|
+
# - `if: { file_present: <path> }` (workspace file)
|
|
136
|
+
#
|
|
137
|
+
# Retry: optional `on_error.retry: { count, backoff_ms }`. Default
|
|
138
|
+
# policy is 3 retries with exponential backoff starting at 1s,
|
|
139
|
+
# then abort. `on_error.then: { route_to: <state> }` reroutes the
|
|
140
|
+
# issue to a holding state instead of aborting.
|
|
141
|
+
#
|
|
142
|
+
# merge's `on_conflict: { route_to: <state> }` is a fast-path
|
|
143
|
+
# reroute. Use `on_conflict: abort` to fail the action and abort
|
|
144
|
+
# the cleanup pass without a state move.
|
|
145
|
+
#
|
|
146
|
+
# run_in_vm has content-hash caching: identical (workspace tree
|
|
147
|
+
# ⊕ cmd ⊕ env) tuples skip execution and re-use the prior
|
|
148
|
+
# successful result. The workspace-tree hash reflects live
|
|
149
|
+
# contents (tracked + modified + untracked-not-gitignored), so
|
|
150
|
+
# an uncommitted agent edit forces a cache miss. Cache lives
|
|
151
|
+
# under `~/.cache/symphony/actions/run_in_vm/<name>/<sha256>/`.
|
|
152
|
+
# `symphony rerun --check=<name>` drops the whole `<name>/`
|
|
153
|
+
# namespace dir so the next dispatch re-runs that one check.
|
|
154
|
+
# pr (map, optional): PR autopilot routing for this state (issue 139).
|
|
155
|
+
# Valid only on a `terminal` state, and only acts when the
|
|
156
|
+
# top-level `pr:` engine block (below) has `enabled: true`. This is
|
|
157
|
+
# where the routing the engine used to name by string now lives —
|
|
158
|
+
# on the state it describes. Two shapes:
|
|
159
|
+
# • Merge state: pr: { auto_merge: squash|merge|rebase,
|
|
160
|
+
# on_conflict: { route_to: <active state> } }
|
|
161
|
+
# A MERGEABLE PR for an issue in this state has GitHub
|
|
162
|
+
# auto-merge armed with `auto_merge`; a CONFLICTING one is
|
|
163
|
+
# routed back to `on_conflict.route_to` (defaults to the first
|
|
164
|
+
# declared active state when omitted) for the dispatched agent
|
|
165
|
+
# to rebase. While the engine is enabled, transitions INTO this
|
|
166
|
+
# state do not fire the standard terminal workspace cleanup —
|
|
167
|
+
# the pr resource owns the workspace until the PR merges/closes.
|
|
168
|
+
# • Close state: pr: { close: true }
|
|
169
|
+
# An open PR for an issue in this state is closed without merge
|
|
170
|
+
# and its remote branch is best-effort-deleted. Standard
|
|
171
|
+
# terminal cleanup still runs (the close path needs no workspace).
|
|
172
|
+
# The merge/close/route targets are DERIVED by scanning states for
|
|
173
|
+
# this field — there is no named-string sibling block. At most one
|
|
174
|
+
# terminal state may declare `auto_merge`, and at most one may
|
|
175
|
+
# declare `close`; an `on_conflict.route_to` naming an undeclared
|
|
176
|
+
# state is rejected at parse time.
|
|
177
|
+
#
|
|
178
|
+
# Declaration order matters: role-filtered listings (active states, terminal
|
|
179
|
+
# states) follow it, and the dashboard renders state columns in the same order.
|
|
180
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
states:
|
|
182
|
+
Todo:
|
|
183
|
+
role: active
|
|
184
|
+
adapter: claude
|
|
185
|
+
model: claude-opus-4-7
|
|
186
|
+
effort: xhigh
|
|
187
|
+
max_turns: 10
|
|
188
|
+
max_concurrent: 1 # at most one implementer agent at a time
|
|
189
|
+
Review:
|
|
190
|
+
role: active
|
|
191
|
+
adapter: codex
|
|
192
|
+
model: gpt-5-codex
|
|
193
|
+
max_turns: 4
|
|
194
|
+
allowed_transitions: [Todo, Done]
|
|
195
|
+
Done:
|
|
196
|
+
role: terminal
|
|
197
|
+
# PR autopilot merge state (issue 139). Acts only when the top-level `pr:`
|
|
198
|
+
# engine block (below) is enabled. Derived as the merge state because it
|
|
199
|
+
# declares `auto_merge`.
|
|
200
|
+
# pr:
|
|
201
|
+
# auto_merge: squash
|
|
202
|
+
# on_conflict: { route_to: Todo }
|
|
203
|
+
# Terminal-state handoff via typed actions (issue 36). On transition into
|
|
204
|
+
# Done, push the branch (if SYMPHONY_REPO is set → $repo non-empty) and
|
|
205
|
+
# open a PR if one does not already exist. Templates resolve against the
|
|
206
|
+
# fixed ActionContext namespace; the orchestrator stages $pr_title and
|
|
207
|
+
# $pr_body_file from the issue file before firing.
|
|
208
|
+
# actions:
|
|
209
|
+
# - { kind: push_branch, remote: origin, ref: $branch, if: $repo }
|
|
210
|
+
# - kind: create_pr_if_missing
|
|
211
|
+
# base: $base_branch
|
|
212
|
+
# head: $branch
|
|
213
|
+
# title_from: $pr_title
|
|
214
|
+
# body_from: $pr_body_file
|
|
215
|
+
# if: $repo
|
|
216
|
+
Cancelled:
|
|
217
|
+
role: terminal
|
|
218
|
+
# PR autopilot close state (issue 139). Derived as the close state because
|
|
219
|
+
# it declares `close: true` — an open PR is closed without merge.
|
|
220
|
+
# pr:
|
|
221
|
+
# close: true
|
|
222
|
+
Triage:
|
|
223
|
+
role: holding
|
|
45
224
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
225
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
# SLEEP CYCLE — a reflection state that mines finished work for harness
|
|
227
|
+
# improvements (issue 122). Optional, opt-in pattern; layered on top of the
|
|
228
|
+
# states block above. The shipped smol-symphony WORKFLOW.md wires it for the
|
|
229
|
+
# dogfooding (symphony-on-symphony) setup.
|
|
230
|
+
#
|
|
231
|
+
# The idea: every dispatch starts from the same static prompt + config, no
|
|
232
|
+
# matter what the last 100 issues taught us about where agents stall, get
|
|
233
|
+
# rejected, burn their turn budget, or fight the harness. A periodic
|
|
234
|
+
# "reflection" turn closes that feedback loop — it reads completed-task history
|
|
235
|
+
# (the read-only mounts `eval_mode` exposes), distils *recurring* friction, and
|
|
236
|
+
# files improvement proposals against the HARNESS (this WORKFLOW.md's prompt
|
|
237
|
+
# branches and per-state model/max_turns/allowed_transitions/effort/actions, the
|
|
238
|
+
# gondolin image config, acceptance criteria, timeouts) — never the product code under
|
|
239
|
+
# review. Proposals land in Triage via `propose_issue`, so a human stays the
|
|
240
|
+
# gate. This is the "self-improving agent" pattern aimed at the harness rather
|
|
241
|
+
# than the product.
|
|
242
|
+
#
|
|
243
|
+
# Two states implement it:
|
|
244
|
+
#
|
|
245
|
+
# Reflect (role: active, eval_mode: true):
|
|
246
|
+
# - eval_mode binds /symphony/issues (all state dirs, incl. the Done/*.md
|
|
247
|
+
# handoff transcripts) + /symphony/logs (per-issue JSONL run logs)
|
|
248
|
+
# read-only into the VM. No extra mount plumbing — it reuses the existing
|
|
249
|
+
# eval_mode mounts.
|
|
250
|
+
# - Give it a capable adapter/model (large context helps: a reflection turn
|
|
251
|
+
# reads many transcripts + logs) and a higher max_turns than your
|
|
252
|
+
# implement/review states.
|
|
253
|
+
# - allowed_transitions: [Dormant] — the reflector may ONLY go dormant. It
|
|
254
|
+
# cannot route itself into the implement/review/done flow. Filing
|
|
255
|
+
# improvements goes through propose_issue (→ Triage), which is independent
|
|
256
|
+
# of allowed_transitions.
|
|
257
|
+
# - The prompt body's `when "Reflect"` branch encodes the
|
|
258
|
+
# read → distil → propose loop and the GUARDRAILS below.
|
|
259
|
+
#
|
|
260
|
+
# Dormant (role: holding):
|
|
261
|
+
# - Resting place for the single recurring "Sleep cycle" issue between runs.
|
|
262
|
+
# Holding → never dispatched. Declare it AFTER your Triage state so Triage
|
|
263
|
+
# stays the first holding state (the `propose_issue` landing + triage
|
|
264
|
+
# approve/discard target both resolve the FIRST declared holding state).
|
|
265
|
+
# - Dashboard caveat: the dashboard currently renders triage approve/discard
|
|
266
|
+
# buttons on every holding row, and the tracker resolves a move by issue
|
|
267
|
+
# id regardless of source directory — so clicking those buttons on a
|
|
268
|
+
# Dormant issue would mis-route it. Re-arm via cron/CLI/filesystem, not the
|
|
269
|
+
# dashboard buttons.
|
|
270
|
+
#
|
|
271
|
+
# GUARDRAILS (this is a self-modifying loop — keep the human in it):
|
|
272
|
+
# - Output is proposals into Triage (holding, never auto-dispatched). The
|
|
273
|
+
# operator approves/discards. Do not bypass this gate.
|
|
274
|
+
# - Constrain the proposal surface to harness config. Forbid any proposal that
|
|
275
|
+
# weakens the Review state, the test/lint gates, or the Triage gate itself.
|
|
276
|
+
# - Each proposal must cite the issue ids that motivated it, so the operator
|
|
277
|
+
# checks the lesson against the evidence rather than the reflector's summary.
|
|
278
|
+
#
|
|
279
|
+
# CADENCE (v1 — operator/scheduled-triggered, no orchestrator trigger logic):
|
|
280
|
+
# A single recurring issue (e.g. titled "Sleep cycle") oscillates Reflect ↔
|
|
281
|
+
# Dormant. The operator drops it into Reflect (dashboard, or `mv` on disk), or
|
|
282
|
+
# an external cron / a `symphony reflect` verb arms it. After it files
|
|
283
|
+
# proposals it transitions to Dormant and waits to be re-armed. Auto-arm on
|
|
284
|
+
# idle (no active issues) or after N transitions into Done is a deliberate
|
|
285
|
+
# follow-up, out of scope for v1.
|
|
286
|
+
#
|
|
287
|
+
# Example states to add (names are yours to choose):
|
|
288
|
+
#
|
|
289
|
+
# states:
|
|
290
|
+
# # ... your active/terminal states ...
|
|
291
|
+
# Reflect:
|
|
292
|
+
# role: active
|
|
293
|
+
# adapter: claude
|
|
294
|
+
# model: claude-opus-4-8[1m] # large context for reading transcripts
|
|
295
|
+
# max_turns: 20 # higher than implement/review
|
|
296
|
+
# eval_mode: true
|
|
297
|
+
# allowed_transitions: [Dormant]
|
|
298
|
+
# Triage:
|
|
299
|
+
# role: holding # declared before Dormant
|
|
300
|
+
# Dormant:
|
|
301
|
+
# role: holding
|
|
302
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
51
303
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
304
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
305
|
+
# pr — PR autopilot engine toggle: arm GitHub auto-merge when a terminal-state
|
|
306
|
+
# PR is mergeable; route non-mergeable PRs back to the implementing state.
|
|
307
|
+
#
|
|
308
|
+
# Optional. This is the SLIM host-global half only — the on/off switch and the
|
|
309
|
+
# per-PR `gh pr view` cache TTL. The merge/close/route targets and the
|
|
310
|
+
# auto-merge strategy live ON the terminal states they describe, as
|
|
311
|
+
# `states.<name>.pr` (see the `pr:` field doc in the states block above):
|
|
312
|
+
# • the MERGE state declares `pr: { auto_merge: <strategy>,
|
|
313
|
+
# on_conflict: { route_to: <active state> } }`
|
|
314
|
+
# • the CLOSE state declares `pr: { close: true }`
|
|
315
|
+
# The reconciler derives the targets by scanning states for that field — there
|
|
316
|
+
# is no named-string sibling block here.
|
|
317
|
+
#
|
|
318
|
+
# When `enabled: true` the reconciler grows a `pr` resource that, on every tick,
|
|
319
|
+
# looks up each issue in the merge state via `gh pr list --head agent/<id>`,
|
|
320
|
+
# fetches its detail with `gh pr view`, and:
|
|
321
|
+
#
|
|
322
|
+
# • Arms GitHub's auto-merge when the PR is `mergeable: MERGEABLE`
|
|
323
|
+
# (`gh pr merge --auto --<strategy> --delete-branch`, where `<strategy>` is
|
|
324
|
+
# the merge state's `pr.auto_merge`). GitHub merges as soon as required
|
|
325
|
+
# checks pass and review requirements are satisfied.
|
|
326
|
+
# • When the PR is `mergeable: CONFLICTING`, appends a structured notes block
|
|
327
|
+
# to the issue file and routes the issue from the merge state back to that
|
|
328
|
+
# state's `pr.on_conflict.route_to` (default: the first declared
|
|
329
|
+
# `role: active` state). The workspace + `agent/<id>` branch are preserved.
|
|
330
|
+
# Before the next dispatch symphony runs `git fetch origin <base>` so
|
|
331
|
+
# `origin/<base>` is current, and the Todo prompt's first step is
|
|
332
|
+
# `git rebase origin/<base>` — resolving the conflict is the agent's normal
|
|
333
|
+
# flow, not an out-of-band autopilot operation.
|
|
334
|
+
# • For an issue in the close state with an open PR, closes the PR without
|
|
335
|
+
# merge and best-effort-deletes the remote branch.
|
|
336
|
+
#
|
|
337
|
+
# Requires `gh` authenticated on the host (`gh auth status` clean). The token
|
|
338
|
+
# never enters the VM. Auto-merge ALSO requires at least one branch protection
|
|
339
|
+
# rule on the base branch, or `gh pr merge --auto` will error — set one in
|
|
340
|
+
# the repo's GitHub settings before flipping `enabled: true`.
|
|
341
|
+
#
|
|
342
|
+
# When `enabled: false` (or the block is absent) the autopilot is fully inert:
|
|
343
|
+
# the resource is never constructed and the orchestrator's existing Done-state
|
|
344
|
+
# behavior (workspace cleanup + the Done-state `actions:` block that pushes the
|
|
345
|
+
# branch and opens the PR + operator-merge) is unchanged.
|
|
346
|
+
#
|
|
347
|
+
# Workspace lifecycle gotcha: when `enabled: true`, transitions into the merge
|
|
348
|
+
# state no longer fire the standard terminal workspace cleanup. The pr resource
|
|
349
|
+
# owns the workspace from that point on and removes it once the PR has merged
|
|
350
|
+
# (or been closed). Transitions into the close state (and any other terminal
|
|
351
|
+
# state) keep the standard cleanup-on-transition behavior.
|
|
352
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
pr:
|
|
354
|
+
# enabled (bool): master switch. Default false.
|
|
355
|
+
enabled: false
|
|
356
|
+
|
|
357
|
+
# poll_interval_ms (int): per-PR GitHub view cache TTL, milliseconds. The
|
|
358
|
+
# reconciler may tick more often than this; a single PR view is reused
|
|
359
|
+
# within the window. Default 30000.
|
|
360
|
+
poll_interval_ms: 30000
|
|
361
|
+
# ----------------------------------------------------------------------------
|
|
362
|
+
# SLEEP CYCLE (auto-arm) — issue 140 moved this trigger ONTO the active state it
|
|
363
|
+
# arms. There is NO top-level `sleep_cycle:` block anymore; declare an `arm:`
|
|
364
|
+
# block on the active state (e.g. Reflect) in the `states:` map above:
|
|
365
|
+
#
|
|
366
|
+
# states:
|
|
367
|
+
# Reflect:
|
|
368
|
+
# role: active
|
|
369
|
+
# arm:
|
|
370
|
+
# issue: sleep-cycle # the recurring issue to auto-enter this state
|
|
371
|
+
# from: Dormant # the holding state it rests in between runs
|
|
372
|
+
# on_idle: true # arm when idle with >=1 terminal since last run
|
|
373
|
+
# after_terminal: 10 # arm after N terminal transitions (0 disables)
|
|
374
|
+
#
|
|
375
|
+
# SEMANTICS (unchanged from the old sleep_cycle block):
|
|
376
|
+
# - on_idle: arm when the orchestrator is idle (nothing running, claimed, or
|
|
377
|
+
# pending retry, and no active candidate this poll) AND >=1 issue has reached
|
|
378
|
+
# a terminal state since the last run. The ">=1 since last run" gate is
|
|
379
|
+
# load-bearing: without it an idle orchestrator re-arms in a tight loop with
|
|
380
|
+
# nothing new to mine.
|
|
381
|
+
# - after_terminal: a backstop for busy stretches that never go idle — arm once
|
|
382
|
+
# this many issues have reached a terminal state (Done/Cancelled) since the
|
|
383
|
+
# last run. 0 disables the count trigger.
|
|
384
|
+
# The terminal-transition counter resets to 0 the moment the issue is armed and
|
|
385
|
+
# is held in orchestrator memory only (a restart resets it).
|
|
386
|
+
#
|
|
387
|
+
# VALIDATION (structural, from each state's own role — no dedicated re-validator):
|
|
388
|
+
# `arm:` is only valid on an `active` state, `arm.from` must be a declared
|
|
389
|
+
# `holding` state, `arm.issue` is required, and at most one active state may
|
|
390
|
+
# declare `arm`.
|
|
391
|
+
#
|
|
392
|
+
# GUARDRAILS: arming ONLY moves the issue into the armed state. The proposals the
|
|
393
|
+
# reflector files still land in Triage and still require human approve/discard —
|
|
394
|
+
# arming does not bypass the human gate. Requires a single issue with id
|
|
395
|
+
# `arm.issue` resting in `arm.from` (created by the operator); the trigger is
|
|
396
|
+
# inert until that issue exists.
|
|
397
|
+
#
|
|
398
|
+
# MIGRATION: a deprecated top-level `sleep_cycle:` block is still parsed for one
|
|
399
|
+
# release and folded onto states.<reflect_state>.arm (dormant_state -> from,
|
|
400
|
+
# reflect_state -> the armed state, issue_id -> issue, arm_on_idle -> on_idle,
|
|
401
|
+
# arm_after_done -> after_terminal) with a startup deprecation warning. Prefer
|
|
402
|
+
# the per-state `arm:` block.
|
|
403
|
+
# ----------------------------------------------------------------------------
|
|
58
404
|
|
|
59
405
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
60
406
|
# polling — how often to poll the tracker.
|
|
@@ -72,59 +418,156 @@ workspace:
|
|
|
72
418
|
# Default: $TMPDIR/symphony_workspaces
|
|
73
419
|
root: ./.symphony/workspaces
|
|
74
420
|
|
|
421
|
+
# github_repo (string | null): the GitHub `owner/repo` slug symphony pushes the
|
|
422
|
+
# per-issue `agent/<id>` branch to and opens a PR against on the Done-state
|
|
423
|
+
# transition. This is the config-file home for what was previously the
|
|
424
|
+
# `SYMPHONY_REPO` env var only.
|
|
425
|
+
# • unset / null / "none" → LOCAL-ONLY mode: no `origin` is restored, nothing
|
|
426
|
+
# is pushed, and the branch is left in the workspace for `git log agent/<id>`.
|
|
427
|
+
# • "owner/repo" → PR mode: the canonical workspace setup restores an
|
|
428
|
+
# `origin` at https://github.com/<owner>/<repo>.git and the Done-state
|
|
429
|
+
# actions push + `gh pr create`. Requires `gh auth status` clean on the host
|
|
430
|
+
# (the token never enters the VM).
|
|
431
|
+
# GitHub-only by construction (the origin URL and `gh` are hardwired). There is
|
|
432
|
+
# NO auto-detection — set the literal slug or leave it local-only. A malformed
|
|
433
|
+
# value (a URL, a bare name, extra slashes/whitespace) is REJECTED at parse time
|
|
434
|
+
# rather than silently falling back to local-only.
|
|
435
|
+
#
|
|
436
|
+
# Precedence: the `SYMPHONY_REPO` env var, when set non-empty, OVERRIDES this
|
|
437
|
+
# field (so existing exports keep working); otherwise this value is used.
|
|
438
|
+
# Default: null.
|
|
439
|
+
# github_repo: owner/repo
|
|
440
|
+
|
|
441
|
+
# base_branch (string): the branch the per-issue workspace clones from and
|
|
442
|
+
# (in PR mode) targets as the PR base. The source repo's local
|
|
443
|
+
# `<base_branch>` is the canonical base ref — symphony does NOT implicitly
|
|
444
|
+
# fetch `origin/<base_branch>` at setup time; update the source repo before
|
|
445
|
+
# the next dispatch to move the base.
|
|
446
|
+
# Precedence: the `SYMPHONY_BASE_BRANCH` env var, when set non-empty,
|
|
447
|
+
# OVERRIDES this field; otherwise this value is used.
|
|
448
|
+
# Default: main.
|
|
449
|
+
base_branch: main
|
|
450
|
+
|
|
75
451
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
-
#
|
|
452
|
+
# logs — per-issue JSONL run logs (everything to/from the VM, plus typed-action
|
|
453
|
+
# output) AND the orchestrator-side text log mirrored to disk for offline debugging.
|
|
454
|
+
#
|
|
455
|
+
# Per-issue: one file per issue at `<root>/<sanitized-identifier>.jsonl`,
|
|
456
|
+
# appended across attempts AND across symphony process restarts. Each line is
|
|
457
|
+
# a self-describing JSON object with `ts`, `issue_id`, `attempt`, and a
|
|
458
|
+
# `channel` discriminator:
|
|
459
|
+
#
|
|
460
|
+
# channel: "acp" — JSON-RPC frame between host and the in-VM adapter.
|
|
461
|
+
# `direction` ("host_to_vm" | "vm_to_host") and `frame`
|
|
462
|
+
# (parsed JSON) — or `kind: "unparseable"` + `raw`.
|
|
463
|
+
# channel: "stderr" — raw byte chunk from the adapter / VM stderr.
|
|
464
|
+
# channel: "hook" — stdout/stderr chunk from a terminal-state `actions:`
|
|
465
|
+
# run, plus a final `kind: "result"` line (exit_code,
|
|
466
|
+
# signal, timed_out). The `hook` field names the action
|
|
467
|
+
# group (e.g. `actions`). (The channel name is a holdover
|
|
468
|
+
# from the retired workflow-hook surface.)
|
|
469
|
+
# channel: "system" — orchestrator lifecycle events (attempt_started — which
|
|
470
|
+
# also carries the per-state `max_turns` budget,
|
|
471
|
+
# attempt_ended, transition, reconciliation_terminating,
|
|
472
|
+
# etc.). The `transition` event records each state move
|
|
473
|
+
# (from_state, to_state, notes, actor, terminal,
|
|
474
|
+
# rerouted) so the trajectory is reconstructable.
|
|
475
|
+
#
|
|
476
|
+
# Per-issue run summary (for the sleep-cycle reflector): alongside each
|
|
477
|
+
# `<root>/<key>.jsonl`, the orchestrator writes a compact, versioned
|
|
478
|
+
# `<root>/<key>.summary.json` at the issue's terminal unwind. It is a pure
|
|
479
|
+
# REDUCTION over the lifecycle (`system`) events already in the JSONL — no extra
|
|
480
|
+
# hot-path logging — so a reflection turn can read dozens of summaries without
|
|
481
|
+
# re-parsing multi-MB frame logs. Fields (schema_version 1):
|
|
482
|
+
# • state_path — distinct states visited, terminal appended
|
|
483
|
+
# (e.g. ["Todo","Review","Todo","Review","Done"]);
|
|
484
|
+
# • attempts — total dispatched attempts;
|
|
485
|
+
# • per_state[] — {state, attempts, turns_used, max_turns,
|
|
486
|
+
# budget_exhausted, wall_clock_ms};
|
|
487
|
+
# • review_rejections + rejection_notes[] — count and each reviewer kick-back's
|
|
488
|
+
# notes (a non-reroute transition back to the INITIAL
|
|
489
|
+
# implementing state, i.e. a Review→Todo rework);
|
|
490
|
+
# • turn_budget_exhausted, timeouts[] (stall / prompt_timeout / transport);
|
|
491
|
+
# • conflict_routes[] — PR-autopilot / action reroutes (rebase churn);
|
|
492
|
+
# • terminal_state + terminal_outcome (completed | cancelled | incomplete);
|
|
493
|
+
# • pr_number / pr_url (best-effort, scraped from the Done-state actions
|
|
494
|
+
# stdout; null when unavailable);
|
|
495
|
+
# • first/last_event_at, wall_clock_ms_total, generated_at.
|
|
496
|
+
# Graceful absence / backfill: the summary is best-effort. Issues that closed
|
|
497
|
+
# BEFORE this feature shipped have no `*.summary.json`; a write failure or a
|
|
498
|
+
# mid-issue process restart (the in-memory accumulator only sees post-restart
|
|
499
|
+
# attempts) can leave it missing or partial. The reflector MUST treat an absent
|
|
500
|
+
# or partial summary as "no signal for this issue" and fall back to the raw
|
|
501
|
+
# JSONL (or skip the issue) — never as an error. No backfill job is run; old
|
|
502
|
+
# issues simply carry no summary.
|
|
77
503
|
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
504
|
+
# Orchestrator-side: a single `<root>/symphony.log` (created on demand) gets
|
|
505
|
+
# every structured log line symphony emits — workflow loads, dispatch
|
|
506
|
+
# decisions, action results, reconciler ticks, shutdown — in `key=value` text
|
|
507
|
+
# format. Lets an agent reviewing a finished run (typically with
|
|
508
|
+
# `.symphony/logs/` mounted into a VM) replay orchestrator-side events
|
|
509
|
+
# alongside the per-issue JSONL traces in the same directory. Set the
|
|
510
|
+
# `SYMPHONY_LOG_FILE` env var to override the path; set it to the empty
|
|
511
|
+
# string to disable the file sink entirely (stderr remains).
|
|
80
512
|
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
513
|
+
# Console routing: while the file sink is active (the default), the structured
|
|
514
|
+
# stream goes to the file ONLY — the console shows just the startup banner
|
|
515
|
+
# (workflow, tracker root, dashboard URL, log-file path). `tail -f` the log
|
|
516
|
+
# file to follow the detail. Pass `--verbose` (alias `--foreground`) to mirror
|
|
517
|
+
# the structured stream back onto the console for interactive debugging. With
|
|
518
|
+
# no file sink configured, the structured stream stays on stderr.
|
|
86
519
|
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
520
|
+
# Intended for later evaluation — typically by another agent running inside a VM
|
|
521
|
+
# — so the schema is verbose on purpose. Writes are best-effort: a failure to
|
|
522
|
+
# write a log line never crashes the orchestrator.
|
|
523
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
524
|
+
logs:
|
|
525
|
+
# root (path): directory holding per-issue JSONL files and symphony.log.
|
|
526
|
+
# Default: ./.symphony/logs
|
|
527
|
+
root: ./.symphony/logs
|
|
528
|
+
|
|
529
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
530
|
+
# workspace lifecycle — no shell `hooks:` surface.
|
|
531
|
+
#
|
|
532
|
+
# Symphony has no `hooks:` block (workflow-level or per-state). The behaviors
|
|
533
|
+
# the old `after_create` / `before_run` / `after_run` / `before_remove` hooks
|
|
534
|
+
# covered now live in typed, testable homes:
|
|
535
|
+
#
|
|
536
|
+
# • First-creation workspace setup (clone + base checkout + `agent/<id>`
|
|
537
|
+
# branch cut + origin/identity) is owned by the orchestrator's TypeScript
|
|
538
|
+
# `setupWorkspaceDir`. The workspace arrives at the agent with: a hardlinked
|
|
539
|
+
# `git clone --local --no-tags` of the source repo (`SYMPHONY_SOURCE_REPO`,
|
|
540
|
+
# default: the dir containing WORKFLOW.md) on the base branch
|
|
541
|
+
# (`SYMPHONY_BASE_BRANCH`, default `main`); all network remotes stripped; an
|
|
542
|
+
# `origin` restored to the canonical HTTPS URL when `workspace.github_repo`
|
|
543
|
+
# (or the `SYMPHONY_REPO` env override) is set (so the Done-state push can
|
|
544
|
+
# reach a remote — `gh auth setup-git` runs host-side; the token never
|
|
545
|
+
# enters the VM); pinned `--local` git identity;
|
|
546
|
+
# and `agent/<id>` checked out off the base SHA.
|
|
547
|
+
# • Per-VM tooling (extra CLIs, language runtimes, dependency bootstrap) is
|
|
548
|
+
# baked into the agent image — see `gondolin.image` and `images/agents/`.
|
|
549
|
+
# • Arbitrary in-sandbox commands run from a state's `actions:` via a
|
|
550
|
+
# `run_in_vm` action (executes inside the per-issue VM, not on the host).
|
|
551
|
+
# • The post-attempt handoff (push branch, open PR) is the Done state's
|
|
552
|
+
# `actions:` block — see `states.Done.actions` above for the canonical pair
|
|
553
|
+
# (push_branch + create_pr_if_missing). The action executor exposes
|
|
554
|
+
# `$branch`, `$base_branch`, `$pr_title`, `$pr_body_file`, `$repo`.
|
|
555
|
+
#
|
|
556
|
+
# Workspace removal is a plain best-effort `rm -rf` of the per-issue dir once
|
|
557
|
+
# the run unwinds; there is no pre-removal artifact-rescue hook (rescue what you
|
|
558
|
+
# need via a terminal-state action before transitioning).
|
|
90
559
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
-
hooks:
|
|
92
|
-
# timeout_ms (int): max wall time for a single hook invocation.
|
|
93
|
-
# Default: 60000
|
|
94
|
-
timeout_ms: 120000
|
|
95
|
-
|
|
96
|
-
# after_create (string | null): runs right after the workspace directory is
|
|
97
|
-
# created, before the first dispatch. Use for git clone, dependency install,
|
|
98
|
-
# etc. Default: null.
|
|
99
|
-
after_create: |
|
|
100
|
-
set -eu
|
|
101
|
-
# ... your setup ...
|
|
102
|
-
|
|
103
|
-
# before_run (string | null): runs before each turn. Default: null. Use for
|
|
104
|
-
# cheap "make sure the workspace is sane" checks; expensive setup belongs in
|
|
105
|
-
# after_create.
|
|
106
|
-
before_run: |
|
|
107
|
-
set -eu
|
|
108
|
-
# ... pre-turn checks ...
|
|
109
|
-
|
|
110
|
-
# after_run (string | null): runs after each turn, regardless of outcome.
|
|
111
|
-
# Inspect cwd or the tracker to decide whether work is complete. Default: null.
|
|
112
|
-
after_run: |
|
|
113
|
-
set -eu
|
|
114
|
-
# ... post-turn handoff (push, format-patch, …) ...
|
|
115
|
-
|
|
116
|
-
# before_remove (string | null): runs before the workspace directory is
|
|
117
|
-
# deleted. Use to extract artifacts you want to keep. Default: null.
|
|
118
|
-
before_remove: |
|
|
119
|
-
set -eu
|
|
120
|
-
# ... rescue artifacts ...
|
|
121
560
|
|
|
122
561
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
123
562
|
# agent — concurrency and turn budget.
|
|
124
563
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
125
564
|
agent:
|
|
126
|
-
# max_concurrent_agents (int):
|
|
127
|
-
# the whole workflow.
|
|
565
|
+
# max_concurrent_agents (int): GLOBAL host ceiling on simultaneously-running
|
|
566
|
+
# agents across the whole workflow. This is the cross-state RAM bound that
|
|
567
|
+
# memory admission clamps (see memory_admission_enabled below); the sum of
|
|
568
|
+
# every state's `max_concurrent` is validated against it at startup. It stays
|
|
569
|
+
# top-level — not on a state — because it bounds total host memory across all
|
|
570
|
+
# VMs at once. Default: 10
|
|
128
571
|
max_concurrent_agents: 2
|
|
129
572
|
|
|
130
573
|
# max_turns (int): hard ceiling on autonomous turns per issue. Steering-reply
|
|
@@ -135,27 +578,120 @@ agent:
|
|
|
135
578
|
# after recoverable failures. Default: 300000
|
|
136
579
|
max_retry_backoff_ms: 120000
|
|
137
580
|
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
581
|
+
# memory_admission_enabled (bool): when true, before each dispatch the
|
|
582
|
+
# orchestrator reads `/proc/meminfo` (MemAvailable) and clamps the effective
|
|
583
|
+
# concurrency cap to what currently fits at `gondolin.mem_mib` per VM after
|
|
584
|
+
# subtracting `host_memory_reserve_mib`. This is a defense-in-depth backstop
|
|
585
|
+
# for hosts where the static `max_concurrent_agents` is set generously: when
|
|
586
|
+
# MemAvailable drops, new dispatches are gated so a misconfigured cap can't
|
|
587
|
+
# walk the host into OOM (issue 27). On hosts without `/proc/meminfo`
|
|
588
|
+
# (macOS, BSD) the probe degrades gracefully and the static cap is used
|
|
589
|
+
# unchanged. Default: true.
|
|
590
|
+
memory_admission_enabled: true
|
|
591
|
+
|
|
592
|
+
# host_memory_reserve_mib (int): headroom (MiB) the memory admission cap
|
|
593
|
+
# keeps for the orchestrator process itself, the per-VM Gondolin runners, and
|
|
594
|
+
# the kernel's own working set. Only consulted when
|
|
595
|
+
# `memory_admission_enabled` is true. Raise on hosts with heavy non-symphony
|
|
596
|
+
# workloads; lower on dedicated worker hosts. Default: 2048.
|
|
597
|
+
host_memory_reserve_mib: 2048
|
|
598
|
+
|
|
599
|
+
# circuit_breaker_threshold (int): after this many CONSECUTIVE dispatch
|
|
600
|
+
# attempts fail with the *same* (normalized) reason, the orchestrator stops
|
|
601
|
+
# retrying the issue and routes it to a holding state (the first declared
|
|
602
|
+
# `role: holding` state) for a human to inspect, instead of looping forever
|
|
603
|
+
# on a deterministically-failing dispatch (issue 128 — a persistent
|
|
604
|
+
# `401 invalid_api_key` once looped ~324 attempts over ~13h). The streak
|
|
605
|
+
# resets the moment an attempt fails with a different reason or exits
|
|
606
|
+
# cleanly, so transient/varied failures still retry under the normal backoff
|
|
607
|
+
# (`max_retry_backoff_ms`). The tripped issue's body gets a diagnostic note
|
|
608
|
+
# explaining the trip so the dashboard shows "stuck on identical failure"
|
|
609
|
+
# rather than a silent loop. Set to 0 to disable; must otherwise be >= 2
|
|
610
|
+
# (1 would trip on the first failure, never retrying). Default: 5.
|
|
611
|
+
circuit_breaker_threshold: 5
|
|
143
612
|
|
|
144
613
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
145
614
|
# acp — Agent Client Protocol adapter selection.
|
|
146
615
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
147
616
|
acp:
|
|
148
|
-
# adapter (string): one of symphony's known profiles.
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
617
|
+
# adapter (string): one of symphony's known profiles. Default: 'claude'.
|
|
618
|
+
# claude — claude-agent-acp. The guest holds a placeholder bearer; the host
|
|
619
|
+
# substitutes the real Anthropic OAuth token at Gondolin egress.
|
|
620
|
+
# No real credential enters the VM.
|
|
621
|
+
# codex — codex-acp (issue 116). Same model: the guest holds a placeholder
|
|
622
|
+
# bearer (in a fake ~/.codex/auth.json); the host substitutes the
|
|
623
|
+
# real OpenAI/ChatGPT token at egress. No real credential — and no
|
|
624
|
+
# real OPENAI_API_KEY — enters the VM.
|
|
625
|
+
# opencode — opencode acp, backed by GitHub Copilot (issue 130). The host
|
|
626
|
+
# exchanges the operator's `opencode auth login` GitHub OAuth token
|
|
627
|
+
# for a short-lived Copilot token host-side and substitutes it at
|
|
628
|
+
# egress — the GitHub token never enters the VM. One Copilot
|
|
629
|
+
# credential unlocks many models (GPT-4o/4.1, Claude Sonnet,
|
|
630
|
+
# Gemini, o-series, …).
|
|
152
631
|
adapter: claude
|
|
153
632
|
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
633
|
+
# Credentials never enter the VM (issue 113; codex generalized in 116). The
|
|
634
|
+
# guest holds only a token-shaped placeholder; on every outbound request the
|
|
635
|
+
# host substitutes the real upstream credential at Gondolin egress (TLS-MITM,
|
|
636
|
+
# see src/agent/credential-secrets.ts). The real refresh/durable token always
|
|
637
|
+
# stays host-side.
|
|
638
|
+
#
|
|
639
|
+
# For claude: the host reads the live access token from
|
|
640
|
+
# ~/.claude/.credentials.json (refreshing host-side via `claude -p "ok"` under
|
|
641
|
+
# flock when the cache is stale) and injects it at egress to api.anthropic.com.
|
|
642
|
+
# A minimal ~/.claude.json is staged for identity only — NO refreshToken, NO
|
|
643
|
+
# accessToken on the VM.
|
|
644
|
+
#
|
|
645
|
+
# For codex: the host reads the live credential (`tokens.access_token` from
|
|
646
|
+
# ~/.codex/auth.json, with an OPENAI_API_KEY env fallback — NEVER the refresh
|
|
647
|
+
# token) and injects it at egress to chatgpt.com / api.openai.com. A COMPLETE
|
|
648
|
+
# fake ~/.codex/auth.json is staged (JWT-shaped placeholder tokens + the
|
|
649
|
+
# non-secret account_id/auth_mode/last_refresh codex's completeness check needs),
|
|
650
|
+
# so codex-acp runs in its native mode without an in-VM OAuth handshake or
|
|
651
|
+
# refresh (both stay host-side). Every credential-bearing var is stripped from
|
|
652
|
+
# the forwarded VM boot env.
|
|
653
|
+
#
|
|
654
|
+
# For opencode: a staged opencode.json (at /root/.config/opencode/opencode.json)
|
|
655
|
+
# declares a custom @ai-sdk/openai-compatible provider whose baseURL/apiKey read
|
|
656
|
+
# the OPENCODE_PROXY_* env vars (a `gho_`-shaped placeholder bearer). The host
|
|
657
|
+
# reads the durable GitHub OAuth token from ~/.local/share/opencode/auth.json
|
|
658
|
+
# (COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN env fallback), exchanges it
|
|
659
|
+
# host-side at api.github.com/copilot_internal/v2/token for a short-lived Copilot
|
|
660
|
+
# token (cached + TTL-refreshed before expiry), injects the Copilot editor
|
|
661
|
+
# headers, and substitutes it at egress to api.githubcopilot.com. The durable
|
|
662
|
+
# GitHub token never enters the VM — so do NOT also list it in
|
|
663
|
+
# `gondolin.forward_env`. See docs/research/opencode-copilot-accept-matrix.md.
|
|
664
|
+
|
|
665
|
+
# model (string | null): optional model selector forwarded to the chosen adapter.
|
|
666
|
+
# Each adapter profile knows how to surface it natively:
|
|
667
|
+
# claude — exported as ANTHROPIC_MODEL on the adapter process. Accepts anything
|
|
668
|
+
# claude-agent-acp would (aliases like "opus", "sonnet", or full IDs
|
|
669
|
+
# like "claude-opus-4-7").
|
|
670
|
+
# codex — passed as `-c model="<value>"` argv to codex-acp (parsed as TOML).
|
|
671
|
+
# opencode— baked into the staged opencode.json as model="symphony-copilot/<value>".
|
|
672
|
+
# Use a Copilot chat-completions model id (e.g. gpt-4o, gpt-4.1,
|
|
673
|
+
# claude-sonnet-4.5, gemini-2.5-pro); codex-class models served only
|
|
674
|
+
# on Copilot's /responses path are NOT reachable. Default: gpt-4o.
|
|
675
|
+
# Leave unset / null to use the adapter's own default model. Default: null.
|
|
676
|
+
# model: claude-opus-4-7
|
|
677
|
+
|
|
678
|
+
# effort (string | null): optional reasoning-effort lever forwarded to the chosen
|
|
679
|
+
# adapter. Profile-specific surface:
|
|
680
|
+
# claude — written into a staged `settings.json` ({"effortLevel": "<value>"})
|
|
681
|
+
# copied to /root/.claude/settings.json in the VM before claude-agent-acp
|
|
682
|
+
# starts. Valid values are `low|medium|high|xhigh|max`, gated per-model
|
|
683
|
+
# by claude-agent-acp's `supportedEffortLevels` (Opus supports `xhigh` and
|
|
684
|
+
# `max`; Haiku does not). Symphony does not validate the value — the
|
|
685
|
+
# adapter rejects unsupported choices at startup, which keeps symphony
|
|
686
|
+
# from drifting from the adapter's own supported list.
|
|
687
|
+
# codex — not wired (codex-acp has no first-class effort knob on the wrapper);
|
|
688
|
+
# setting `acp.effort` for a codex-backed state is a no-op.
|
|
689
|
+
# Leave unset / null for the adapter's own default. Default: null.
|
|
690
|
+
# effort: xhigh
|
|
691
|
+
|
|
692
|
+
# NOTE: the launch shape is fixed (an in-VM agent dials back over the bridge
|
|
693
|
+
# and spawns the chosen adapter). Customizing what the agent spawns requires
|
|
694
|
+
# forking that agent and rebuilding the VM image with the fork in place.
|
|
159
695
|
|
|
160
696
|
# shell (string): shell used to run the ACP launch command. Default: 'bash'.
|
|
161
697
|
shell: bash
|
|
@@ -173,18 +709,69 @@ acp:
|
|
|
173
709
|
# the turn is killed and retried. Default: 300000
|
|
174
710
|
stall_timeout_ms: 300000
|
|
175
711
|
|
|
712
|
+
# bridge — host-side TCP listener the in-VM agent dials back to for ACP traffic.
|
|
713
|
+
#
|
|
714
|
+
# This replaced the earlier in-VM-exec stdio path. Symphony writes ACP JSON-RPC frames
|
|
715
|
+
# onto an authenticated TCP socket; the in-VM agent (`/opt/symphony/vm-agent.mjs`)
|
|
716
|
+
# spawns the adapter via `child_process.spawn` with kernel pipes and bridges the
|
|
717
|
+
# socket to the adapter's stdio. This decouples symphony from any particular
|
|
718
|
+
# sandbox's stdio quirks — any sandbox that can launch a process with env vars and
|
|
719
|
+
# reach the host loopback works unchanged.
|
|
720
|
+
bridge:
|
|
721
|
+
# bind_host (string): host symphony binds the listener on. 0.0.0.0 allows any
|
|
722
|
+
# in-VM interface to reach the host loopback (Gondolin maps a synthetic guest host to
|
|
723
|
+
# host loopback transparently). Default: 0.0.0.0
|
|
724
|
+
bind_host: 0.0.0.0
|
|
725
|
+
|
|
726
|
+
# bind_port (int): port symphony binds the listener on. 0 picks an ephemeral
|
|
727
|
+
# port (used port surfaces via the in-VM SYMPHONY_ACP_URL env var). Default: 8788
|
|
728
|
+
bind_port: 8788
|
|
729
|
+
|
|
730
|
+
# reach_host (string): host the in-VM agent dials back to. Under Gondolin this is
|
|
731
|
+
# 127.0.0.1 because the guest loopback hits the host loopback. Other sandboxes
|
|
732
|
+
# may need a different alias. Default: 127.0.0.1
|
|
733
|
+
reach_host: 127.0.0.1
|
|
734
|
+
|
|
735
|
+
# reach_url (string|null): full URL override for the in-VM agent's dial
|
|
736
|
+
# destination, e.g. through a reverse proxy or different scheme. When null,
|
|
737
|
+
# symphony constructs `tcp://<reach_host>:<bind_port>`. Default: null
|
|
738
|
+
# reach_url: null
|
|
739
|
+
|
|
740
|
+
# connect_timeout_ms (int): how long to wait for the in-VM agent to connect after
|
|
741
|
+
# the sandbox is launched, before failing the attempt. Default: 30000
|
|
742
|
+
connect_timeout_ms: 30000
|
|
743
|
+
|
|
176
744
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
-
#
|
|
745
|
+
# credentials — host credential lifecycle (issue 113). The host substitutes the
|
|
746
|
+
# real OAuth access token into each VM's outbound request at Gondolin egress; the
|
|
747
|
+
# ticker keeps the host's cached access token warm by periodically running
|
|
748
|
+
# `claude -p "ok"` — Claude Code's own OAuth path detects the stale token,
|
|
749
|
+
# refreshes against Anthropic, and atomically writes the rotated tuple back to
|
|
750
|
+
# `~/.claude/.credentials.json`. Symphony never implements OAuth; Anthropic's own
|
|
751
|
+
# client does.
|
|
178
752
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
|
|
753
|
+
credentials:
|
|
754
|
+
# ticker_interval_ms (int): how often the host ticker spawns `claude -p "ok"`
|
|
755
|
+
# to refresh the OAuth cache. Each live VM also refreshes proactively before its
|
|
756
|
+
# cached token expires, so the ticker is belt-to-the-braces for idle periods.
|
|
757
|
+
# Set to 0 to disable the in-symphony ticker entirely (operator runs their own
|
|
758
|
+
# systemd timer instead). Default: 21600000 (6 hours).
|
|
759
|
+
ticker_interval_ms: 21600000
|
|
184
760
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
761
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
762
|
+
# gondolin — microVM execution environment (Gondolin substrate).
|
|
763
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
764
|
+
gondolin:
|
|
765
|
+
# image (string | null): the agent rootfs the VM boots, expressed as a Gondolin
|
|
766
|
+
# image selector. Build it ONCE with `npm run build:image` (see images/agents/) —
|
|
767
|
+
# the build prints a content-addressed build id (a digest); pin that id here for
|
|
768
|
+
# an immutable, reproducible reference. A `name:tag` ref (e.g.
|
|
769
|
+
# `symphony-agents:latest`) or a path to an exported asset directory also work.
|
|
770
|
+
# The image bakes a Node.js runtime, every ACP-capable coding agent
|
|
771
|
+
# (claude-agent-acp, codex-acp, opencode), and the in-VM launcher at
|
|
772
|
+
# /opt/symphony/vm-agent.mjs — so dispatch needs no runtime mounts. REQUIRED:
|
|
773
|
+
# the runner fails fast at boot when this is unset. Default: null.
|
|
774
|
+
image: symphony-agents:latest
|
|
188
775
|
|
|
189
776
|
# cpus (int): vCPU count per VM. Default: 2.
|
|
190
777
|
cpus: 2
|
|
@@ -192,19 +779,12 @@ smolvm:
|
|
|
192
779
|
# mem_mib (int): RAM per VM in MiB. Default: 2048.
|
|
193
780
|
mem_mib: 4096
|
|
194
781
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
#
|
|
201
|
-
# binary, mounted read-only at /opt/codex. Default: null.
|
|
202
|
-
bin_path: null
|
|
203
|
-
|
|
204
|
-
# volumes (list): additional host:guest bind mounts. smolvm has a small
|
|
205
|
-
# per-VM mount cap (the workspace itself already consumes one slot), so keep
|
|
206
|
-
# this list small. Each entry: { host: path, guest: path, readonly?: bool }.
|
|
207
|
-
# Default: [].
|
|
782
|
+
# volumes (list): additional host:guest VFS mounts beyond the auto-mounted
|
|
783
|
+
# workspace. Gondolin's VFS is programmable (no hard per-VM mount cap), but keep
|
|
784
|
+
# this lean — if ANY state sets `eval_mode: true` it adds two read-only mounts
|
|
785
|
+
# (/symphony/issues + /symphony/logs) on top of the workspace. Prefer baking
|
|
786
|
+
# static tooling into the image over a runtime mount. Each entry:
|
|
787
|
+
# { host: path, guest: path, readonly?: bool }. Default: [].
|
|
208
788
|
volumes:
|
|
209
789
|
- host: ~/.cache/npm
|
|
210
790
|
guest: /root/.npm
|
|
@@ -212,13 +792,42 @@ smolvm:
|
|
|
212
792
|
|
|
213
793
|
# forward_env (string[]): host env vars forwarded into the VM exec.
|
|
214
794
|
# Default: [OPENAI_API_KEY, ANTHROPIC_API_KEY]
|
|
795
|
+
# NOTE: the runner strips EVERY credential-bearing var from the forwarded boot
|
|
796
|
+
# env before launch — the guest holds only a token-shaped placeholder that
|
|
797
|
+
# Gondolin substitutes with the real token at egress — so listing a credential
|
|
798
|
+
# var here does NOT plant the real key in the VM's PID-1 env.
|
|
215
799
|
forward_env:
|
|
216
800
|
- OPENAI_API_KEY
|
|
217
801
|
- ANTHROPIC_API_KEY
|
|
218
802
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
803
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
804
|
+
# egress — general dev-tooling firewall for the in-VM agent.
|
|
805
|
+
#
|
|
806
|
+
# Gondolin denies guest egress to non-allowlisted hosts by default. The agent can
|
|
807
|
+
# always reach its own inference host (that is handled by the credential layer,
|
|
808
|
+
# which substitutes the real upstream token at egress). This block additionally
|
|
809
|
+
# opens the dev-tooling hosts the agent needs so gates can run inside the VM —
|
|
810
|
+
# `npm install`, git-based dependencies, release-binary downloads.
|
|
811
|
+
#
|
|
812
|
+
# SECURITY: this is the firewall ONLY. No credential is ever substituted for a host
|
|
813
|
+
# listed here — the real token substitutes solely on each adapter's inference host
|
|
814
|
+
# (see src/agent/credential-secrets.ts `substitutionHosts`). The effective
|
|
815
|
+
# per-adapter allowlist handed to Gondolin is THIS list UNION that adapter's
|
|
816
|
+
# substitution host(s). Listing a host therefore grants plain network egress, never
|
|
817
|
+
# a token. Keep the list tight — every entry widens the network surface of
|
|
818
|
+
# semi-trusted in-VM code.
|
|
819
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
820
|
+
egress:
|
|
821
|
+
# allowed_hosts (string[]): hostnames the in-VM agent may reach for dev tooling.
|
|
822
|
+
# Default: [] (no extra hosts — the agent can reach only its inference host).
|
|
823
|
+
# Bare hostnames ONLY — no scheme, port, or path (`github.com`, not
|
|
824
|
+
# `https://github.com/...`). A malformed entry fails safe (the host simply stays
|
|
825
|
+
# blocked, never opened). Each entry is matched against the request host exactly.
|
|
826
|
+
allowed_hosts:
|
|
827
|
+
- registry.npmjs.org # npm install
|
|
828
|
+
- github.com # git-based deps / release pages
|
|
829
|
+
- codeload.github.com # GitHub tarball fetch
|
|
830
|
+
- objects.githubusercontent.com # release-binary downloads
|
|
222
831
|
|
|
223
832
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
224
833
|
# server — HTTP dashboard + MCP endpoint listener.
|
|
@@ -236,11 +845,24 @@ server:
|
|
|
236
845
|
# mcp — Model Context Protocol server exposed to in-VM agents.
|
|
237
846
|
#
|
|
238
847
|
# The orchestrator runs a JSON-RPC endpoint scoped to each active issue at
|
|
239
|
-
# /api/v1/issues/<id>/mcp, gated by a per-dispatch bearer token.
|
|
240
|
-
# there:
|
|
848
|
+
# /api/v1/issues/<id>/mcp, gated by a per-dispatch bearer token. Three tools
|
|
849
|
+
# live there:
|
|
241
850
|
#
|
|
242
|
-
# • symphony.
|
|
851
|
+
# • symphony.transition({ to_state, notes? })
|
|
852
|
+
# — canonical (and only) exit verb. Moves the issue into another declared
|
|
853
|
+
# state, optionally appending `notes` (markdown) to the issue body before
|
|
854
|
+
# the move so the next agent (in `to_state`) reads them as part of
|
|
855
|
+
# `issue.description`. Terminal targets clean the workspace; active and
|
|
856
|
+
# holding targets preserve it so the same `agent/<id>` git branch
|
|
857
|
+
# survives the handoff. Rejected transitions return MCP tool-result
|
|
858
|
+
# errors (isError:true) the agent can read and retry.
|
|
243
859
|
# • symphony.request_human_steering({ question, context? })
|
|
860
|
+
# • symphony.propose_issue({ title, description?, labels?, priority? })
|
|
861
|
+
# — drops a new issue into the first declared `role: holding` state
|
|
862
|
+
# directory (literal Triage if none declared). The orchestrator does NOT
|
|
863
|
+
# dispatch it; the operator approves or discards from the dashboard. The
|
|
864
|
+
# calling issue is recorded as proposed_by in the new file's
|
|
865
|
+
# front-matter.
|
|
244
866
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
245
867
|
mcp:
|
|
246
868
|
# enabled (bool): when false, the orchestrator refuses to dispatch (MCP is
|
|
@@ -248,8 +870,8 @@ mcp:
|
|
|
248
870
|
enabled: true
|
|
249
871
|
|
|
250
872
|
# host (string): hostname or IP the agent uses to reach the orchestrator
|
|
251
|
-
# from inside the
|
|
252
|
-
# actually-bound HTTP server. Default: '127.0.0.1' (
|
|
873
|
+
# from inside the VM. The port is resolved at runtime from the
|
|
874
|
+
# actually-bound HTTP server. Default: '127.0.0.1' (Gondolin maps VM
|
|
253
875
|
# loopback to host loopback; verified empirically).
|
|
254
876
|
host: 127.0.0.1
|
|
255
877
|
|
|
@@ -264,17 +886,27 @@ Liquid-templated prompt body. Rendered once per dispatched issue. Context:
|
|
|
264
886
|
|
|
265
887
|
issue.identifier — the issue's external id (e.g. "DEMO-42").
|
|
266
888
|
issue.title — issue title (string).
|
|
267
|
-
issue.state — current state (string, matches
|
|
268
|
-
issue.description — body text (string or empty).
|
|
889
|
+
issue.state — current state (string, matches a key in `states:`).
|
|
890
|
+
issue.description — body text (string or empty). `symphony.transition`
|
|
891
|
+
appends its `notes` block here before the file moves,
|
|
892
|
+
so the next state's agent reads the previous state's
|
|
893
|
+
handoff message verbatim.
|
|
269
894
|
issue.priority — number or null.
|
|
270
895
|
issue.labels — list of strings (lowercased).
|
|
271
896
|
attempt — int, 1-based attempt counter; absent on first attempt.
|
|
272
897
|
|
|
273
898
|
Available Liquid filters: standard Shopify Liquid plus `escape_once`.
|
|
274
899
|
|
|
900
|
+
Per-state prompt branching (V1 pattern): when `states:` declares more than
|
|
901
|
+
one active role (e.g. Todo + Review), wrap the state-specific instructions in
|
|
902
|
+
a `{% case issue.state %}` / `{% when "..." %}` / `{% else %}` block. The
|
|
903
|
+
runner renders the prompt fresh on every dispatch, so each state's agent sees
|
|
904
|
+
only its own instructions plus whatever common preamble / postamble lives
|
|
905
|
+
outside the case. See WORKFLOW.md in this repo for a worked example.
|
|
906
|
+
|
|
275
907
|
The body below is the literal prompt sent to the agent. Keep it specific to
|
|
276
|
-
this workflow; orchestrator behavior (
|
|
277
|
-
the same no matter what you write here.
|
|
908
|
+
this workflow; orchestrator behavior (transition, request_human_steering,
|
|
909
|
+
propose_issue) is the same no matter what you write here.
|
|
278
910
|
-->
|
|
279
911
|
|
|
280
912
|
You are picking up a single issue and shepherding it through the workflow.
|
|
@@ -294,12 +926,20 @@ Goals:
|
|
|
294
926
|
|
|
295
927
|
1. Work in the current directory only; treat it as the issue workspace.
|
|
296
928
|
2. Make the smallest correct change that satisfies the issue.
|
|
297
|
-
3.
|
|
298
|
-
|
|
299
|
-
to the
|
|
929
|
+
3. Hand off when done. `symphony.transition({ to_state, notes? })` is the
|
|
930
|
+
canonical (and only) exit verb: pass a declared state name and optional
|
|
931
|
+
markdown notes that get appended to the issue body for the next agent.
|
|
932
|
+
For single-agent workflows, transition straight into the first declared
|
|
933
|
+
`role: terminal` state to end the run.
|
|
300
934
|
4. If you cannot proceed without human input, call
|
|
301
935
|
`symphony.request_human_steering({ question, context? })`. Your turn ends
|
|
302
936
|
immediately; the human's reply arrives as your next prompt.
|
|
937
|
+
5. If you notice work out of scope for this issue — unrelated bugs, follow-ups
|
|
938
|
+
a human should size, refactors worth a separate dispatch — call
|
|
939
|
+
`symphony.propose_issue({ title, description?, labels?, priority? })`. It
|
|
940
|
+
lands in the first declared `role: holding` state directory (defaults to
|
|
941
|
+
`Triage/`); the operator approves or discards. Do not graft unrelated
|
|
942
|
+
edits onto this branch.
|
|
303
943
|
|
|
304
944
|
{% if attempt -%}
|
|
305
945
|
This is continuation/retry attempt {{ attempt }}. Inspect the workspace before
|