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/AGENTS.md
CHANGED
|
@@ -1,35 +1,77 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
Standing instructions for any AI agent (Claude Code, Codex,
|
|
4
|
-
working on this repo.
|
|
3
|
+
Standing instructions and a small map for any AI agent (Claude Code, Codex,
|
|
4
|
+
OpenCode, etc.) working on this repo. Keep it short.
|
|
5
|
+
|
|
6
|
+
## Where things live
|
|
7
|
+
|
|
8
|
+
- `src/orchestrator.ts` — top-level wiring; per-issue dispatch lifecycle entry.
|
|
9
|
+
- `src/agent/runner.ts` — per-issue runner; owns the running-entry state the
|
|
10
|
+
Done-state action context is built from.
|
|
11
|
+
- `src/mcp.ts` — MCP tools exposed to in-VM agents (`symphony.transition`,
|
|
12
|
+
`symphony.request_human_steering`, `symphony.propose_issue`).
|
|
13
|
+
- `src/workflow.ts` — workflow file parser; the contract `WORKFLOW.template.md`
|
|
14
|
+
documents.
|
|
15
|
+
- `src/trackers/local.ts` — local markdown tracker (only kind today).
|
|
16
|
+
- `src/acp-bridge.ts` + `scripts/vm-agent.mjs` — host↔VM ACP transport.
|
|
17
|
+
- `images/agents/` — the agent rootfs image (Node + the ACP coding-agent CLIs +
|
|
18
|
+
the in-VM `/opt/symphony/vm-agent.mjs` launcher), built **once** via
|
|
19
|
+
`npm run build:image`. Selected by `WORKFLOW.md`'s `gondolin.image` (a build
|
|
20
|
+
id or `name:tag` ref). See `images/agents/README.md`.
|
|
21
|
+
- `src/http.ts` — HTTP dashboard + MCP endpoint listener.
|
|
22
|
+
- `WORKFLOW.md` — canonical workflow this repo dispatches against itself.
|
|
23
|
+
- `WORKFLOW.template.md` — annotated reference for workflow file syntax.
|
|
24
|
+
- Handoff (push + PR) is documented in `README.md` § "After-run handoff".
|
|
5
25
|
|
|
6
26
|
## Workflow template stays in sync
|
|
7
27
|
|
|
8
|
-
The repo ships two workflow files:
|
|
9
|
-
|
|
10
|
-
- `WORKFLOW.md` — the canonical workflow used to dispatch agents against this
|
|
11
|
-
project.
|
|
12
|
-
- `WORKFLOW.template.md` — the annotated reference covering every supported
|
|
13
|
-
option, its type, default, and example.
|
|
14
|
-
|
|
15
28
|
**When you change anything that affects workflow file syntax or semantics,
|
|
16
29
|
update `WORKFLOW.template.md` in the same commit.** Concretely:
|
|
17
30
|
|
|
18
31
|
- Adding a new YAML key under `tracker:`, `polling:`, `workspace:`, `hooks:`,
|
|
19
|
-
`agent:`, `acp:`, `
|
|
32
|
+
`agent:`, `acp:`, `gondolin:`, `server:`, or `mcp:` → document it in the
|
|
20
33
|
matching section of the template.
|
|
21
34
|
- Renaming or removing a key → rename or remove it in the template too.
|
|
22
|
-
- Changing a default value in `src/workflow.ts`
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
template
|
|
26
|
-
|
|
27
|
-
alongside the existing ones (under `hooks:` or under the prompt-body
|
|
28
|
-
comment).
|
|
35
|
+
- Changing a default value in `src/workflow.ts` → update the `Default:`
|
|
36
|
+
annotation in the template.
|
|
37
|
+
- Adding a new typed action-context variable (`$branch`, `$pr_title`, …) or
|
|
38
|
+
Liquid prompt-context field → list it in the template alongside the existing
|
|
39
|
+
ones.
|
|
29
40
|
|
|
30
41
|
If you find the template already drifted from the parser at the start of your
|
|
31
|
-
task, fix it as part of your change.
|
|
32
|
-
|
|
42
|
+
task, fix it as part of your change. An out-of-date template is a bug, not
|
|
43
|
+
paperwork.
|
|
44
|
+
|
|
45
|
+
## Where behavior belongs: orchestrator, typed actions, the image
|
|
46
|
+
|
|
47
|
+
State-machine logic — new transitions, anything that mutates tracker files the
|
|
48
|
+
orchestrator wrote, anything the runner must re-detect afterward — belongs in
|
|
49
|
+
the orchestrator (runner / MCP / `src/orchestrator.ts`) behind typed APIs with
|
|
50
|
+
tests. It is not an extension point you bolt on from the outside.
|
|
51
|
+
|
|
52
|
+
Repo-local glue that fires on a transition (push the branch, open a PR) belongs
|
|
53
|
+
in a state's typed `actions:` block — declarative records like `push_branch`,
|
|
54
|
+
`create_pr_if_missing`, and `run_in_vm` that the action executor runs against a
|
|
55
|
+
fixed context (`$branch`, `$base_branch`, `$pr_title`, `$pr_body_file`,
|
|
56
|
+
`$repo`). See `states.Done.actions` in `WORKFLOW.md` for the canonical
|
|
57
|
+
push-branch + open-PR pair, and `WORKFLOW.template.md` for the full kind list.
|
|
58
|
+
|
|
59
|
+
Per-VM tooling — the CLIs and runtimes the agent needs at runtime — belongs in
|
|
60
|
+
the agent image under `images/agents/`, baked once via `npm run build:image`.
|
|
61
|
+
Canonical workspace setup (clone, base checkout, `agent/<id>` branch cut,
|
|
62
|
+
remote stripping) is owned by the orchestrator's TypeScript `setupWorkspaceDir`;
|
|
63
|
+
the surviving shell `hooks:` (`after_create` / `before_run` / `before_remove`)
|
|
64
|
+
are only for additional repo-local prep on top of that, never for state-machine
|
|
65
|
+
behavior.
|
|
66
|
+
|
|
67
|
+
If you find yourself adding a `SYMPHONY_*` env var so a shell hook can reach
|
|
68
|
+
into orchestrator-owned state, or writing a hook the runner then has to
|
|
69
|
+
re-detect via a post-hook scan, that is the signal the logic is on the wrong
|
|
70
|
+
side of the seam — surface it as a typed call (a transition, an MCP tool, or a
|
|
71
|
+
typed action) instead. Issue bodies sometimes sketch a shell-shaped solution
|
|
72
|
+
under an `after_run:` heading; `after_run` is no longer a hook kind (it was
|
|
73
|
+
replaced by the typed `actions:` block above), so treat that as a sketch to
|
|
74
|
+
re-express, not a directive.
|
|
33
75
|
|
|
34
76
|
## Build, test, and check before declaring done
|
|
35
77
|
|
|
@@ -37,30 +79,55 @@ operators can write; an out-of-date template is a bug, not paperwork.
|
|
|
37
79
|
- `npm test` — must pass.
|
|
38
80
|
- `npm run build` — must pass.
|
|
39
81
|
|
|
40
|
-
Run all three before calling `symphony.
|
|
41
|
-
|
|
42
|
-
## Handoff: patch bundle vs. pull request
|
|
82
|
+
Run all three before calling `symphony.transition` into a terminal state.
|
|
43
83
|
|
|
44
|
-
|
|
84
|
+
## Filing tracker issues
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
before launch AND the working repo has an `origin` remote. The hook then
|
|
51
|
-
pushes the per-issue branch and runs `gh pr create`. `gh auth status` must
|
|
52
|
-
be clean on the host; the token never enters the VM.
|
|
53
|
-
|
|
54
|
-
To switch this project to PR mode:
|
|
86
|
+
When asked to file an issue against this repo's own tracker (the local Markdown
|
|
87
|
+
tracker at `~/.symphony/trackers/smol-symphony/<state>/`), POST to the running
|
|
88
|
+
symphony HTTP server rather than hand-writing the file or guessing the next
|
|
89
|
+
identifier:
|
|
55
90
|
|
|
56
91
|
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
curl -s -X POST http://127.0.0.1:8787/api/v1/issues \
|
|
93
|
+
-H "content-type: application/json" \
|
|
94
|
+
--data-binary @/tmp/issue.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Body shape (from `decideCreateIssue` in `src/http-handlers.ts`):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"title": "required, non-empty",
|
|
102
|
+
"state": "Todo", // optional; defaults to first declared active state
|
|
103
|
+
"identifier": "108", // optional; auto-picked as next free integer
|
|
104
|
+
"description": "markdown body, written below the YAML front matter",
|
|
105
|
+
"priority": 2, // optional integer; omitted ⇒ no `priority:` key
|
|
106
|
+
"labels": ["refactor"], // optional
|
|
107
|
+
"blocked_by": ["107"] // optional
|
|
108
|
+
}
|
|
60
109
|
```
|
|
61
110
|
|
|
62
|
-
`
|
|
63
|
-
|
|
111
|
+
State must be a declared non-terminal state (`active` or `holding`); terminals
|
|
112
|
+
are closed to direct creation. The server stamps `created_at` / `updated_at`,
|
|
113
|
+
allocates the next free numeric identifier when one isn't supplied, and writes
|
|
114
|
+
`<tracker.root>/<state>/<identifier>.md`. Response is `201` with
|
|
115
|
+
`{ path, identifier, state }`. Conflicts (duplicate identifier, bad state) come
|
|
116
|
+
back as `400`/`409` with a typed error code.
|
|
117
|
+
|
|
118
|
+
The HTTP server (`server.port` in `WORKFLOW.md`, currently `8787`) is unauthenticated
|
|
119
|
+
inside the trusted tailscale boundary — no bearer, no CSRF on JSON POSTs from curl.
|
|
120
|
+
If the server isn't running, hand-writing `~/.symphony/trackers/smol-symphony/<state>/<n>.md`
|
|
121
|
+
with the same YAML front-matter shape (`id`, `identifier`, `title`, `created_at`,
|
|
122
|
+
`updated_at`, optional `priority`/`labels`) is equivalent — see existing files
|
|
123
|
+
under `Done/` for canonical examples.
|
|
124
|
+
|
|
125
|
+
Issue bodies on this tracker follow a four-section shape: **Problem** (what's
|
|
126
|
+
wrong / why now), **Change** (concrete edits, with file paths and line ranges
|
|
127
|
+
where known), **allowed_paths** (the workspace scope the dispatched agent is
|
|
128
|
+
permitted to touch), **Acceptance** (the checks that must pass before
|
|
129
|
+
`symphony.transition` into a terminal state). Look at recent `Done/*.md` for the
|
|
130
|
+
shape — these are the prompts the dispatched agent sees, so be specific.
|
|
64
131
|
|
|
65
132
|
## Don't write to generated state
|
|
66
133
|
|
|
@@ -69,5 +136,5 @@ Skip these when staging commits unless the user asks:
|
|
|
69
136
|
- `.agents/`, `.claude/`, `.impeccable/` — local tooling state.
|
|
70
137
|
- `issues*/Done/`, `issues*/Cancelled/`, `issues*/In Progress/` — runtime
|
|
71
138
|
tracker artifacts from prior symphony runs.
|
|
72
|
-
- `.symphony/` — workspaces,
|
|
139
|
+
- `.symphony/` — workspaces, runtime caches.
|
|
73
140
|
- `skills-lock.json` — auto-generated.
|
package/PRODUCT.md
CHANGED
|
@@ -23,7 +23,8 @@ process is logging.
|
|
|
23
23
|
|
|
24
24
|
smol-symphony is a small TypeScript orchestrator that takes Markdown issues off a
|
|
25
25
|
local tracker, prepares per-issue workspaces, and runs coding agents
|
|
26
|
-
(Claude Code, Codex, OpenCode) inside isolated
|
|
26
|
+
(Claude Code, Codex, OpenCode) inside isolated per-issue Gondolin microVMs over
|
|
27
|
+
ACP. The
|
|
27
28
|
HTTP dashboard exists to do two things well:
|
|
28
29
|
|
|
29
30
|
1. **Dispatch** — create issues into the tracker without dropping back to the
|
package/README.md
CHANGED
|
@@ -1,77 +1,130 @@
|
|
|
1
1
|
# smol-symphony
|
|
2
2
|
|
|
3
|
+
> **Disclaimer:** this project is written using AI — the orchestrator
|
|
4
|
+
> dispatches AI coding agents at itself, and the bulk of the code in this
|
|
5
|
+
> repository was authored by those agents under human review.
|
|
6
|
+
|
|
3
7
|
A small TypeScript orchestrator that reads issues off a local Markdown tracker,
|
|
4
8
|
prepares per-issue workspaces, and runs coding agents (Claude Code, Codex,
|
|
5
|
-
OpenCode) inside isolated [
|
|
6
|
-
[Agent Client Protocol](https://agentclientprotocol.com).
|
|
9
|
+
OpenCode) inside isolated per-issue [Gondolin](https://github.com/earendil-works/gondolin)
|
|
10
|
+
microVMs over the [Agent Client Protocol](https://agentclientprotocol.com).
|
|
7
11
|
|
|
8
|
-
The agent signals
|
|
9
|
-
`request_human_steering`); the orchestrator handles state,
|
|
10
|
-
and
|
|
12
|
+
The agent signals progress through an injected MCP server (`transition`,
|
|
13
|
+
`request_human_steering`, `propose_issue`); the orchestrator handles state,
|
|
14
|
+
retry, concurrency, and opens a pull request per issue when configured for
|
|
15
|
+
a GitHub remote. In local-only mode the per-issue branch is left in the
|
|
16
|
+
workspace for review.
|
|
11
17
|
|
|
12
18
|
```
|
|
13
19
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
14
20
|
│ symphony (node host) │
|
|
15
21
|
│ │
|
|
16
|
-
│
|
|
17
|
-
│
|
|
18
|
-
│
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
│
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
29
|
-
│
|
|
22
|
+
│ ./issues/<state>/*.md ──┐ │
|
|
23
|
+
│ ./WORKFLOW.md ├──▶ orchestrator ──▶ agent runner │
|
|
24
|
+
│ ./WORKFLOW.template.md ──┘ poll · reconcile · dispatch │
|
|
25
|
+
│ │ │
|
|
26
|
+
│ ▼ ACP/RPC │
|
|
27
|
+
│ ┌───────────────────────────────┐ │
|
|
28
|
+
│ │ Gondolin (per-issue VM) │ │
|
|
29
|
+
│ │ adapter (claude / codex) │ │
|
|
30
|
+
│ │ workspace mount │ │
|
|
31
|
+
│ │ mcp client ────────────────┼─┐│
|
|
32
|
+
│ └───────────────────────────────┘ ││
|
|
33
|
+
│ ││
|
|
34
|
+
│ symphony MCP server ◀─────────────────────────────────────┘│
|
|
35
|
+
│ ( transition · request_human_steering · propose_issue ) │
|
|
36
|
+
│ │
|
|
37
|
+
│ HTTP dashboard (HTMX): / │
|
|
38
|
+
│ attention · sessions · on disk · new issue · totals │
|
|
30
39
|
└──────────────────────────────────────────────────────────────────────────┘
|
|
31
40
|
```
|
|
32
41
|
|
|
42
|
+
`SPEC.md` documents the contracts this repo's code references; for the
|
|
43
|
+
original architectural narrative, see
|
|
44
|
+
[openai/symphony/SPEC.md](https://github.com/openai/symphony/blob/main/SPEC.md).
|
|
45
|
+
|
|
33
46
|
## Quick start
|
|
34
47
|
|
|
35
48
|
Prerequisites:
|
|
36
49
|
|
|
37
50
|
- Node.js ≥ 20.
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`codex-acp`,
|
|
51
|
+
- The agent rootfs image. Gondolin is the in-process microVM substrate
|
|
52
|
+
(`@earendil-works/gondolin`), so there is no separate VM daemon to run. The
|
|
53
|
+
agent image is built **once** from `images/agents/` via `npm run build:image`:
|
|
54
|
+
it starts from `node:24-bookworm-slim`, installs base CLI tooling, npm-installs
|
|
55
|
+
every ACP-capable coding agent (`claude-agent-acp`, `codex-acp`, `opencode`),
|
|
56
|
+
and bakes the in-VM stdio launcher at `/opt/symphony/vm-agent.mjs` into the
|
|
57
|
+
image. The build prints a content-addressed build id; pin it (or a
|
|
58
|
+
`name:tag` ref like `symphony-agents:latest`) in `WORKFLOW.md`'s
|
|
59
|
+
`gondolin.image`. See [images/agents/README.md](./images/agents/README.md)
|
|
60
|
+
for the build steps and requirements.
|
|
43
61
|
- For the default `acp.adapter: claude`: a credentials file at
|
|
44
|
-
`~/.claude/.credentials.json` on the host
|
|
45
|
-
host
|
|
62
|
+
`~/.claude/.credentials.json` on the host. Symphony reads it only on the
|
|
63
|
+
host side: the guest holds only a token-shaped placeholder, and the host
|
|
64
|
+
substitutes the real OAuth access token into the outbound request at
|
|
65
|
+
Gondolin egress (TLS-MITM). The credential file itself is never staged into
|
|
66
|
+
the VM.
|
|
46
67
|
|
|
47
|
-
Run:
|
|
68
|
+
Run, against an existing workflow file in the current directory:
|
|
48
69
|
|
|
49
70
|
```bash
|
|
50
|
-
|
|
51
|
-
npm run build
|
|
52
|
-
npx symphony WORKFLOW.md
|
|
71
|
+
npx smol-symphony WORKFLOW.md
|
|
53
72
|
```
|
|
54
73
|
|
|
74
|
+
(Or `npm i -g smol-symphony` and then `symphony WORKFLOW.md` to skip the
|
|
75
|
+
fetch.) Both invoke the `symphony` bin shipped in this package.
|
|
76
|
+
|
|
55
77
|
Open the dashboard at `http://127.0.0.1:8787/`. Drop issues into
|
|
56
78
|
`issues/Todo/` from the filesystem or the dashboard's `new issue` form;
|
|
57
79
|
symphony dispatches them on the next poll.
|
|
58
80
|
|
|
81
|
+
The console stays quiet: with the default log file configured, symphony prints
|
|
82
|
+
a one-line-per-field startup banner (workflow, tracker root, dashboard URL,
|
|
83
|
+
log-file path) and routes the structured `key=value` stream to
|
|
84
|
+
`.symphony/logs/symphony.log` only. `tail -f` that file to follow the detail.
|
|
85
|
+
Pass `--verbose` (alias `--foreground`) to mirror the structured stream back to
|
|
86
|
+
the console for interactive debugging. With no log file configured (the
|
|
87
|
+
`SYMPHONY_LOG_FILE=""` override), the structured stream stays on stderr.
|
|
88
|
+
|
|
89
|
+
### From a checkout
|
|
90
|
+
|
|
91
|
+
If you're hacking on symphony itself:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/dizk/smol-symphony.git
|
|
95
|
+
cd smol-symphony
|
|
96
|
+
npm install
|
|
97
|
+
npm run build
|
|
98
|
+
npx symphony WORKFLOW.md # the local bin
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`npm run dev` (via `tsx watch`) reruns on source edits.
|
|
102
|
+
|
|
59
103
|
## Local Markdown tracker
|
|
60
104
|
|
|
61
105
|
Issues live as `.md` files under `tracker.root`. The parent directory is the
|
|
62
|
-
issue state
|
|
106
|
+
issue state; the set of valid state directories comes from the `states:` block
|
|
107
|
+
in `WORKFLOW.md` (see below) and is auto-mkdir'd on startup.
|
|
63
108
|
|
|
64
109
|
```
|
|
65
110
|
issues/
|
|
66
111
|
├── Todo/
|
|
67
|
-
│ ├──
|
|
68
|
-
│ └──
|
|
69
|
-
├──
|
|
70
|
-
│ └──
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
│ ├── 1.md
|
|
113
|
+
│ └── 2.md
|
|
114
|
+
├── Review/
|
|
115
|
+
│ └── 3.md
|
|
116
|
+
├── Done/
|
|
117
|
+
│ └── 4.md
|
|
118
|
+
└── Triage/
|
|
119
|
+
└── 5.md
|
|
73
120
|
```
|
|
74
121
|
|
|
122
|
+
The basename is the issue identifier. When a caller (dashboard form, MCP
|
|
123
|
+
`propose_issue`) omits an explicit identifier, the tracker picks the next free
|
|
124
|
+
positive integer by scanning every state directory under `tracker.root`.
|
|
125
|
+
Operator-supplied identifiers (e.g. `CACHE-7.md`) pass through unchanged and
|
|
126
|
+
coexist with the numeric ones.
|
|
127
|
+
|
|
75
128
|
Each file has YAML front matter and an optional body:
|
|
76
129
|
|
|
77
130
|
```markdown
|
|
@@ -79,16 +132,16 @@ Each file has YAML front matter and an optional body:
|
|
|
79
132
|
title: "Fix the login bug"
|
|
80
133
|
priority: 2
|
|
81
134
|
labels: [bug, auth]
|
|
82
|
-
blocked_by: [
|
|
135
|
+
blocked_by: [5]
|
|
83
136
|
---
|
|
84
137
|
Long-form description in the body.
|
|
85
138
|
```
|
|
86
139
|
|
|
87
140
|
State comparison is case-insensitive. Moving the file between state
|
|
88
141
|
directories is the canonical state transition; the orchestrator does this
|
|
89
|
-
itself in response to `
|
|
90
|
-
filesystem access to the tracker root: it signals
|
|
91
|
-
MCP server and the orchestrator does the file move.
|
|
142
|
+
itself in response to `symphony.transition`. The agent inside the VM does
|
|
143
|
+
**not** have filesystem access to the tracker root: it signals progress
|
|
144
|
+
through the MCP server and the orchestrator does the file move.
|
|
92
145
|
|
|
93
146
|
## WORKFLOW.md
|
|
94
147
|
|
|
@@ -98,9 +151,19 @@ this repo is the canonical project workflow; see
|
|
|
98
151
|
[WORKFLOW.template.md](./WORKFLOW.template.md) for the annotated reference
|
|
99
152
|
covering every supported option, its type, default, and example.
|
|
100
153
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
154
|
+
The workflow is a **state machine**. A required top-level `states:` block
|
|
155
|
+
declares every state an issue can occupy, its `role` (`active` — dispatched;
|
|
156
|
+
`terminal` — triggers cleanup and handoff; `holding` — sits outside the
|
|
157
|
+
dispatch loop, e.g. `Triage`), and optional per-state `adapter`, `model`,
|
|
158
|
+
`max_turns`, and `allowed_transitions` overrides. A single issue can travel
|
|
159
|
+
through any number of states with distinct adapters and instructions; the
|
|
160
|
+
prompt body can branch on the current state with Liquid
|
|
161
|
+
`{% case issue.state %}`. The shipped workflow uses a two-stage
|
|
162
|
+
`Todo → Review → Done` flow (Claude implements, Codex reviews).
|
|
163
|
+
|
|
164
|
+
Symphony watches the file and re-applies poll interval, concurrency, typed
|
|
165
|
+
actions, prompt body, gondolin settings, etc. on change without restart.
|
|
166
|
+
In-flight runs keep the settings they started with.
|
|
104
167
|
|
|
105
168
|
## Dashboard
|
|
106
169
|
|
|
@@ -130,35 +193,45 @@ API clients. CSRF-relevant content types (`text/plain`,
|
|
|
130
193
|
|
|
131
194
|
Symphony injects an MCP server into each ACP session at
|
|
132
195
|
`http://<host>:<bound-port>/api/v1/issues/<id>/mcp`, gated by a per-dispatch
|
|
133
|
-
bearer token.
|
|
134
|
-
|
|
135
|
-
- **`symphony.
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
196
|
+
bearer token. Three tools:
|
|
197
|
+
|
|
198
|
+
- **`symphony.transition({ to_state, notes? })`** — canonical (and only)
|
|
199
|
+
exit verb. Moves the issue into another declared state, optionally
|
|
200
|
+
appending markdown `notes` to the issue body before the move so the next
|
|
201
|
+
agent (in `to_state`) reads them as part of `issue.description`. A
|
|
202
|
+
transition into a `role: terminal` state ends the run and triggers
|
|
203
|
+
workspace cleanup; transitions between active/holding states preserve the
|
|
204
|
+
workspace so the same `agent/<id>` git branch survives the handoff.
|
|
205
|
+
Rejected transitions (unknown target, disallowed by
|
|
206
|
+
`allowed_transitions`) return MCP tool-result errors the agent can read
|
|
207
|
+
and retry.
|
|
143
208
|
- **`symphony.request_human_steering({ question, context? })`** — call
|
|
144
209
|
when blocked on something only a human can answer. The turn ends
|
|
145
210
|
immediately; the human's reply arrives as the prompt for the next turn.
|
|
146
211
|
Steering-reply turns don't count against `agent.max_turns`.
|
|
147
|
-
|
|
148
|
-
|
|
212
|
+
- **`symphony.propose_issue({ title, description?, labels?, priority? })`** —
|
|
213
|
+
call when the agent notices work that is out of scope for its current
|
|
214
|
+
task. The proposal lands in the `Triage/` state directory, which the
|
|
215
|
+
orchestrator does **not** dispatch; the operator approves (→ first active
|
|
216
|
+
state) or discards (→ first terminal state, prefers `Cancelled`) from the
|
|
217
|
+
dashboard. The calling issue's identifier and a timestamp are stamped into
|
|
218
|
+
the proposal's front-matter as `proposed_by` / `proposed_at` so provenance
|
|
219
|
+
is visible.
|
|
220
|
+
|
|
221
|
+
In Gondolin, the VM's `127.0.0.1` transparently reaches the host's
|
|
149
222
|
`127.0.0.1` (verified empirically), so the agent reaches the orchestrator
|
|
150
223
|
without any mount or special host alias.
|
|
151
224
|
|
|
152
225
|
## ACP — adapter registry
|
|
153
226
|
|
|
154
227
|
One ACP client (symphony's `agent/acp.ts`), two shipped adapter profiles.
|
|
155
|
-
Each profile encodes the binary symphony launches and the
|
|
156
|
-
|
|
228
|
+
Each profile encodes the binary symphony launches and the credential path
|
|
229
|
+
the adapter reaches for inside the VM:
|
|
157
230
|
|
|
158
|
-
| Adapter | Binary |
|
|
231
|
+
| Adapter | Binary | Credential surface |
|
|
159
232
|
| --------- | ------------------ | --------------------------------- |
|
|
160
|
-
| `claude` | `claude-agent-acp` |
|
|
161
|
-
| `codex` | `codex-acp` |
|
|
233
|
+
| `claude` | `claude-agent-acp` | placeholder + host egress swap |
|
|
234
|
+
| `codex` | `codex-acp` | placeholder + host egress swap |
|
|
162
235
|
|
|
163
236
|
`WORKFLOW.md`:
|
|
164
237
|
|
|
@@ -171,44 +244,64 @@ acp:
|
|
|
171
244
|
stall_timeout_ms: 300000
|
|
172
245
|
```
|
|
173
246
|
|
|
174
|
-
Selecting an adapter is enough — symphony auto-derives the launch command
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
247
|
+
Selecting an adapter is enough — symphony auto-derives the launch command.
|
|
248
|
+
For `claude`, a per-VM identity file (organization + account UUIDs only,
|
|
249
|
+
no tokens) is staged into the workspace runtime dir and copied to
|
|
250
|
+
`~/.claude.json` inside the VM; the guest holds only a token-shaped
|
|
251
|
+
placeholder, and the host substitutes the real access token into the
|
|
252
|
+
outbound request at Gondolin egress. For `codex`, no identity file is
|
|
253
|
+
staged (OpenAI ships no third-party fingerprint check); the VM is launched
|
|
254
|
+
with `OPENAI_API_KEY=<placeholder>`, and the host substitutes the real
|
|
255
|
+
OpenAI credential at egress. Set `command:` only to override (testing a
|
|
256
|
+
forked adapter, a non-standard binary path).
|
|
257
|
+
|
|
258
|
+
Credentials **never enter the VM** for either adapter. For `claude`, the
|
|
259
|
+
host's `~/.claude/.credentials.json` is read only on the host side; the VM
|
|
260
|
+
sees `~/.claude.json` (identity-only) plus the placeholder value in its
|
|
261
|
+
`Authorization` header, which the host swaps for the real token at egress.
|
|
262
|
+
For `codex`, the host's `~/.codex/auth.json` (access token /
|
|
263
|
+
`OPENAI_API_KEY`, **never** the refresh token) is read only host-side; the
|
|
264
|
+
real `OPENAI_API_KEY` is stripped from the forwarded VM boot env, so the VM
|
|
265
|
+
holds only the placeholder.
|
|
266
|
+
|
|
267
|
+
## After-run handoff: pull request
|
|
268
|
+
|
|
269
|
+
On transition into the Done state the orchestrator runs that state's typed
|
|
270
|
+
`actions:` block — two records, `push_branch` then `create_pr_if_missing` —
|
|
271
|
+
which push the per-issue branch and open a PR when a GitHub repo is configured:
|
|
272
|
+
set `workspace.github_repo: <owner>/<repo>` in `WORKFLOW.md` (or export
|
|
273
|
+
`SYMPHONY_REPO=<owner>/<repo>`, which overrides the file). Each action's
|
|
274
|
+
`if: $repo` predicate short-circuits to a no-op in local-only mode (neither
|
|
275
|
+
set), so the branch is simply left in the workspace until cleanup; pick the
|
|
276
|
+
commits up with `git log agent/<id>` against your local clone. The PR is opened with `gh pr create --base
|
|
277
|
+
$base_branch ...`, which requires `gh auth status` to be clean on the host; the
|
|
278
|
+
token never enters the VM.
|
|
279
|
+
|
|
280
|
+
The orchestrator stages the action context — `$branch`, `$base_branch`,
|
|
281
|
+
`$pr_title` (already id-prefixed), and `$pr_body_file` (a temp file holding the
|
|
282
|
+
current issue body) — before the block fires, so the PR description carries
|
|
283
|
+
every `symphony.transition` notes block accumulated across the run, the full
|
|
284
|
+
handoff thread from implementer through reviewer to approval. Per-action retry
|
|
285
|
+
and snapshot plumbing replaces the old opaque shell-exit surface: on a
|
|
286
|
+
rate-limit the `create_pr_if_missing` action surfaces "retrying in 60s" on the
|
|
287
|
+
dashboard rather than failing silently.
|
|
288
|
+
|
|
289
|
+
See `states.Done.actions` in [WORKFLOW.md](./WORKFLOW.md) for the canonical
|
|
290
|
+
record pair and [WORKFLOW.template.md](./WORKFLOW.template.md) for the
|
|
291
|
+
typed-action reference.
|
|
202
292
|
|
|
203
293
|
## Trust posture
|
|
204
294
|
|
|
205
|
-
Sandbox isolation comes from running each agent inside a
|
|
206
|
-
The VM has no
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
295
|
+
Sandbox isolation comes from running each agent inside a Gondolin microVM.
|
|
296
|
+
The VM has no OAuth refresh tokens or long-lived access tokens: for both
|
|
297
|
+
`claude` and `codex`, the guest holds only a token-shaped placeholder and
|
|
298
|
+
the host substitutes the real credential into the outbound request at
|
|
299
|
+
Gondolin egress, so no real Anthropic or OpenAI credential is present in
|
|
300
|
+
the VM. The VM has no tracker filesystem access (the tracker is reached
|
|
301
|
+
only through the MCP server) and stripped git remotes (set by the
|
|
302
|
+
orchestrator's `setupWorkspaceDir`).
|
|
210
303
|
|
|
211
|
-
Within the ACP session, the orchestrator follows SPEC §
|
|
304
|
+
Within the ACP session, the orchestrator follows SPEC §6.1's "high-trust"
|
|
212
305
|
posture:
|
|
213
306
|
|
|
214
307
|
- Command execution and file change approvals: auto-approve.
|
|
@@ -220,12 +313,16 @@ posture:
|
|
|
220
313
|
|
|
221
314
|
```bash
|
|
222
315
|
npm run typecheck # tsc --noEmit
|
|
223
|
-
npm test #
|
|
224
|
-
# adapters, http,
|
|
316
|
+
npm test # 170 tests across workflow, tracker, prompt, workspace,
|
|
317
|
+
# adapters, http, mcp, acp-bridge, orchestrator, run log,
|
|
318
|
+
# runner state resolution, and tool-call summary surfaces
|
|
225
319
|
npm run build # tsc emit to dist/
|
|
226
320
|
```
|
|
227
321
|
|
|
228
|
-
An end-to-end smoke run needs
|
|
322
|
+
An end-to-end smoke run needs the built agent image (see `images/agents/`).
|
|
323
|
+
|
|
324
|
+
See [CHANGELOG.md](./CHANGELOG.md) for operator-visible changes between
|
|
325
|
+
releases.
|
|
229
326
|
|
|
230
327
|
## License
|
|
231
328
|
|