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/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # smol-symphony
2
+
3
+ A small TypeScript orchestrator that reads issues off a local Markdown tracker,
4
+ prepares per-issue workspaces, and runs coding agents (Claude Code, Codex,
5
+ OpenCode) inside isolated [smolvm](https://smolmachines.com/) microVMs over the
6
+ [Agent Client Protocol](https://agentclientprotocol.com).
7
+
8
+ The agent signals completion through an injected MCP server (`mark_done`,
9
+ `request_human_steering`); the orchestrator handles state, retry, concurrency,
10
+ and produces either a pull request or a `git format-patch` bundle per issue.
11
+
12
+ ```
13
+ ┌──────────────────────────────────────────────────────────────────────────┐
14
+ │ symphony (node host) │
15
+ │ │
16
+ │ issues/<state>/*.md ─┐ │
17
+ │ WORKFLOW.md ─┼──▶ orchestrator ──▶ agent runner │
18
+ │ WORKFLOW.template.md │ (poll → reconcile → dispatch) │
19
+ │ │ │ │
20
+ │ │ ▼ ACP (JSON-RPC) │
21
+ │ │ ┌──────────────────────────┐ │
22
+ │ │ │ smolvm (per-issue VM) │ │
23
+ │ │ │ adapter binary │ │
24
+ │ │ │ workspace mount │ │
25
+ │ │ │ mcp client ←───────────┼─┼─▶ symphony MCP
26
+ │ │ └──────────────────────────┘ │ mark_done
27
+ │ ▼ │ request_human_steering
28
+ │ HTTP dashboard (HTMX): / │
29
+ │ attention · sessions · on disk · new issue · totals │
30
+ └──────────────────────────────────────────────────────────────────────────┘
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ Prerequisites:
36
+
37
+ - Node.js ≥ 20.
38
+ - A `smolvm` binary on `$PATH` with the server reachable on the configured
39
+ endpoint (e.g. `smolvm serve start --listen unix:///run/user/$UID/smolvm.sock`).
40
+ - A packed VM image (one-time): `bash scripts/build-vm.sh` produces
41
+ `.vm/symphony.smolmachine.smolmachine` (~1.1 GB; ships `claude-agent-acp`,
42
+ `codex-acp`, and `opencode` on the guest `$PATH`).
43
+ - For the default `acp.adapter: claude`: a credentials file at
44
+ `~/.claude/.credentials.json` on the host (symphony reads and stages it; the
45
+ host directory is **not** bind-mounted into the VM).
46
+
47
+ Run:
48
+
49
+ ```bash
50
+ npm install
51
+ npm run build
52
+ npx symphony WORKFLOW.md
53
+ ```
54
+
55
+ Open the dashboard at `http://127.0.0.1:8787/`. Drop issues into
56
+ `issues/Todo/` from the filesystem or the dashboard's `new issue` form;
57
+ symphony dispatches them on the next poll.
58
+
59
+ ## Local Markdown tracker
60
+
61
+ Issues live as `.md` files under `tracker.root`. The parent directory is the
62
+ issue state.
63
+
64
+ ```
65
+ issues/
66
+ ├── Todo/
67
+ │ ├── ABC-1.md
68
+ │ └── ABC-2.md
69
+ ├── In Progress/
70
+ │ └── ABC-3.md
71
+ └── Done/
72
+ └── ABC-4.md
73
+ ```
74
+
75
+ Each file has YAML front matter and an optional body:
76
+
77
+ ```markdown
78
+ ---
79
+ title: "Fix the login bug"
80
+ priority: 2
81
+ labels: [bug, auth]
82
+ blocked_by: [ABC-5]
83
+ ---
84
+ Long-form description in the body.
85
+ ```
86
+
87
+ State comparison is case-insensitive. Moving the file between state
88
+ directories is the canonical state transition; the orchestrator does this
89
+ itself in response to `mark_done`. The agent inside the VM does **not** have
90
+ filesystem access to the tracker root: it signals completion through the
91
+ MCP server and the orchestrator does the file move.
92
+
93
+ ## WORKFLOW.md
94
+
95
+ `WORKFLOW.md` is a YAML front matter block (orchestrator config) plus a
96
+ [Liquid](https://liquidjs.com/)-templated prompt body. The shipped file in
97
+ this repo is the canonical project workflow; see
98
+ [WORKFLOW.template.md](./WORKFLOW.template.md) for the annotated reference
99
+ covering every supported option, its type, default, and example.
100
+
101
+ Symphony watches the file and re-applies poll interval, concurrency, hooks,
102
+ prompt body, smolvm settings, etc. on change without restart. In-flight runs
103
+ keep the settings they started with.
104
+
105
+ ## Dashboard
106
+
107
+ When `server.port` is set (or `--port <n>` is passed), a single-page HTMX
108
+ dashboard is served at `/`. Five live regions poll their own partials every
109
+ 2s; idiomorph keeps unchanged DOM stable so polling doesn't twitch text.
110
+
111
+ - **header strip** — workflow file + tracker root + status badge
112
+ (`working` / `attention` / `idle`).
113
+ - **attention** — only present when something needs you. Steering requests
114
+ (question, original task, agent's context, reply textarea) and retry
115
+ queue ease open with a CSS `max-height` transition so the page doesn't
116
+ jump.
117
+ - **sessions** — running issues. Two-line rows: identifier + pill + turn +
118
+ tokens, then a dim last-message line.
119
+ - **on disk** — active-state issues not currently dispatched.
120
+ - **new issue** — collapsed `<details>` form. Posts JSON to
121
+ `POST /api/v1/issues`.
122
+ - **totals** — dim footer with token + runtime aggregate.
123
+
124
+ The steering reply form posts form-encoded with `HX-Request: true` and a
125
+ same-origin check. The endpoint also accepts `application/json` for direct
126
+ API clients. CSRF-relevant content types (`text/plain`,
127
+ `multipart/form-data`) are rejected with 415.
128
+
129
+ ## MCP — how the agent talks back
130
+
131
+ Symphony injects an MCP server into each ACP session at
132
+ `http://<host>:<bound-port>/api/v1/issues/<id>/mcp`, gated by a per-dispatch
133
+ bearer token. Two tools:
134
+
135
+ - **`symphony.mark_done({ title, summary })`** — call once at end of a
136
+ successful run. `title` is a single-line imperative summary (≤72 chars);
137
+ `summary` is a one- to three-paragraph narrative. The orchestrator
138
+ atomically moves the issue file to the terminal state and stops
139
+ dispatching. The pair lands in
140
+ `<workspace>/.git/symphony-runtime/mark_done.md` (or
141
+ `<workspace>/.symphony-runtime/mark_done.md` when the workspace doesn't
142
+ have its own `.git/`) for the `after_run` hook to consume.
143
+ - **`symphony.request_human_steering({ question, context? })`** — call
144
+ when blocked on something only a human can answer. The turn ends
145
+ immediately; the human's reply arrives as the prompt for the next turn.
146
+ Steering-reply turns don't count against `agent.max_turns`.
147
+
148
+ In smolvm, the VM's `127.0.0.1` transparently reaches the host's
149
+ `127.0.0.1` (verified empirically), so the agent reaches the orchestrator
150
+ without any mount or special host alias.
151
+
152
+ ## ACP — adapter registry
153
+
154
+ One ACP client (symphony's `agent/acp.ts`), two shipped adapter profiles.
155
+ Each profile encodes the binary symphony launches and the host credential
156
+ file it stages into the workspace before exec:
157
+
158
+ | Adapter | Binary | Host credential file |
159
+ | --------- | ------------------ | --------------------------------- |
160
+ | `claude` | `claude-agent-acp` | `~/.claude/.credentials.json` |
161
+ | `codex` | `codex-acp` | `~/.codex/auth.json` |
162
+
163
+ `WORKFLOW.md`:
164
+
165
+ ```yaml
166
+ acp:
167
+ adapter: claude
168
+ shell: bash
169
+ prompt_timeout_ms: 1800000
170
+ read_timeout_ms: 30000
171
+ stall_timeout_ms: 300000
172
+ ```
173
+
174
+ Selecting an adapter is enough — symphony auto-derives the launch command
175
+ that stages the credential into the workspace's runtime dir and copies it
176
+ into the adapter's expected guest path before exec. Set `command:` only to
177
+ override (testing a forked adapter, a non-standard binary path); doing so
178
+ opts out of automatic credential staging.
179
+
180
+ Credentials are **never bind-mounted from the host**. Symphony copies the
181
+ single credential file into a per-workspace location (under `.git/` when
182
+ the workspace has its own clone, else `.symphony-runtime/`) and refuses to
183
+ operate on workspaces inside the credential file's ancestor repo.
184
+
185
+ ## After-run handoff: PR or patch
186
+
187
+ `WORKFLOW.md`'s `after_run` hook ships in two modes:
188
+
189
+ - **Pull request mode.** Triggered when `SYMPHONY_REPO=<owner>/<repo>` is
190
+ exported. The hook pushes the per-issue branch to GitHub and runs
191
+ `gh pr create --base $SYMPHONY_BASE_BRANCH ...`. Requires `gh auth status`
192
+ to be clean on the host. The token never enters the VM.
193
+ - **Patch bundle mode** (default). Writes
194
+ `.symphony/patches/<branch>.patch` via `git format-patch` so you can
195
+ review and apply with `git am`. No remote required.
196
+
197
+ The agent's `mark_done.md` provides the PR title/body or commit message; the
198
+ hook reads it from the workspace's runtime dir.
199
+
200
+ See [AGENTS.md](./AGENTS.md) for the env-var contract and switch-over
201
+ commands.
202
+
203
+ ## Trust posture
204
+
205
+ Sandbox isolation comes from running each agent inside a smolvm microVM.
206
+ The VM has no network credentials (only the agent's API key is forwarded
207
+ via `smolvm.forward_env`), no tracker filesystem access (the tracker is
208
+ reached only through the MCP server), and stripped git remotes (set by
209
+ `after_create`).
210
+
211
+ Within the ACP session, the orchestrator follows SPEC §10.5's "high-trust"
212
+ posture:
213
+
214
+ - Command execution and file change approvals: auto-approve.
215
+ - User-input-required turns: end the turn (the orchestrator retries on
216
+ backoff).
217
+ - Unsupported dynamic tool calls: return failure; the session keeps running.
218
+
219
+ ## Tests
220
+
221
+ ```bash
222
+ npm run typecheck # tsc --noEmit
223
+ npm test # 67 tests across workflow, tracker, prompt, workspace,
224
+ # adapters, http, and mcp surfaces
225
+ npm run build # tsc emit to dist/
226
+ ```
227
+
228
+ An end-to-end smoke run needs a real smolvm + VM image.
229
+
230
+ ## License
231
+
232
+ MIT. See [LICENSE](./LICENSE).