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.
Files changed (140) hide show
  1. package/AGENTS.md +105 -38
  2. package/PRODUCT.md +2 -1
  3. package/README.md +195 -98
  4. package/SPEC.md +543 -1915
  5. package/WORKFLOW.md +654 -179
  6. package/WORKFLOW.template.md +761 -121
  7. package/dist/acp-bridge.js +324 -0
  8. package/dist/acp-bridge.js.map +1 -0
  9. package/dist/actions/cache.js +191 -0
  10. package/dist/actions/cache.js.map +1 -0
  11. package/dist/actions/effects.js +41 -0
  12. package/dist/actions/effects.js.map +1 -0
  13. package/dist/actions/executor.js +570 -0
  14. package/dist/actions/executor.js.map +1 -0
  15. package/dist/actions/index.js +13 -0
  16. package/dist/actions/index.js.map +1 -0
  17. package/dist/actions/parsing.js +273 -0
  18. package/dist/actions/parsing.js.map +1 -0
  19. package/dist/actions/predicate-env.js +27 -0
  20. package/dist/actions/predicate-env.js.map +1 -0
  21. package/dist/actions/predicates.js +49 -0
  22. package/dist/actions/predicates.js.map +1 -0
  23. package/dist/actions/templating.js +66 -0
  24. package/dist/actions/templating.js.map +1 -0
  25. package/dist/actions/types.js +15 -0
  26. package/dist/actions/types.js.map +1 -0
  27. package/dist/agent/acp.js +232 -63
  28. package/dist/agent/acp.js.map +1 -1
  29. package/dist/agent/adapter-names.js +159 -0
  30. package/dist/agent/adapter-names.js.map +1 -0
  31. package/dist/agent/adapters.js +338 -102
  32. package/dist/agent/adapters.js.map +1 -1
  33. package/dist/agent/credential-extractors.js +342 -0
  34. package/dist/agent/credential-extractors.js.map +1 -0
  35. package/dist/agent/credential-secrets.js +628 -0
  36. package/dist/agent/credential-secrets.js.map +1 -0
  37. package/dist/agent/credential-ticker.js +57 -0
  38. package/dist/agent/credential-ticker.js.map +1 -0
  39. package/dist/agent/gondolin-creds-staging.js +356 -0
  40. package/dist/agent/gondolin-creds-staging.js.map +1 -0
  41. package/dist/agent/gondolin-dispatch.js +375 -0
  42. package/dist/agent/gondolin-dispatch.js.map +1 -0
  43. package/dist/agent/gondolin.js +124 -0
  44. package/dist/agent/gondolin.js.map +1 -0
  45. package/dist/agent/runner-decisions.js +134 -0
  46. package/dist/agent/runner-decisions.js.map +1 -0
  47. package/dist/agent/runner.js +1352 -290
  48. package/dist/agent/runner.js.map +1 -1
  49. package/dist/agent/tool-call-summary.js +102 -0
  50. package/dist/agent/tool-call-summary.js.map +1 -0
  51. package/dist/agent/vm-acp-mapping.js +73 -0
  52. package/dist/agent/vm-acp-mapping.js.map +1 -0
  53. package/dist/agent/vm-guards.js +262 -0
  54. package/dist/agent/vm-guards.js.map +1 -0
  55. package/dist/agent/vm-port.js +22 -0
  56. package/dist/agent/vm-port.js.map +1 -0
  57. package/dist/agent/vm-process-registry.js +79 -0
  58. package/dist/agent/vm-process-registry.js.map +1 -0
  59. package/dist/bin/cli-args.js +105 -0
  60. package/dist/bin/cli-args.js.map +1 -0
  61. package/dist/bin/symphony.js +719 -130
  62. package/dist/bin/symphony.js.map +1 -1
  63. package/dist/errors.js +15 -0
  64. package/dist/errors.js.map +1 -0
  65. package/dist/http-disk.js +135 -0
  66. package/dist/http-disk.js.map +1 -0
  67. package/dist/http-handlers.js +180 -0
  68. package/dist/http-handlers.js.map +1 -0
  69. package/dist/http.js +1476 -764
  70. package/dist/http.js.map +1 -1
  71. package/dist/issues.js +178 -0
  72. package/dist/issues.js.map +1 -0
  73. package/dist/logging.js +163 -5
  74. package/dist/logging.js.map +1 -1
  75. package/dist/mcp.js +391 -163
  76. package/dist/mcp.js.map +1 -1
  77. package/dist/memory.js +85 -0
  78. package/dist/memory.js.map +1 -0
  79. package/dist/orchestrator-decisions.js +331 -0
  80. package/dist/orchestrator-decisions.js.map +1 -0
  81. package/dist/orchestrator.js +1189 -303
  82. package/dist/orchestrator.js.map +1 -1
  83. package/dist/prompt.js +5 -5
  84. package/dist/prompt.js.map +1 -1
  85. package/dist/reconciler/cache.js +65 -0
  86. package/dist/reconciler/cache.js.map +1 -0
  87. package/dist/reconciler/index.js +448 -0
  88. package/dist/reconciler/index.js.map +1 -0
  89. package/dist/reconciler/ledger.js +131 -0
  90. package/dist/reconciler/ledger.js.map +1 -0
  91. package/dist/reconciler/pr-adapters.js +174 -0
  92. package/dist/reconciler/pr-adapters.js.map +1 -0
  93. package/dist/reconciler/pr-decide.js +167 -0
  94. package/dist/reconciler/pr-decide.js.map +1 -0
  95. package/dist/reconciler/pr.js +422 -0
  96. package/dist/reconciler/pr.js.map +1 -0
  97. package/dist/reconciler/types.js +12 -0
  98. package/dist/reconciler/types.js.map +1 -0
  99. package/dist/reconciler/vm.js +243 -0
  100. package/dist/reconciler/vm.js.map +1 -0
  101. package/dist/reconciler/workspace-defaults.js +83 -0
  102. package/dist/reconciler/workspace-defaults.js.map +1 -0
  103. package/dist/reconciler/workspace.js +272 -0
  104. package/dist/reconciler/workspace.js.map +1 -0
  105. package/dist/runlog.js +403 -0
  106. package/dist/runlog.js.map +1 -0
  107. package/dist/scaffold.js +165 -0
  108. package/dist/scaffold.js.map +1 -0
  109. package/dist/trackers/local.js +234 -133
  110. package/dist/trackers/local.js.map +1 -1
  111. package/dist/trackers/types.js +1 -1
  112. package/dist/trackers/types.js.map +1 -1
  113. package/dist/types.js +1 -1
  114. package/dist/util/clock.js +12 -0
  115. package/dist/util/clock.js.map +1 -0
  116. package/dist/util/crypto.js +25 -0
  117. package/dist/util/crypto.js.map +1 -0
  118. package/dist/util/frontmatter.js +70 -0
  119. package/dist/util/frontmatter.js.map +1 -0
  120. package/dist/util/fs-issues.js +22 -0
  121. package/dist/util/fs-issues.js.map +1 -0
  122. package/dist/util/process.js +152 -0
  123. package/dist/util/process.js.map +1 -0
  124. package/dist/util/workspace-key.js +10 -0
  125. package/dist/util/workspace-key.js.map +1 -0
  126. package/dist/workflow-loader.js +147 -0
  127. package/dist/workflow-loader.js.map +1 -0
  128. package/dist/workflow.js +656 -219
  129. package/dist/workflow.js.map +1 -1
  130. package/dist/workspace-types.js +8 -0
  131. package/dist/workspace-types.js.map +1 -0
  132. package/dist/workspace.js +367 -120
  133. package/dist/workspace.js.map +1 -1
  134. package/package.json +14 -6
  135. package/scripts/vm-agent.mjs +211 -0
  136. package/dist/agent/codex.js +0 -439
  137. package/dist/agent/codex.js.map +0 -1
  138. package/dist/agent/smolvm.js +0 -174
  139. package/dist/agent/smolvm.js.map +0 -1
  140. 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, OpenCode, etc.)
4
- working on this repo. Short list; keep it that way.
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:`, `smolvm:`, `server:`, or `mcp:` → document it in the
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` (or whichever parser becomes
23
- authoritative) → update the `Default:` annotation in the template.
24
- - Introducing a new top-level section add a new section block to the
25
- template.
26
- - Adding a new hook env var or Liquid context field → list it in the template
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. The template is the contract for what
32
- operators can write; an out-of-date template is a bug, not paperwork.
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.mark_done`.
41
-
42
- ## Handoff: patch bundle vs. pull request
82
+ Run all three before calling `symphony.transition` into a terminal state.
43
83
 
44
- `after_run` in `WORKFLOW.md` ships in two modes:
84
+ ## Filing tracker issues
45
85
 
46
- - **Patch bundle (default).** Writes `git format-patch` to
47
- `.symphony/patches/<branch>.patch` for human review. No remote needed.
48
- This is what fires when no GitHub remote is wired up.
49
- - **Pull request.** Triggered when `SYMPHONY_REPO=<owner>/<repo>` is exported
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
- git remote add origin git@github.com:<owner>/smol-symphony.git
58
- git push -u origin main
59
- SYMPHONY_REPO=<owner>/smol-symphony npx symphony WORKFLOW.md
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
- `SYMPHONY_BASE_BRANCH` (default `main`) overrides the base the agent branches
63
- from and the PR opens against.
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, patch bundles, runtime caches.
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 smolvm microVMs over ACP. The
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 [smolvm](https://smolmachines.com/) microVMs over the
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 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.
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
- 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
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
- - 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`).
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 (symphony reads and stages it; the
45
- host directory is **not** bind-mounted into the VM).
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
- npm install
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
- │ ├── ABC-1.md
68
- │ └── ABC-2.md
69
- ├── In Progress/
70
- │ └── ABC-3.md
71
- └── Done/
72
- └── ABC-4.md
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: [ABC-5]
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 `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.
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
- 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.
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. 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.
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
- In smolvm, the VM's `127.0.0.1` transparently reaches the host's
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 host credential
156
- file it stages into the workspace before exec:
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 | Host credential file |
231
+ | Adapter | Binary | Credential surface |
159
232
  | --------- | ------------------ | --------------------------------- |
160
- | `claude` | `claude-agent-acp` | `~/.claude/.credentials.json` |
161
- | `codex` | `codex-acp` | `~/.codex/auth.json` |
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
- 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.
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 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`).
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 §10.5's "high-trust"
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 # 67 tests across workflow, tracker, prompt, workspace,
224
- # adapters, http, and mcp surfaces
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 a real smolvm + VM image.
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