smol-symphony 0.1.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/WORKFLOW.md ADDED
@@ -0,0 +1,269 @@
1
+ ---
2
+ # WORKFLOW.md — symphony dispatched against smol-symphony itself.
3
+ #
4
+ # Run with:
5
+ #
6
+ # npx symphony WORKFLOW.md
7
+ #
8
+ # Defaults assume a fully local setup: the per-issue workspace clones from this
9
+ # repo's `.git` directory, the agent has no network credentials, and on
10
+ # mark_done the host writes a `git format-patch` bundle to
11
+ # `.symphony/patches/<branch>.patch` for human review.
12
+ #
13
+ # To opt into the remote PR flow, export before launching:
14
+ #
15
+ # SYMPHONY_REPO=owner/smol-symphony \
16
+ # SYMPHONY_BASE_BRANCH=main \
17
+ # npx symphony WORKFLOW.md
18
+ #
19
+ # `gh` on the host must be authenticated (`gh auth status` clean). The token
20
+ # never enters the VM.
21
+ #
22
+ # Every section and option is documented in WORKFLOW.template.md.
23
+
24
+ tracker:
25
+ kind: local
26
+ root: ./issues
27
+ active_states:
28
+ - Todo
29
+ - In Progress
30
+ terminal_states:
31
+ - Done
32
+ - Cancelled
33
+
34
+ polling:
35
+ interval_ms: 5000
36
+
37
+ workspace:
38
+ root: ./.symphony/workspaces
39
+
40
+ hooks:
41
+ timeout_ms: 120000
42
+
43
+ # Clone smol-symphony into the fresh per-issue workspace from a strictly local
44
+ # source (no creds needed). The agent receives a working git repo with full
45
+ # history on the configured base branch, plus a per-issue branch checked out.
46
+ # All network remotes are stripped so any `git push`/`git fetch` from inside
47
+ # the VM fails closed.
48
+ after_create: |
49
+ set -eu
50
+ SOURCE_REPO="${SYMPHONY_SOURCE_REPO:-${PWD}/../../..}"
51
+ BASE="${SYMPHONY_BASE_BRANCH:-main}"
52
+ ISSUE_ID="$(basename "$PWD")"
53
+ BRANCH="agent/${ISSUE_ID}"
54
+
55
+ if [ ! -d "${SOURCE_REPO}/.git" ]; then
56
+ echo "after_create: SOURCE_REPO=${SOURCE_REPO} is not a git repo" >&2
57
+ exit 1
58
+ fi
59
+
60
+ # `git clone --local` hardlinks .git/objects when possible; fast and disk-cheap.
61
+ # `--no-tags` keeps the local refspec minimal; `--branch` lands on the right base.
62
+ git clone --local --no-tags --branch "${BASE}" "${SOURCE_REPO}" .
63
+
64
+ # Strip all remotes. The agent will see no network targets at all.
65
+ for remote in $(git remote); do
66
+ git remote remove "${remote}"
67
+ done
68
+ git config --local --unset credential.helper 2>/dev/null || true
69
+
70
+ # If SYMPHONY_REPO is set, restore an `origin` pointing at the GitHub remote
71
+ # so the after_run hook can push. The URL is the canonical HTTPS form (no
72
+ # token); auth comes from the host's `gh`, which never enters the VM.
73
+ if [ -n "${SYMPHONY_REPO:-}" ]; then
74
+ git remote add origin "https://github.com/${SYMPHONY_REPO}.git"
75
+ gh auth setup-git 2>/dev/null || true
76
+ git fetch --no-tags origin "${BASE}:refs/remotes/origin/${BASE}" || true
77
+ fi
78
+
79
+ git config --local user.name "symphony-agent"
80
+ git config --local user.email "agent@symphony.local"
81
+
82
+ git checkout -b "${BRANCH}"
83
+
84
+ echo "workspace ready: base=${BASE} branch=${BRANCH} source=${SOURCE_REPO}"
85
+
86
+ # Runs after every attempt. Gated on the issue file being in Done/ (i.e. the
87
+ # agent has called symphony.mark_done). Two outputs:
88
+ # - If SYMPHONY_REPO is set: push branch + open (or update) a PR via gh.
89
+ # - Else (local-only mode): write the agent's work as a git format-patch
90
+ # bundle into ./.symphony/patches/<branch>.patch for human review.
91
+ after_run: |
92
+ set -eu
93
+ BASE="${SYMPHONY_BASE_BRANCH:-main}"
94
+ ISSUE_ID="$(basename "$PWD")"
95
+ BRANCH="agent/${ISSUE_ID}"
96
+ TRACKER_ROOT="${SYMPHONY_TRACKER_ROOT:-$PWD/../../../issues}"
97
+ PATCHES_DIR="${SYMPHONY_PATCHES_DIR:-$PWD/../../../.symphony/patches}"
98
+
99
+ if [ ! -f "${TRACKER_ROOT}/Done/${ISSUE_ID}.md" ]; then
100
+ echo "issue ${ISSUE_ID} not in Done/ yet; skipping handoff"
101
+ exit 0
102
+ fi
103
+ if ! git rev-parse --verify "${BRANCH}" >/dev/null 2>&1; then
104
+ echo "no branch ${BRANCH}; nothing to hand off"
105
+ exit 0
106
+ fi
107
+
108
+ # Resolve the ref the agent diverged from. In remote mode origin/${BASE}
109
+ # exists (after_create's `git fetch`). In local-only mode we kept the local
110
+ # ${BASE} branch alive (`git clone --branch ${BASE}` creates it at the
111
+ # original tip; `git checkout -b ${BRANCH}` does not remove it). That tip
112
+ # is exactly where the agent diverged from.
113
+ if git rev-parse --verify "origin/${BASE}" >/dev/null 2>&1; then
114
+ MERGE_BASE="origin/${BASE}"
115
+ elif git rev-parse --verify "${BASE}" >/dev/null 2>&1; then
116
+ MERGE_BASE="${BASE}"
117
+ else
118
+ echo "could not resolve merge base (no origin/${BASE} or local ${BASE})" >&2
119
+ exit 1
120
+ fi
121
+
122
+ if [ -z "$(git log --oneline "${MERGE_BASE}..${BRANCH}" 2>/dev/null)" ]; then
123
+ echo "no new commits on ${BRANCH}; nothing to hand off"
124
+ exit 0
125
+ fi
126
+
127
+ # The mark_done MCP tool persists its title + summary into a structured
128
+ # markdown file at <staging>/mark_done.md. The staging dir is inside .git/
129
+ # when the workspace has its own clone; .symphony-runtime/ otherwise.
130
+ MARKDONE=""
131
+ if [ -f .git/symphony-runtime/mark_done.md ]; then
132
+ MARKDONE=.git/symphony-runtime/mark_done.md
133
+ elif [ -f .symphony-runtime/mark_done.md ]; then
134
+ MARKDONE=.symphony-runtime/mark_done.md
135
+ fi
136
+ if [ -n "${MARKDONE}" ]; then
137
+ TITLE="$(sed -n '1 s/^# //p' "${MARKDONE}")"
138
+ BODY="$(tail -n +3 "${MARKDONE}")"
139
+ else
140
+ TITLE="${ISSUE_ID}"
141
+ BODY="Symphony run for ${ISSUE_ID}."
142
+ fi
143
+
144
+ if [ -n "${SYMPHONY_REPO:-}" ]; then
145
+ # Remote PR mode.
146
+ git push -u origin "${BRANCH}"
147
+ if gh pr view "${BRANCH}" >/dev/null 2>&1; then
148
+ echo "PR already exists for ${BRANCH}; pushed updates"
149
+ else
150
+ gh pr create \
151
+ --base "${BASE}" \
152
+ --head "${BRANCH}" \
153
+ --title "${ISSUE_ID}: ${TITLE}" \
154
+ --body "${BODY}"
155
+ fi
156
+ else
157
+ # Local-only mode: bundle the diff for human review.
158
+ mkdir -p "${PATCHES_DIR}"
159
+ OUT="${PATCHES_DIR}/$(echo "${BRANCH}" | tr '/' '_').patch"
160
+ git format-patch --stdout "${MERGE_BASE}..${BRANCH}" > "${OUT}"
161
+ echo "wrote patch bundle: ${OUT}"
162
+ echo " apply with: git -C <target-repo> am ${OUT}"
163
+ fi
164
+
165
+ agent:
166
+ max_concurrent_agents: 1
167
+ max_turns: 6
168
+ max_retry_backoff_ms: 120000
169
+
170
+ acp:
171
+ # Selecting "claude" is enough: symphony reads ~/.claude/.credentials.json on
172
+ # the host, stages a copy into the workspace's runtime dir, and auto-generates
173
+ # a launch command that places the file at the adapter's expected path inside
174
+ # the VM before exec'ing claude-agent-acp. Set `command` only to override.
175
+ adapter: claude
176
+ shell: bash
177
+ prompt_timeout_ms: 1800000
178
+ read_timeout_ms: 30000
179
+ stall_timeout_ms: 300000
180
+
181
+ smolvm:
182
+ from: ./.vm/symphony.smolmachine.smolmachine
183
+ cpus: 2
184
+ mem_mib: 4096
185
+ net: true
186
+ # No volume mounts. Workspace is auto-mounted by the runner. Credentials are
187
+ # staged into the workspace by symphony and copied into ~/.claude by the
188
+ # auto-derived acp.command. The tracker is reached only through the symphony
189
+ # MCP server.
190
+ volumes: []
191
+ forward_env:
192
+ - OPENAI_API_KEY
193
+ - ANTHROPIC_API_KEY
194
+
195
+ server:
196
+ port: 8787
197
+ # Bound to all interfaces because access is gated by tailscale, not by the
198
+ # HTTP server itself. The endpoint has no auth; only expose it inside a
199
+ # trusted network boundary.
200
+ host: 0.0.0.0
201
+
202
+ mcp:
203
+ # The VM's loopback transparently reaches the host's loopback in smolvm, so
204
+ # 127.0.0.1 from inside the VM hits the host's listener. Override only if
205
+ # your VMM has a different host alias.
206
+ host: 127.0.0.1
207
+ ---
208
+ You are working on **smol-symphony**, a TypeScript orchestrator that dispatches
209
+ coding agents into per-issue smolvm microVMs and talks to them over the Agent
210
+ Client Protocol (ACP). Your workspace is a fresh clone of this repo.
211
+
212
+ Issue: **{{ issue.identifier }} — {{ issue.title }}**
213
+ State: {{ issue.state }}
214
+ {% if issue.priority -%}Priority: {{ issue.priority }}{%- endif %}
215
+ {% if issue.labels.size > 0 -%}Labels: {% for l in issue.labels %}{{ l }}{% unless forloop.last %}, {% endunless %}{% endfor %}{%- endif %}
216
+
217
+ {% if issue.description -%}
218
+ Description:
219
+
220
+ {{ issue.description }}
221
+ {%- endif %}
222
+
223
+ Orientation:
224
+
225
+ - This is the smol-symphony codebase. Start by reading `README.md` and
226
+ `PRODUCT.md` if you haven't seen them. `SPEC.md` is the long-form design
227
+ spec. `CLAUDE.md` (if present) has any standing instructions for this repo.
228
+ - Source lives under `src/`. Tests live under `tests/`. Run `npm test` and
229
+ `npm run typecheck` before declaring work done.
230
+ - You are on a per-issue branch (`agent/{{ issue.identifier }}`) checked out
231
+ from the configured base branch. Commit your work locally. You do **not**
232
+ have network credentials; pushing is the host's job, after you mark done.
233
+
234
+ Workflow:
235
+
236
+ 1. Read enough of the codebase to understand the change you need to make.
237
+ 2. Make the smallest correct change. Add or update tests where the change is
238
+ testable. Run `npm run typecheck` and `npm test`; both must pass.
239
+ 3. Commit your work to the per-issue branch with a short message.
240
+ 4. Call `symphony.mark_done({ title, summary })`:
241
+ - `title`: a single line in imperative voice, ≤72 chars. Becomes the
242
+ PR/commit title.
243
+ - `summary`: a one- to three-paragraph narrative of what you did and why,
244
+ plus any follow-ups you noticed but didn't do. Becomes the PR body.
245
+ This is the only way to signal completion; nothing else will move the issue
246
+ out of an active state.
247
+
248
+ If you genuinely cannot proceed — ambiguous requirements, missing context that
249
+ only a human can supply, a design decision that needs human input — call
250
+ `symphony.request_human_steering({ question, context })` instead of guessing.
251
+ Your turn will end immediately and the human's response will arrive as your
252
+ next prompt.
253
+
254
+ Steering tips:
255
+
256
+ - The operator's dashboard already shows the original issue title and body
257
+ alongside your question. Do **not** restate or paraphrase the issue body in
258
+ `question`; ask the specific thing you need answered.
259
+ - Use `context` for facts the operator wouldn't otherwise see: what you've
260
+ inspected, what you've already tried, what constraint is forcing the
261
+ question.
262
+
263
+ {% if attempt -%}
264
+ This is continuation/retry attempt {{ attempt }}. Inspect the workspace before
265
+ making new edits; your previous run may have left commits on the branch. Check
266
+ `git log agent/{{ issue.identifier }}` to see what's there. If the work is
267
+ already complete, call `symphony.mark_done` with an appropriate title and
268
+ summary and stop.
269
+ {%- endif %}
@@ -0,0 +1,307 @@
1
+ <!--
2
+ WORKFLOW.template.md — annotated reference for symphony workflow files.
3
+
4
+ A workflow file is a YAML front-matter block plus a Liquid-templated prompt
5
+ body. The orchestrator parses the front matter into ServiceConfig (defined in
6
+ src/types.ts) and renders the prompt body once per dispatched issue, with the
7
+ Liquid context `{ issue, attempt }`.
8
+
9
+ This document lists every supported section, every option within it, the
10
+ parser default, and a small example. For a complete worked example, see
11
+ WORKFLOW.md in this repo.
12
+
13
+ Notation:
14
+ • Required keys are marked (required).
15
+ • Types: `string`, `int`, `bool`, `path` (string resolved relative to the
16
+ workflow file's directory unless absolute), `string[]`, `map<K, V>`.
17
+ • Defaults are what the parser writes when the key is absent.
18
+ -->
19
+
20
+ ---
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ # tracker — where issues come from.
23
+ # ─────────────────────────────────────────────────────────────────────────────
24
+ tracker:
25
+ # kind (required): 'local' or 'linear'.
26
+ # local — markdown files under `root`, one per issue, organized by state.
27
+ # linear — Linear API (workspace + project_slug).
28
+ kind: local
29
+
30
+ # root (path): local-tracker only. Directory containing `<state>/<id>.md`
31
+ # files. Required when kind=local. Resolved relative to the workflow file
32
+ # if not absolute.
33
+ root: ./issues
34
+
35
+ # endpoint (string): linear-tracker only. API endpoint.
36
+ # Default: Linear's public GraphQL endpoint.
37
+ endpoint: https://api.linear.app/graphql
38
+
39
+ # api_key (string): linear-tracker only. Personal API key. Required for
40
+ # kind=linear. Pulled from $LINEAR_API_KEY if you use `$LINEAR_API_KEY`.
41
+ api_key: $LINEAR_API_KEY
42
+
43
+ # project_slug (string): linear-tracker only. Required for kind=linear.
44
+ project_slug: my-team/my-project
45
+
46
+ # active_states (string[]): states the orchestrator dispatches against.
47
+ # Default: [Todo, In Progress]
48
+ active_states:
49
+ - Todo
50
+ - In Progress
51
+
52
+ # terminal_states (string[]): states the orchestrator treats as complete;
53
+ # mark_done moves issues to the first listed state.
54
+ # Default: [Done]
55
+ terminal_states:
56
+ - Done
57
+ - Cancelled
58
+
59
+ # ─────────────────────────────────────────────────────────────────────────────
60
+ # polling — how often to poll the tracker.
61
+ # ─────────────────────────────────────────────────────────────────────────────
62
+ polling:
63
+ # interval_ms (int): tick interval, milliseconds.
64
+ # Default: 30000
65
+ interval_ms: 5000
66
+
67
+ # ─────────────────────────────────────────────────────────────────────────────
68
+ # workspace — per-issue working directory.
69
+ # ─────────────────────────────────────────────────────────────────────────────
70
+ workspace:
71
+ # root (path): parent directory holding `<issue-id>/` working trees.
72
+ # Default: $TMPDIR/symphony_workspaces
73
+ root: ./.symphony/workspaces
74
+
75
+ # ─────────────────────────────────────────────────────────────────────────────
76
+ # hooks — shell scripts the orchestrator runs at workspace lifecycle points.
77
+ #
78
+ # All hooks run on the HOST (not inside the VM), with cwd set to the per-issue
79
+ # workspace path. Each hook is a multi-line shell snippet. Available env vars:
80
+ #
81
+ # PWD — the workspace directory (cwd at hook start).
82
+ # SYMPHONY_ISSUE_ID — the issue identifier.
83
+ # SYMPHONY_ISSUE_STATE — the issue's current state.
84
+ # SYMPHONY_ATTEMPT — 1-based attempt counter.
85
+ # SYMPHONY_WORKFLOW — absolute path to the workflow file.
86
+ #
87
+ # Plus any env var the operator exports before launching `symphony`. The
88
+ # common pattern is to plumb tracker root / repo / base via env so the same
89
+ # workflow file works against multiple repos. See WORKFLOW.md for an example.
90
+ # ─────────────────────────────────────────────────────────────────────────────
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
+
122
+ # ─────────────────────────────────────────────────────────────────────────────
123
+ # agent — concurrency and turn budget.
124
+ # ─────────────────────────────────────────────────────────────────────────────
125
+ agent:
126
+ # max_concurrent_agents (int): cap on simultaneously-running agents across
127
+ # the whole workflow. Default: 10
128
+ max_concurrent_agents: 2
129
+
130
+ # max_turns (int): hard ceiling on autonomous turns per issue. Steering-reply
131
+ # turns are free; only autonomous turns count. Default: 20
132
+ max_turns: 6
133
+
134
+ # max_retry_backoff_ms (int): exponential backoff cap for retried dispatches
135
+ # after recoverable failures. Default: 300000
136
+ max_retry_backoff_ms: 120000
137
+
138
+ # max_concurrent_agents_by_state (map<string, int>): optional per-state
139
+ # concurrency cap. Sums must not exceed max_concurrent_agents. Default: {}.
140
+ max_concurrent_agents_by_state:
141
+ Todo: 1
142
+ In Progress: 1
143
+
144
+ # ─────────────────────────────────────────────────────────────────────────────
145
+ # acp — Agent Client Protocol adapter selection.
146
+ # ─────────────────────────────────────────────────────────────────────────────
147
+ acp:
148
+ # adapter (string): one of symphony's known profiles. The profile encodes the
149
+ # binary to launch and the host credential file to stage. Default: 'claude'.
150
+ # claude — claude-agent-acp; stages ~/.claude/.credentials.json
151
+ # codex — codex-acp; stages ~/.codex/auth.json
152
+ adapter: claude
153
+
154
+ # command (string | null): optional shell override. When set, replaces the
155
+ # auto-generated launch command and OPTS OUT of credential staging — you
156
+ # become responsible for placing whatever credentials the adapter needs.
157
+ # Leave null for the supported zero-config flow. Default: null.
158
+ command: null
159
+
160
+ # shell (string): shell used to run the ACP launch command. Default: 'bash'.
161
+ shell: bash
162
+
163
+ # prompt_timeout_ms (int): max wall time for a single ACP `session/prompt`
164
+ # call (one symphony turn). Default: 3600000 (1 hour).
165
+ prompt_timeout_ms: 1800000
166
+
167
+ # read_timeout_ms (int): max time between bytes on the ACP stdio. Bumped from
168
+ # a small default because VM cold-boot + adapter startup can take ~10s on
169
+ # first use. Default: 30000
170
+ read_timeout_ms: 30000
171
+
172
+ # stall_timeout_ms (int): max time the adapter can be idle (no events) before
173
+ # the turn is killed and retried. Default: 300000
174
+ stall_timeout_ms: 300000
175
+
176
+ # ─────────────────────────────────────────────────────────────────────────────
177
+ # smolvm — microVM execution environment.
178
+ # ─────────────────────────────────────────────────────────────────────────────
179
+ smolvm:
180
+ # from (path | null): path to a packed .smolmachine.smolmachine artifact.
181
+ # Built once with `scripts/build-vm.sh`. Mutually exclusive with `image`.
182
+ # Default: null.
183
+ from: ./.vm/symphony.smolmachine.smolmachine
184
+
185
+ # image (string | null): container image to pull instead of a packed artifact.
186
+ # Mutually exclusive with `from`. Default: null.
187
+ image: null
188
+
189
+ # cpus (int): vCPU count per VM. Default: 2.
190
+ cpus: 2
191
+
192
+ # mem_mib (int): RAM per VM in MiB. Default: 2048.
193
+ mem_mib: 4096
194
+
195
+ # net (bool): whether the VM has outbound networking. Default: true.
196
+ # Setting false isolates the VM at the cost of breaking adapters that fetch
197
+ # tokens, models, or dependencies at run time.
198
+ net: true
199
+
200
+ # bin_path (path | null): legacy mount; host directory containing the codex
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: [].
208
+ volumes:
209
+ - host: ~/.cache/npm
210
+ guest: /root/.npm
211
+ readonly: false
212
+
213
+ # forward_env (string[]): host env vars forwarded into the VM exec.
214
+ # Default: [OPENAI_API_KEY, ANTHROPIC_API_KEY]
215
+ forward_env:
216
+ - OPENAI_API_KEY
217
+ - ANTHROPIC_API_KEY
218
+
219
+ # endpoint (string): smolvm server. unix:// or http:// URI.
220
+ # Default: unix://$XDG_RUNTIME_DIR/smolvm.sock (or /run/user/1000/smolvm.sock)
221
+ endpoint: unix:///run/user/1000/smolvm.sock
222
+
223
+ # ─────────────────────────────────────────────────────────────────────────────
224
+ # server — HTTP dashboard + MCP endpoint listener.
225
+ # ─────────────────────────────────────────────────────────────────────────────
226
+ server:
227
+ # port (int | null): when null, no HTTP server is started. `--port <n>` on
228
+ # the CLI overrides this. Default: null.
229
+ port: 8787
230
+
231
+ # host (string): bind address. Default: '127.0.0.1'. Bind to '0.0.0.0' only
232
+ # inside a trusted network boundary; the dashboard has no built-in auth.
233
+ host: 0.0.0.0
234
+
235
+ # ─────────────────────────────────────────────────────────────────────────────
236
+ # mcp — Model Context Protocol server exposed to in-VM agents.
237
+ #
238
+ # 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. Two tools live
240
+ # there:
241
+ #
242
+ # • symphony.mark_done({ title, summary })
243
+ # • symphony.request_human_steering({ question, context? })
244
+ # ─────────────────────────────────────────────────────────────────────────────
245
+ mcp:
246
+ # enabled (bool): when false, the orchestrator refuses to dispatch (MCP is
247
+ # required for completion signaling). Default: true.
248
+ enabled: true
249
+
250
+ # host (string): hostname or IP the agent uses to reach the orchestrator
251
+ # from inside the smolvm. The port is resolved at runtime from the
252
+ # actually-bound HTTP server. Default: '127.0.0.1' (smolvm proxies VM
253
+ # loopback to host loopback; verified empirically).
254
+ host: 127.0.0.1
255
+
256
+ # host_url (string | null): full-URL override. When set, used verbatim and
257
+ # `host` + bound port are ignored. Use only when the VM cannot reach the
258
+ # orchestrator through the host gateway (e.g. bridge networking with a
259
+ # fixed reverse-proxy URL). Default: null.
260
+ host_url: null
261
+ ---
262
+ <!--
263
+ Liquid-templated prompt body. Rendered once per dispatched issue. Context:
264
+
265
+ issue.identifier — the issue's external id (e.g. "DEMO-42").
266
+ issue.title — issue title (string).
267
+ issue.state — current state (string, matches active_states[]).
268
+ issue.description — body text (string or empty).
269
+ issue.priority — number or null.
270
+ issue.labels — list of strings (lowercased).
271
+ attempt — int, 1-based attempt counter; absent on first attempt.
272
+
273
+ Available Liquid filters: standard Shopify Liquid plus `escape_once`.
274
+
275
+ The body below is the literal prompt sent to the agent. Keep it specific to
276
+ this workflow; orchestrator behavior (mark_done, request_human_steering) is
277
+ the same no matter what you write here.
278
+ -->
279
+
280
+ You are picking up a single issue and shepherding it through the workflow.
281
+
282
+ Issue: **{{ issue.identifier }} — {{ issue.title }}**
283
+ State: {{ issue.state }}
284
+ {% if issue.priority -%}Priority: {{ issue.priority }}{%- endif %}
285
+ {% if issue.labels.size > 0 -%}Labels: {% for l in issue.labels %}{{ l }}{% unless forloop.last %}, {% endunless %}{% endfor %}{%- endif %}
286
+
287
+ {% if issue.description -%}
288
+ Description:
289
+
290
+ {{ issue.description }}
291
+ {%- endif %}
292
+
293
+ Goals:
294
+
295
+ 1. Work in the current directory only; treat it as the issue workspace.
296
+ 2. Make the smallest correct change that satisfies the issue.
297
+ 3. Call `symphony.mark_done({ title, summary })` when done. This is the only
298
+ way to signal completion. The orchestrator atomically moves the issue file
299
+ to the terminal state and stops dispatching.
300
+ 4. If you cannot proceed without human input, call
301
+ `symphony.request_human_steering({ question, context? })`. Your turn ends
302
+ immediately; the human's reply arrives as your next prompt.
303
+
304
+ {% if attempt -%}
305
+ This is continuation/retry attempt {{ attempt }}. Inspect the workspace before
306
+ making new edits; your previous run may have left state behind.
307
+ {%- endif %}