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/AGENTS.md +73 -0
- package/DESIGN.md +453 -0
- package/LICENSE +21 -0
- package/PRODUCT.md +106 -0
- package/README.md +232 -0
- package/SPEC.md +2169 -0
- package/WORKFLOW.md +269 -0
- package/WORKFLOW.template.md +307 -0
- package/dist/agent/acp.js +304 -0
- package/dist/agent/acp.js.map +1 -0
- package/dist/agent/adapters.js +275 -0
- package/dist/agent/adapters.js.map +1 -0
- package/dist/agent/codex.js +439 -0
- package/dist/agent/codex.js.map +1 -0
- package/dist/agent/runner.js +394 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/smolvm.js +174 -0
- package/dist/agent/smolvm.js.map +1 -0
- package/dist/bin/symphony.js +205 -0
- package/dist/bin/symphony.js.map +1 -0
- package/dist/http.js +1189 -0
- package/dist/http.js.map +1 -0
- package/dist/logging.js +45 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp.js +478 -0
- package/dist/mcp.js.map +1 -0
- package/dist/orchestrator.js +683 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/prompt.js +65 -0
- package/dist/prompt.js.map +1 -0
- package/dist/trackers/local.js +344 -0
- package/dist/trackers/local.js.map +1 -0
- package/dist/trackers/types.js +10 -0
- package/dist/trackers/types.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow.js +385 -0
- package/dist/workflow.js.map +1 -0
- package/dist/workspace.js +196 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +68 -0
- package/scripts/build-vm.sh +67 -0
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 %}
|