smol-symphony 0.1.0 → 0.3.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 (627) hide show
  1. package/AGENTS.md +125 -39
  2. package/DESIGN.md +494 -273
  3. package/PRODUCT.md +2 -1
  4. package/README.md +259 -110
  5. package/SPEC.md +565 -1928
  6. package/WORKFLOW.minimal.yaml +34 -0
  7. package/WORKFLOW.template.yaml +1100 -0
  8. package/WORKFLOW.yaml +487 -0
  9. package/assets/skills/symphony-issues/SKILL.md +136 -0
  10. package/assets/symphony-mise.system.toml +68 -0
  11. package/dist/src/bin/symphony.js +30 -0
  12. package/dist/src/bin/symphony.js.map +1 -0
  13. package/dist/src/core/actions/context.js +109 -0
  14. package/dist/src/core/actions/context.js.map +1 -0
  15. package/dist/src/core/actions/parse.js +192 -0
  16. package/dist/src/core/actions/parse.js.map +1 -0
  17. package/dist/src/core/actions/plan.js +197 -0
  18. package/dist/src/core/actions/plan.js.map +1 -0
  19. package/dist/src/core/actions/predicates.js +111 -0
  20. package/dist/src/core/actions/predicates.js.map +1 -0
  21. package/dist/src/core/actions/run-fold.js +248 -0
  22. package/dist/src/core/actions/run-fold.js.map +1 -0
  23. package/dist/src/core/actions/template.js +118 -0
  24. package/dist/src/core/actions/template.js.map +1 -0
  25. package/dist/src/core/cli/args.js +116 -0
  26. package/dist/src/core/cli/args.js.map +1 -0
  27. package/dist/src/core/coerce.js +75 -0
  28. package/dist/src/core/coerce.js.map +1 -0
  29. package/dist/src/core/credential/account-id.js +20 -0
  30. package/dist/src/core/credential/account-id.js.map +1 -0
  31. package/dist/src/core/credential/adapter-config.js +136 -0
  32. package/dist/src/core/credential/adapter-config.js.map +1 -0
  33. package/dist/src/core/credential/availability.js +98 -0
  34. package/dist/src/core/credential/availability.js.map +1 -0
  35. package/dist/src/core/credential/extract.js +228 -0
  36. package/dist/src/core/credential/extract.js.map +1 -0
  37. package/dist/src/core/credential/fake-creds.js +171 -0
  38. package/dist/src/core/credential/fake-creds.js.map +1 -0
  39. package/dist/src/core/credential/identity.js +125 -0
  40. package/dist/src/core/credential/identity.js.map +1 -0
  41. package/dist/src/core/credential/shape.js +230 -0
  42. package/dist/src/core/credential/shape.js.map +1 -0
  43. package/dist/src/core/credential/strings.js +15 -0
  44. package/dist/src/core/credential/strings.js.map +1 -0
  45. package/dist/src/core/doctor/checks.js +303 -0
  46. package/dist/src/core/doctor/checks.js.map +1 -0
  47. package/dist/src/core/git/result.js +107 -0
  48. package/dist/src/core/git/result.js.map +1 -0
  49. package/dist/src/core/http/decisions.js +225 -0
  50. package/dist/src/core/http/decisions.js.map +1 -0
  51. package/dist/src/core/http/render.js +1635 -0
  52. package/dist/src/core/http/render.js.map +1 -0
  53. package/dist/src/core/http/routes.js +145 -0
  54. package/dist/src/core/http/routes.js.map +1 -0
  55. package/dist/src/core/http/views.js +181 -0
  56. package/dist/src/core/http/views.js.map +1 -0
  57. package/dist/src/core/image/managed-image.js +95 -0
  58. package/dist/src/core/image/managed-image.js.map +1 -0
  59. package/dist/src/core/issue/file.js +149 -0
  60. package/dist/src/core/issue/file.js.map +1 -0
  61. package/dist/src/core/issue/parse.js +210 -0
  62. package/dist/src/core/issue/parse.js.map +1 -0
  63. package/dist/src/core/mcp/dispatch.js +239 -0
  64. package/dist/src/core/mcp/dispatch.js.map +1 -0
  65. package/dist/src/core/mcp/post-move.js +92 -0
  66. package/dist/src/core/mcp/post-move.js.map +1 -0
  67. package/dist/src/core/mcp/protocol.js +293 -0
  68. package/dist/src/core/mcp/protocol.js.map +1 -0
  69. package/dist/src/core/mcp/url.js +162 -0
  70. package/dist/src/core/mcp/url.js.map +1 -0
  71. package/dist/src/core/path.js +63 -0
  72. package/dist/src/core/path.js.map +1 -0
  73. package/dist/src/core/reconcile/image-decide.js +48 -0
  74. package/dist/src/core/reconcile/image-decide.js.map +1 -0
  75. package/dist/src/core/reconcile/ledger.js +142 -0
  76. package/dist/src/core/reconcile/ledger.js.map +1 -0
  77. package/dist/src/core/reconcile/pr-classify.js +62 -0
  78. package/dist/src/core/reconcile/pr-classify.js.map +1 -0
  79. package/dist/src/core/reconcile/pr-decide.js +180 -0
  80. package/dist/src/core/reconcile/pr-decide.js.map +1 -0
  81. package/dist/src/core/reconcile/pr-loop.js +161 -0
  82. package/dist/src/core/reconcile/pr-loop.js.map +1 -0
  83. package/dist/src/core/reconcile/pr-notes.js +35 -0
  84. package/dist/src/core/reconcile/pr-notes.js.map +1 -0
  85. package/dist/src/core/reconcile/vm-decide.js +70 -0
  86. package/dist/src/core/reconcile/vm-decide.js.map +1 -0
  87. package/dist/src/core/reconcile/vm-reap.js +207 -0
  88. package/dist/src/core/reconcile/vm-reap.js.map +1 -0
  89. package/dist/src/core/reconcile/workspace-decide.js +162 -0
  90. package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
  91. package/dist/src/core/runlog/summary.js +231 -0
  92. package/dist/src/core/runlog/summary.js.map +1 -0
  93. package/dist/src/core/runner/dispatch-config.js +95 -0
  94. package/dist/src/core/runner/dispatch-config.js.map +1 -0
  95. package/dist/src/core/runner/injection.js +61 -0
  96. package/dist/src/core/runner/injection.js.map +1 -0
  97. package/dist/src/core/runner/mise.js +210 -0
  98. package/dist/src/core/runner/mise.js.map +1 -0
  99. package/dist/src/core/runner/prompt.js +720 -0
  100. package/dist/src/core/runner/prompt.js.map +1 -0
  101. package/dist/src/core/runner/turn.js +242 -0
  102. package/dist/src/core/runner/turn.js.map +1 -0
  103. package/dist/src/core/runner/vm-plan.js +390 -0
  104. package/dist/src/core/runner/vm-plan.js.map +1 -0
  105. package/dist/src/core/schedule/admission.js +123 -0
  106. package/dist/src/core/schedule/admission.js.map +1 -0
  107. package/dist/src/core/schedule/circuit-breaker.js +111 -0
  108. package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
  109. package/dist/src/core/schedule/eligibility.js +83 -0
  110. package/dist/src/core/schedule/eligibility.js.map +1 -0
  111. package/dist/src/core/schedule/reconcile-issue.js +82 -0
  112. package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
  113. package/dist/src/core/schedule/retry.js +96 -0
  114. package/dist/src/core/schedule/retry.js.map +1 -0
  115. package/dist/src/core/schedule/sleep-cycle.js +133 -0
  116. package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
  117. package/dist/src/core/schedule/slots.js +124 -0
  118. package/dist/src/core/schedule/slots.js.map +1 -0
  119. package/dist/src/core/schedule/tick.js +553 -0
  120. package/dist/src/core/schedule/tick.js.map +1 -0
  121. package/dist/src/core/schedule/token-fold.js +181 -0
  122. package/dist/src/core/schedule/token-fold.js.map +1 -0
  123. package/dist/src/core/state-resolve.js +86 -0
  124. package/dist/src/core/state-resolve.js.map +1 -0
  125. package/dist/src/core/vm-guards.js +278 -0
  126. package/dist/src/core/vm-guards.js.map +1 -0
  127. package/dist/src/core/workflow/derive.js +107 -0
  128. package/dist/src/core/workflow/derive.js.map +1 -0
  129. package/dist/src/core/workflow/parse.js +687 -0
  130. package/dist/src/core/workflow/parse.js.map +1 -0
  131. package/dist/src/core/workflow/prompt-probe.js +78 -0
  132. package/dist/src/core/workflow/prompt-probe.js.map +1 -0
  133. package/dist/src/core/workflow/validate.js +189 -0
  134. package/dist/src/core/workflow/validate.js.map +1 -0
  135. package/dist/src/core/workspace-key.js +19 -0
  136. package/dist/src/core/workspace-key.js.map +1 -0
  137. package/dist/src/shell/actions-runner.js +356 -0
  138. package/dist/src/shell/actions-runner.js.map +1 -0
  139. package/dist/src/shell/adapter/adapter-registry.js +45 -0
  140. package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
  141. package/dist/src/shell/adapter/clock-random.js +96 -0
  142. package/dist/src/shell/adapter/clock-random.js.map +1 -0
  143. package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
  144. package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
  145. package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
  146. package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
  147. package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
  148. package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
  149. package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
  150. package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
  151. package/dist/src/shell/adapter/launcher-asset.js +57 -0
  152. package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
  153. package/dist/src/shell/adapter/mise-config-asset.js +65 -0
  154. package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
  155. package/dist/src/shell/adapter/workflow-loader.js +304 -0
  156. package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
  157. package/dist/src/shell/cli/doctor.js +268 -0
  158. package/dist/src/shell/cli/doctor.js.map +1 -0
  159. package/dist/src/shell/effect-interpreter-families.js +314 -0
  160. package/dist/src/shell/effect-interpreter-families.js.map +1 -0
  161. package/dist/src/shell/effect-interpreter.js +29 -0
  162. package/dist/src/shell/effect-interpreter.js.map +1 -0
  163. package/dist/src/shell/interp/acp-frame.js +137 -0
  164. package/dist/src/shell/interp/acp-frame.js.map +1 -0
  165. package/dist/src/shell/interp/acp-ws-conn.js +320 -0
  166. package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
  167. package/dist/src/shell/interp/acp-ws-frames.js +159 -0
  168. package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
  169. package/dist/src/shell/interp/acp-ws.js +197 -0
  170. package/dist/src/shell/interp/acp-ws.js.map +1 -0
  171. package/dist/src/shell/interp/acp.js +319 -0
  172. package/dist/src/shell/interp/acp.js.map +1 -0
  173. package/dist/src/shell/interp/credential-defaults.js +128 -0
  174. package/dist/src/shell/interp/credential-defaults.js.map +1 -0
  175. package/dist/src/shell/interp/credential-hooks.js +149 -0
  176. package/dist/src/shell/interp/credential-hooks.js.map +1 -0
  177. package/dist/src/shell/interp/credential-registry.js +226 -0
  178. package/dist/src/shell/interp/credential-registry.js.map +1 -0
  179. package/dist/src/shell/interp/credential.js +103 -0
  180. package/dist/src/shell/interp/credential.js.map +1 -0
  181. package/dist/src/shell/interp/gh.js +163 -0
  182. package/dist/src/shell/interp/gh.js.map +1 -0
  183. package/dist/src/shell/interp/git.js +28 -0
  184. package/dist/src/shell/interp/git.js.map +1 -0
  185. package/dist/src/shell/interp/log.js +213 -0
  186. package/dist/src/shell/interp/log.js.map +1 -0
  187. package/dist/src/shell/interp/process.js +178 -0
  188. package/dist/src/shell/interp/process.js.map +1 -0
  189. package/dist/src/shell/interp/runlog.js +193 -0
  190. package/dist/src/shell/interp/runlog.js.map +1 -0
  191. package/dist/src/shell/interp/timer.js +64 -0
  192. package/dist/src/shell/interp/timer.js.map +1 -0
  193. package/dist/src/shell/interp/tracker-disk.js +99 -0
  194. package/dist/src/shell/interp/tracker-disk.js.map +1 -0
  195. package/dist/src/shell/interp/tracker-parse.js +71 -0
  196. package/dist/src/shell/interp/tracker-parse.js.map +1 -0
  197. package/dist/src/shell/interp/tracker-scan.js +238 -0
  198. package/dist/src/shell/interp/tracker-scan.js.map +1 -0
  199. package/dist/src/shell/interp/tracker-write.js +91 -0
  200. package/dist/src/shell/interp/tracker-write.js.map +1 -0
  201. package/dist/src/shell/interp/tracker.js +41 -0
  202. package/dist/src/shell/interp/tracker.js.map +1 -0
  203. package/dist/src/shell/interp/tty.js +48 -0
  204. package/dist/src/shell/interp/tty.js.map +1 -0
  205. package/dist/src/shell/interp/vm.js +199 -0
  206. package/dist/src/shell/interp/vm.js.map +1 -0
  207. package/dist/src/shell/interp/workspace.js +310 -0
  208. package/dist/src/shell/interp/workspace.js.map +1 -0
  209. package/dist/src/shell/main-acp.js +78 -0
  210. package/dist/src/shell/main-acp.js.map +1 -0
  211. package/dist/src/shell/main-adapters.js +222 -0
  212. package/dist/src/shell/main-adapters.js.map +1 -0
  213. package/dist/src/shell/main-credential.js +122 -0
  214. package/dist/src/shell/main-credential.js.map +1 -0
  215. package/dist/src/shell/main-doctor.js +22 -0
  216. package/dist/src/shell/main-doctor.js.map +1 -0
  217. package/dist/src/shell/main-entry.js +46 -0
  218. package/dist/src/shell/main-entry.js.map +1 -0
  219. package/dist/src/shell/main-http-csrf.js +45 -0
  220. package/dist/src/shell/main-http-csrf.js.map +1 -0
  221. package/dist/src/shell/main-http-handler.js +389 -0
  222. package/dist/src/shell/main-http-handler.js.map +1 -0
  223. package/dist/src/shell/main-http-mcp.js +122 -0
  224. package/dist/src/shell/main-http-mcp.js.map +1 -0
  225. package/dist/src/shell/main-http-views.js +253 -0
  226. package/dist/src/shell/main-http-views.js.map +1 -0
  227. package/dist/src/shell/main-http.js +76 -0
  228. package/dist/src/shell/main-http.js.map +1 -0
  229. package/dist/src/shell/main-loops.js +130 -0
  230. package/dist/src/shell/main-loops.js.map +1 -0
  231. package/dist/src/shell/main-mcp.js +129 -0
  232. package/dist/src/shell/main-mcp.js.map +1 -0
  233. package/dist/src/shell/main-orchestrator.js +120 -0
  234. package/dist/src/shell/main-orchestrator.js.map +1 -0
  235. package/dist/src/shell/main-preflight.js +43 -0
  236. package/dist/src/shell/main-preflight.js.map +1 -0
  237. package/dist/src/shell/main-reconcilers-helpers.js +244 -0
  238. package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
  239. package/dist/src/shell/main-reconcilers-pr.js +148 -0
  240. package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
  241. package/dist/src/shell/main-reconcilers.js +225 -0
  242. package/dist/src/shell/main-reconcilers.js.map +1 -0
  243. package/dist/src/shell/main-runner.js +355 -0
  244. package/dist/src/shell/main-runner.js.map +1 -0
  245. package/dist/src/shell/main-scaffold.js +116 -0
  246. package/dist/src/shell/main-scaffold.js.map +1 -0
  247. package/dist/src/shell/main-shutdown.js +115 -0
  248. package/dist/src/shell/main-shutdown.js.map +1 -0
  249. package/dist/src/shell/main-startup.js +48 -0
  250. package/dist/src/shell/main-startup.js.map +1 -0
  251. package/dist/src/shell/main-substrates.js +43 -0
  252. package/dist/src/shell/main-substrates.js.map +1 -0
  253. package/dist/src/shell/main.js +385 -0
  254. package/dist/src/shell/main.js.map +1 -0
  255. package/dist/src/shell/orchestrator-feedback.js +69 -0
  256. package/dist/src/shell/orchestrator-feedback.js.map +1 -0
  257. package/dist/src/shell/orchestrator-image.js +167 -0
  258. package/dist/src/shell/orchestrator-image.js.map +1 -0
  259. package/dist/src/shell/orchestrator-loop.js +468 -0
  260. package/dist/src/shell/orchestrator-loop.js.map +1 -0
  261. package/dist/src/shell/orchestrator-reconcile.js +36 -0
  262. package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
  263. package/dist/src/shell/reconciler-loop.js +228 -0
  264. package/dist/src/shell/reconciler-loop.js.map +1 -0
  265. package/dist/src/shell/runner-loop-turn.js +301 -0
  266. package/dist/src/shell/runner-loop-turn.js.map +1 -0
  267. package/dist/src/shell/runner-loop.js +338 -0
  268. package/dist/src/shell/runner-loop.js.map +1 -0
  269. package/dist/src/shell/server/http.js +208 -0
  270. package/dist/src/shell/server/http.js.map +1 -0
  271. package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
  272. package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
  273. package/dist/src/shell/server/mcp-runtime.js +99 -0
  274. package/dist/src/shell/server/mcp-runtime.js.map +1 -0
  275. package/dist/src/shell/workspace-key.js +14 -0
  276. package/dist/src/shell/workspace-key.js.map +1 -0
  277. package/dist/src/types/acp.js +8 -0
  278. package/dist/src/types/acp.js.map +1 -0
  279. package/dist/src/types/actions/plan.js +6 -0
  280. package/dist/src/types/actions/plan.js.map +1 -0
  281. package/dist/src/types/actions/predicates.js +6 -0
  282. package/dist/src/types/actions/predicates.js.map +1 -0
  283. package/dist/src/types/actions/run-fold.js +8 -0
  284. package/dist/src/types/actions/run-fold.js.map +1 -0
  285. package/dist/src/types/actions.js +7 -0
  286. package/dist/src/types/actions.js.map +1 -0
  287. package/dist/src/types/adapter/clock-random.js +4 -0
  288. package/dist/src/types/adapter/clock-random.js.map +1 -0
  289. package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
  290. package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
  291. package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
  292. package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
  293. package/dist/src/types/adapter/workflow-loader.js +4 -0
  294. package/dist/src/types/adapter/workflow-loader.js.map +1 -0
  295. package/dist/src/types/cli/args.js +8 -0
  296. package/dist/src/types/cli/args.js.map +1 -0
  297. package/dist/src/types/config.js +8 -0
  298. package/dist/src/types/config.js.map +1 -0
  299. package/dist/src/types/credential-interp.js +6 -0
  300. package/dist/src/types/credential-interp.js.map +1 -0
  301. package/dist/src/types/credentials.js +10 -0
  302. package/dist/src/types/credentials.js.map +1 -0
  303. package/dist/src/types/doctor.js +7 -0
  304. package/dist/src/types/doctor.js.map +1 -0
  305. package/dist/src/types/domain.js +7 -0
  306. package/dist/src/types/domain.js.map +1 -0
  307. package/dist/src/types/effect.js +15 -0
  308. package/dist/src/types/effect.js.map +1 -0
  309. package/dist/src/types/errors.js +39 -0
  310. package/dist/src/types/errors.js.map +1 -0
  311. package/dist/src/types/http/decisions.js +6 -0
  312. package/dist/src/types/http/decisions.js.map +1 -0
  313. package/dist/src/types/http/render.js +10 -0
  314. package/dist/src/types/http/render.js.map +1 -0
  315. package/dist/src/types/http/views.js +6 -0
  316. package/dist/src/types/http/views.js.map +1 -0
  317. package/dist/src/types/http.js +9 -0
  318. package/dist/src/types/http.js.map +1 -0
  319. package/dist/src/types/image/managed-image.js +7 -0
  320. package/dist/src/types/image/managed-image.js.map +1 -0
  321. package/dist/src/types/interp/effect-interpreter.js +8 -0
  322. package/dist/src/types/interp/effect-interpreter.js.map +1 -0
  323. package/dist/src/types/interp/tracker.js +7 -0
  324. package/dist/src/types/interp/tracker.js.map +1 -0
  325. package/dist/src/types/issue/file.js +6 -0
  326. package/dist/src/types/issue/file.js.map +1 -0
  327. package/dist/src/types/issue/parse.js +8 -0
  328. package/dist/src/types/issue/parse.js.map +1 -0
  329. package/dist/src/types/main-acp.js +13 -0
  330. package/dist/src/types/main-acp.js.map +1 -0
  331. package/dist/src/types/main-adapters.js +5 -0
  332. package/dist/src/types/main-adapters.js.map +1 -0
  333. package/dist/src/types/main-credential.js +21 -0
  334. package/dist/src/types/main-credential.js.map +1 -0
  335. package/dist/src/types/main-doctor.js +6 -0
  336. package/dist/src/types/main-doctor.js.map +1 -0
  337. package/dist/src/types/main-http-handler.js +12 -0
  338. package/dist/src/types/main-http-handler.js.map +1 -0
  339. package/dist/src/types/main-http.js +5 -0
  340. package/dist/src/types/main-http.js.map +1 -0
  341. package/dist/src/types/main-loops.js +5 -0
  342. package/dist/src/types/main-loops.js.map +1 -0
  343. package/dist/src/types/main-mcp.js +12 -0
  344. package/dist/src/types/main-mcp.js.map +1 -0
  345. package/dist/src/types/main-orchestrator.js +5 -0
  346. package/dist/src/types/main-orchestrator.js.map +1 -0
  347. package/dist/src/types/main-reconcilers.js +11 -0
  348. package/dist/src/types/main-reconcilers.js.map +1 -0
  349. package/dist/src/types/main-runner.js +13 -0
  350. package/dist/src/types/main-runner.js.map +1 -0
  351. package/dist/src/types/main-startup.js +5 -0
  352. package/dist/src/types/main-startup.js.map +1 -0
  353. package/dist/src/types/main-substrates.js +5 -0
  354. package/dist/src/types/main-substrates.js.map +1 -0
  355. package/dist/src/types/mcp/dispatch.js +4 -0
  356. package/dist/src/types/mcp/dispatch.js.map +1 -0
  357. package/dist/src/types/mcp/post-move.js +7 -0
  358. package/dist/src/types/mcp/post-move.js.map +1 -0
  359. package/dist/src/types/mcp.js +9 -0
  360. package/dist/src/types/mcp.js.map +1 -0
  361. package/dist/src/types/ports.js +12 -0
  362. package/dist/src/types/ports.js.map +1 -0
  363. package/dist/src/types/reconcile/image-decide.js +5 -0
  364. package/dist/src/types/reconcile/image-decide.js.map +1 -0
  365. package/dist/src/types/reconcile/ledger.js +7 -0
  366. package/dist/src/types/reconcile/ledger.js.map +1 -0
  367. package/dist/src/types/reconcile/pr-loop.js +8 -0
  368. package/dist/src/types/reconcile/pr-loop.js.map +1 -0
  369. package/dist/src/types/reconcile/vm-reap.js +8 -0
  370. package/dist/src/types/reconcile/vm-reap.js.map +1 -0
  371. package/dist/src/types/reconcile/workspace-decide.js +7 -0
  372. package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
  373. package/dist/src/types/reconcile.js +9 -0
  374. package/dist/src/types/reconcile.js.map +1 -0
  375. package/dist/src/types/runlog.js +7 -0
  376. package/dist/src/types/runlog.js.map +1 -0
  377. package/dist/src/types/runner/actions-runner.js +12 -0
  378. package/dist/src/types/runner/actions-runner.js.map +1 -0
  379. package/dist/src/types/runner/gondolin-dispatch.js +5 -0
  380. package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
  381. package/dist/src/types/runner/injection.js +6 -0
  382. package/dist/src/types/runner/injection.js.map +1 -0
  383. package/dist/src/types/runner/runner-loop.js +5 -0
  384. package/dist/src/types/runner/runner-loop.js.map +1 -0
  385. package/dist/src/types/runner/turn.js +4 -0
  386. package/dist/src/types/runner/turn.js.map +1 -0
  387. package/dist/src/types/runner/vm-plan.js +4 -0
  388. package/dist/src/types/runner/vm-plan.js.map +1 -0
  389. package/dist/src/types/runtime.js +9 -0
  390. package/dist/src/types/runtime.js.map +1 -0
  391. package/dist/src/types/schedule/admission.js +7 -0
  392. package/dist/src/types/schedule/admission.js.map +1 -0
  393. package/dist/src/types/schedule/circuit-breaker.js +2 -0
  394. package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
  395. package/dist/src/types/schedule/eligibility.js +9 -0
  396. package/dist/src/types/schedule/eligibility.js.map +1 -0
  397. package/dist/src/types/schedule/orchestrator-loop.js +10 -0
  398. package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
  399. package/dist/src/types/schedule/sleep-cycle.js +4 -0
  400. package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
  401. package/dist/src/types/schedule/slots.js +8 -0
  402. package/dist/src/types/schedule/slots.js.map +1 -0
  403. package/dist/src/types/schedule/tick.js +9 -0
  404. package/dist/src/types/schedule/tick.js.map +1 -0
  405. package/dist/src/types/server/mcp-runtime.js +8 -0
  406. package/dist/src/types/server/mcp-runtime.js.map +1 -0
  407. package/dist/src/types/workflow/parse.js +4 -0
  408. package/dist/src/types/workflow/parse.js.map +1 -0
  409. package/dist/tests/core/account-id.test.js +35 -0
  410. package/dist/tests/core/account-id.test.js.map +1 -0
  411. package/dist/tests/core/actions-parse.test.js +176 -0
  412. package/dist/tests/core/actions-parse.test.js.map +1 -0
  413. package/dist/tests/core/adapter-config.test.js +133 -0
  414. package/dist/tests/core/adapter-config.test.js.map +1 -0
  415. package/dist/tests/core/admission.test.js +215 -0
  416. package/dist/tests/core/admission.test.js.map +1 -0
  417. package/dist/tests/core/args.test.js +132 -0
  418. package/dist/tests/core/args.test.js.map +1 -0
  419. package/dist/tests/core/availability.test.js +62 -0
  420. package/dist/tests/core/availability.test.js.map +1 -0
  421. package/dist/tests/core/checks.test.js +395 -0
  422. package/dist/tests/core/checks.test.js.map +1 -0
  423. package/dist/tests/core/circuit-breaker.test.js +172 -0
  424. package/dist/tests/core/circuit-breaker.test.js.map +1 -0
  425. package/dist/tests/core/coerce.test.js +87 -0
  426. package/dist/tests/core/coerce.test.js.map +1 -0
  427. package/dist/tests/core/context.test.js +228 -0
  428. package/dist/tests/core/context.test.js.map +1 -0
  429. package/dist/tests/core/decisions.test.js +310 -0
  430. package/dist/tests/core/decisions.test.js.map +1 -0
  431. package/dist/tests/core/derive.test.js +205 -0
  432. package/dist/tests/core/derive.test.js.map +1 -0
  433. package/dist/tests/core/dispatch-config.test.js +164 -0
  434. package/dist/tests/core/dispatch-config.test.js.map +1 -0
  435. package/dist/tests/core/dispatch.test.js +302 -0
  436. package/dist/tests/core/dispatch.test.js.map +1 -0
  437. package/dist/tests/core/eligibility.test.js +163 -0
  438. package/dist/tests/core/eligibility.test.js.map +1 -0
  439. package/dist/tests/core/extract.test.js +139 -0
  440. package/dist/tests/core/extract.test.js.map +1 -0
  441. package/dist/tests/core/fake-creds.test.js +134 -0
  442. package/dist/tests/core/fake-creds.test.js.map +1 -0
  443. package/dist/tests/core/file.test.js +197 -0
  444. package/dist/tests/core/file.test.js.map +1 -0
  445. package/dist/tests/core/git-result.test.js +113 -0
  446. package/dist/tests/core/git-result.test.js.map +1 -0
  447. package/dist/tests/core/identity.test.js +180 -0
  448. package/dist/tests/core/identity.test.js.map +1 -0
  449. package/dist/tests/core/image-decide.test.js +59 -0
  450. package/dist/tests/core/image-decide.test.js.map +1 -0
  451. package/dist/tests/core/injection.test.js +163 -0
  452. package/dist/tests/core/injection.test.js.map +1 -0
  453. package/dist/tests/core/ledger.test.js +218 -0
  454. package/dist/tests/core/ledger.test.js.map +1 -0
  455. package/dist/tests/core/managed-image.test.js +68 -0
  456. package/dist/tests/core/managed-image.test.js.map +1 -0
  457. package/dist/tests/core/mise.test.js +138 -0
  458. package/dist/tests/core/mise.test.js.map +1 -0
  459. package/dist/tests/core/parse.test.js +174 -0
  460. package/dist/tests/core/parse.test.js.map +1 -0
  461. package/dist/tests/core/path.test.js +50 -0
  462. package/dist/tests/core/path.test.js.map +1 -0
  463. package/dist/tests/core/plan.test.js +218 -0
  464. package/dist/tests/core/plan.test.js.map +1 -0
  465. package/dist/tests/core/post-move.test.js +162 -0
  466. package/dist/tests/core/post-move.test.js.map +1 -0
  467. package/dist/tests/core/pr-classify.test.js +117 -0
  468. package/dist/tests/core/pr-classify.test.js.map +1 -0
  469. package/dist/tests/core/pr-decide.test.js +298 -0
  470. package/dist/tests/core/pr-decide.test.js.map +1 -0
  471. package/dist/tests/core/pr-loop.test.js +301 -0
  472. package/dist/tests/core/pr-loop.test.js.map +1 -0
  473. package/dist/tests/core/pr-notes.test.js +165 -0
  474. package/dist/tests/core/pr-notes.test.js.map +1 -0
  475. package/dist/tests/core/predicates.test.js +154 -0
  476. package/dist/tests/core/predicates.test.js.map +1 -0
  477. package/dist/tests/core/prompt.test.js +189 -0
  478. package/dist/tests/core/prompt.test.js.map +1 -0
  479. package/dist/tests/core/protocol.test.js +195 -0
  480. package/dist/tests/core/protocol.test.js.map +1 -0
  481. package/dist/tests/core/reconcile-issue.test.js +116 -0
  482. package/dist/tests/core/reconcile-issue.test.js.map +1 -0
  483. package/dist/tests/core/render.test.js +549 -0
  484. package/dist/tests/core/render.test.js.map +1 -0
  485. package/dist/tests/core/retry.test.js +186 -0
  486. package/dist/tests/core/retry.test.js.map +1 -0
  487. package/dist/tests/core/routes.test.js +247 -0
  488. package/dist/tests/core/routes.test.js.map +1 -0
  489. package/dist/tests/core/run-fold.test.js +299 -0
  490. package/dist/tests/core/run-fold.test.js.map +1 -0
  491. package/dist/tests/core/shape.test.js +185 -0
  492. package/dist/tests/core/shape.test.js.map +1 -0
  493. package/dist/tests/core/sleep-cycle.test.js +150 -0
  494. package/dist/tests/core/sleep-cycle.test.js.map +1 -0
  495. package/dist/tests/core/slots.test.js +201 -0
  496. package/dist/tests/core/slots.test.js.map +1 -0
  497. package/dist/tests/core/state-resolve.test.js +80 -0
  498. package/dist/tests/core/state-resolve.test.js.map +1 -0
  499. package/dist/tests/core/summary.test.js +200 -0
  500. package/dist/tests/core/summary.test.js.map +1 -0
  501. package/dist/tests/core/template.test.js +116 -0
  502. package/dist/tests/core/template.test.js.map +1 -0
  503. package/dist/tests/core/tick.test.js +558 -0
  504. package/dist/tests/core/tick.test.js.map +1 -0
  505. package/dist/tests/core/token-fold.test.js +176 -0
  506. package/dist/tests/core/token-fold.test.js.map +1 -0
  507. package/dist/tests/core/turn.test.js +388 -0
  508. package/dist/tests/core/turn.test.js.map +1 -0
  509. package/dist/tests/core/url.test.js +118 -0
  510. package/dist/tests/core/url.test.js.map +1 -0
  511. package/dist/tests/core/validate.test.js +247 -0
  512. package/dist/tests/core/validate.test.js.map +1 -0
  513. package/dist/tests/core/views.test.js +252 -0
  514. package/dist/tests/core/views.test.js.map +1 -0
  515. package/dist/tests/core/vm-decide.test.js +110 -0
  516. package/dist/tests/core/vm-decide.test.js.map +1 -0
  517. package/dist/tests/core/vm-guards.test.js +153 -0
  518. package/dist/tests/core/vm-guards.test.js.map +1 -0
  519. package/dist/tests/core/vm-plan.test.js +332 -0
  520. package/dist/tests/core/vm-plan.test.js.map +1 -0
  521. package/dist/tests/core/vm-reap.test.js +196 -0
  522. package/dist/tests/core/vm-reap.test.js.map +1 -0
  523. package/dist/tests/core/workflow-parse.test.js +493 -0
  524. package/dist/tests/core/workflow-parse.test.js.map +1 -0
  525. package/dist/tests/core/workspace-decide.test.js +236 -0
  526. package/dist/tests/core/workspace-decide.test.js.map +1 -0
  527. package/dist/tests/helpers/fixtures.js +167 -0
  528. package/dist/tests/helpers/fixtures.js.map +1 -0
  529. package/dist/tests/shell/acp-substrate.test.js +101 -0
  530. package/dist/tests/shell/acp-substrate.test.js.map +1 -0
  531. package/dist/tests/shell/actions-runner-push.test.js +203 -0
  532. package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
  533. package/dist/tests/shell/credential-hooks.test.js +36 -0
  534. package/dist/tests/shell/credential-hooks.test.js.map +1 -0
  535. package/dist/tests/shell/credential-registry.test.js +165 -0
  536. package/dist/tests/shell/credential-registry.test.js.map +1 -0
  537. package/dist/tests/shell/credential-substrate.test.js +179 -0
  538. package/dist/tests/shell/credential-substrate.test.js.map +1 -0
  539. package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
  540. package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
  541. package/dist/tests/shell/doctor.test.js +101 -0
  542. package/dist/tests/shell/doctor.test.js.map +1 -0
  543. package/dist/tests/shell/effect-vm-create.test.js +52 -0
  544. package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
  545. package/dist/tests/shell/gh-port.test.js +63 -0
  546. package/dist/tests/shell/gh-port.test.js.map +1 -0
  547. package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
  548. package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
  549. package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
  550. package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
  551. package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
  552. package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
  553. package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
  554. package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
  555. package/dist/tests/shell/http-handler.test.js +608 -0
  556. package/dist/tests/shell/http-handler.test.js.map +1 -0
  557. package/dist/tests/shell/http-server.test.js +53 -0
  558. package/dist/tests/shell/http-server.test.js.map +1 -0
  559. package/dist/tests/shell/mcp-runtime.test.js +366 -0
  560. package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
  561. package/dist/tests/shell/mise-config-asset.test.js +87 -0
  562. package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
  563. package/dist/tests/shell/orchestrator-loop.test.js +583 -0
  564. package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
  565. package/dist/tests/shell/reconciler-passes.test.js +314 -0
  566. package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
  567. package/dist/tests/shell/runner-loop-turn.test.js +97 -0
  568. package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
  569. package/dist/tests/shell/runner-slice.test.js +536 -0
  570. package/dist/tests/shell/runner-slice.test.js.map +1 -0
  571. package/dist/tests/shell/scaffold.test.js +65 -0
  572. package/dist/tests/shell/scaffold.test.js.map +1 -0
  573. package/dist/tests/shell/tick-config.test.js +83 -0
  574. package/dist/tests/shell/tick-config.test.js.map +1 -0
  575. package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
  576. package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
  577. package/dist/tests/shell/tracker-write-issue.test.js +154 -0
  578. package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
  579. package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
  580. package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
  581. package/dist/tests/shell/workspace-live-config.test.js +140 -0
  582. package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
  583. package/package.json +31 -11
  584. package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
  585. package/prompts/Reflect.md +91 -0
  586. package/prompts/Review.md +97 -0
  587. package/prompts/Todo.md +96 -0
  588. package/prompts/_footer.md +41 -0
  589. package/prompts/_preamble.md +42 -0
  590. package/prompts-minimal/Todo.md +26 -0
  591. package/scripts/postinstall.mjs +63 -0
  592. package/scripts/vm-agent.mjs +433 -0
  593. package/WORKFLOW.md +0 -269
  594. package/WORKFLOW.template.md +0 -307
  595. package/dist/agent/acp.js +0 -304
  596. package/dist/agent/acp.js.map +0 -1
  597. package/dist/agent/adapters.js +0 -275
  598. package/dist/agent/adapters.js.map +0 -1
  599. package/dist/agent/codex.js +0 -439
  600. package/dist/agent/codex.js.map +0 -1
  601. package/dist/agent/runner.js +0 -394
  602. package/dist/agent/runner.js.map +0 -1
  603. package/dist/agent/smolvm.js +0 -174
  604. package/dist/agent/smolvm.js.map +0 -1
  605. package/dist/bin/symphony.js +0 -205
  606. package/dist/bin/symphony.js.map +0 -1
  607. package/dist/http.js +0 -1189
  608. package/dist/http.js.map +0 -1
  609. package/dist/logging.js +0 -45
  610. package/dist/logging.js.map +0 -1
  611. package/dist/mcp.js +0 -478
  612. package/dist/mcp.js.map +0 -1
  613. package/dist/orchestrator.js +0 -683
  614. package/dist/orchestrator.js.map +0 -1
  615. package/dist/prompt.js +0 -65
  616. package/dist/prompt.js.map +0 -1
  617. package/dist/trackers/local.js +0 -344
  618. package/dist/trackers/local.js.map +0 -1
  619. package/dist/trackers/types.js +0 -10
  620. package/dist/trackers/types.js.map +0 -1
  621. package/dist/types.js +0 -3
  622. package/dist/types.js.map +0 -1
  623. package/dist/workflow.js +0 -385
  624. package/dist/workflow.js.map +0 -1
  625. package/dist/workspace.js +0 -196
  626. package/dist/workspace.js.map +0 -1
  627. package/scripts/build-vm.sh +0 -67
@@ -0,0 +1,1100 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # WORKFLOW.template.yaml — annotated reference for symphony workflow files.
3
+ #
4
+ # A workflow file is pure YAML config. The orchestrator parses it into
5
+ # ServiceConfig (see src/types/config.ts) and, per dispatched issue, renders the
6
+ # active state's prompt — its `prompt_file` wrapped in the shared
7
+ # `prompt.preamble_file` / `prompt.footer_file` — with the Liquid context
8
+ # `{ issue, attempt }`.
9
+ #
10
+ # This document lists every supported section, every option within it, the
11
+ # parser default, and a small example. For a complete worked example, see
12
+ # WORKFLOW.yaml in this repo. For the smallest thing that runs — what
13
+ # `npx smol-symphony` scaffolds by default — see WORKFLOW.minimal.yaml; reach for
14
+ # the options below only when you want a power feature it omits.
15
+ #
16
+ # Notation:
17
+ # • Required keys are marked (required).
18
+ # • Types: `string`, `int`, `bool`, `path` (string resolved relative to the
19
+ # workflow file's directory unless absolute), `string[]`, `map<K, V>`.
20
+ # • Defaults are what the parser writes when the key is absent.
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ # ─────────────────────────────────────────────────────────────────────────────
23
+ # tracker — where issues come from.
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ tracker:
26
+ # kind (required): currently the only supported value is 'local' (markdown
27
+ # files under `root`, one per issue, organized by state subdirectory).
28
+ kind: local
29
+
30
+ # root (path): directory containing `<state>/<id>.md` files. Optional.
31
+ # An explicit value resolves absolute as-is, `~`-expanded against $HOME, or
32
+ # (relative) against the workflow file's directory.
33
+ # Default (unset): ~/.symphony/trackers/<project> — durable tracker state lives
34
+ # OUTSIDE the repo working tree by default, so it survives a delete / re-clone
35
+ # of the repo. <project> is the `workspace.github_repo` slug's repo name
36
+ # (owner/repo → repo), falling back to this workflow file's directory basename;
37
+ # sanitized so two different projects on one machine never collide.
38
+ # root: ~/.symphony/trackers/my-project
39
+
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ # states — per-state configuration map. REQUIRED. Every workflow must declare
42
+ # at least one `active`, one `terminal`, and one `holding` state; a workflow
43
+ # missing the `states:` block (or missing any of those roles) is rejected at
44
+ # parse time. This map is the only place state names and roles are configured;
45
+ # there are no separate active/terminal lists to keep in sync.
46
+ #
47
+ # Keys are state names; values are config objects with these fields:
48
+ # role (required, enum):
49
+ # active — orchestrator dispatches issues in this state.
50
+ # terminal — orchestrator treats issues in this state as complete; the
51
+ # workspace is removed after the run unwinds.
52
+ # holding — directory exists on disk, but the orchestrator never
53
+ # dispatches issues from it. Triage is the canonical example
54
+ # and the landing directory for `symphony.propose_issue`.
55
+ # adapter (string, optional): override the workflow-level `acp.adapter` for
56
+ # agents dispatched in this state. Must be a known profile (claude,
57
+ # codex). Both use host-side credential substitution at Gondolin
58
+ # egress and are startup-probed so a missing credential fails fast.
59
+ # claude has a single host credential file
60
+ # (~/.claude/.credentials.json) that is probed for readability; codex
61
+ # passes when either ~/.codex/auth.json holds a token (ChatGPT-OAuth
62
+ # tokens.access_token or a top-level OPENAI_API_KEY) or the host
63
+ # OPENAI_API_KEY env var is set.
64
+ # prompt_file (path): this state's prompt template, in its own file. The shell
65
+ # loader reads it and assembles `prompt.preamble_file` + this file +
66
+ # `prompt.footer_file` (see the top-level `prompt:` block below) into
67
+ # the template rendered for issues in this state. Every active
68
+ # (dispatched) state should declare one; a state without `prompt_file`
69
+ # renders the generic fallback prompt. Resolved relative to the
70
+ # workflow file. A missing/unparseable file fails at startup (and
71
+ # `symphony doctor`), not mid-dispatch.
72
+ # model (string, optional): override `acp.model` for this state.
73
+ # Blank or whitespace-only values normalize to "use the adapter
74
+ # default" (same as the workflow-level acp.model semantics).
75
+ # effort (string, optional): override `acp.effort` for this state. Same
76
+ # undefined-vs-null semantics as `model`: omit to inherit
77
+ # `acp.effort`; blank/whitespace normalizes to null ("use the
78
+ # adapter default for this state"). Valid values are adapter- and
79
+ # model-specific (see `acp.effort`).
80
+ # max_turns (int, optional): override `agent.max_turns` for this state.
81
+ # max_concurrent (int, optional): cap on agents the orchestrator runs
82
+ # simultaneously for issues in THIS state. Symmetric with
83
+ # `max_turns` — concurrency and turn budget both live on the state.
84
+ # Omit for "no per-state cap; only the global
85
+ # `agent.max_concurrent_agents` ceiling applies". The sum of every
86
+ # state's `max_concurrent` must not exceed that global ceiling
87
+ # (validated at startup).
88
+ # allowed_transitions (string[]|null, optional): when set, restricts which
89
+ # states agents in this state may transition to via the MCP
90
+ # `transition` tool. Each entry must be a declared state. Omit (or
91
+ # explicitly set to null) for "any declared state is reachable".
92
+ # An empty list (`allowed_transitions: []`) means "no transitions
93
+ # allowed out of this state" — the agent's `transition` calls will
94
+ # always be rejected with `transition_not_allowed`. Useful for
95
+ # review-style states that should pause until a human re-routes.
96
+ # eval_mode (bool, optional): when true, the runner adds two extra read-only
97
+ # bind mounts to every per-issue VM dispatched in this state so an
98
+ # in-VM agent can inspect symphony's own state for evaluation /
99
+ # debugging:
100
+ # • `tracker.root` → `/symphony/issues` (every issue file across
101
+ # every state directory)
102
+ # • `logs.root` → `/symphony/logs` (per-issue JSONL run-log
103
+ # transcripts captured by RunLog — ACP frames, stderr,
104
+ # typed-action output, system events — plus the compact
105
+ # `<key>.summary.json` outcome records the reflector reads; see
106
+ # the `logs:` block below)
107
+ # Either mount is skipped silently if the corresponding root is
108
+ # unset. Each VFS mount has a cost, so this is opt-in per state rather
109
+ # than a workflow-wide default — flip it on for a dedicated eval
110
+ # state, not for the routine implement/review flow. Default: false.
111
+ # The canonical consumer is the "sleep cycle" reflection pattern —
112
+ # see the SLEEP CYCLE section below the states block.
113
+ # actions (list, optional): typed action DAG (issue 36, reconciler v2).
114
+ # The single glue mechanism attached to a state — there is no shell
115
+ # `hooks:` surface. When set on a `terminal` state, this list runs
116
+ # on transition INTO the state (the canonical Done-state pair is
117
+ # push_branch + create_pr_if_missing). For arbitrary in-sandbox
118
+ # commands use a `run_in_vm` action; per-VM tooling belongs in the
119
+ # agent image (`images/agents/`); first-creation workspace setup is
120
+ # owned by the orchestrator's `setupWorkspaceDir`.
121
+ # Each entry is a closed-kind record:
122
+ #
123
+ # - kind: push_branch
124
+ # remote: origin
125
+ # ref: $branch
126
+ # if: $repo
127
+ #
128
+ # Recognized kinds:
129
+ # push_branch { remote, ref }
130
+ # create_pr_if_missing { base, head, title_from, body_from }
131
+ # run_in_vm { name, cmd: [...], env?, timeout? }
132
+ #
133
+ # Templating: `$varname` references the fixed ActionContext
134
+ # namespace ($identifier, $workspace, $branch, $base_branch,
135
+ # $issue_title, $issue_body, $repo, $pr_title, $pr_body_file).
136
+ # Unknown $vars throw at run-time (no silent "" expansion).
137
+ #
138
+ # Conditional: optional `if:` field supports three predicates
139
+ # - `if: $repo` (env-var-truthy)
140
+ # - `if: { branch_exists: <ref> }` (workspace branch)
141
+ # - `if: { file_present: <path> }` (workspace file)
142
+ #
143
+ # Retry: optional `on_error.retry: { count, backoff_ms }`. Default
144
+ # policy is 3 retries with exponential backoff starting at 1s,
145
+ # then abort. `on_error.then: { route_to: <state> }` reroutes the
146
+ # issue OUT of the action's state into <state> (which must be
147
+ # declared — validated at parse time) instead of aborting; the
148
+ # runner moves the tracker file there with the failure reason
149
+ # appended as notes. The canonical use is the Done handoff (issue
150
+ # 235): a `push_branch` / `create_pr_if_missing` that fails after
151
+ # its retries reroutes into a `HandoffFailed` holding state so a
152
+ # stranded handoff (no branch on the remote, no PR) is operator-
153
+ # visible instead of dead-ending silently in terminal Done. A
154
+ # rerouted handoff also RETAINS the per-issue workspace (the
155
+ # handoff-pending marker, issue 234) so the unpushed commit
156
+ # survives for a manual push. A `.github/workflows/`-touching push
157
+ # is the common trigger: the shared fine-grained PAT is not granted
158
+ # Workflows:write, so GitHub server-side-declines it and the routed
159
+ # reason is rewritten to an actionable "manual SSH push required".
160
+ #
161
+ # run_in_vm runs uncached: every dispatch into the state re-execs
162
+ # the command on the live per-issue VM. `name` labels its run-log
163
+ # output (no caching layer; the action is the plain escape hatch).
164
+ # pr (map, optional): PR autopilot routing for this state (issue 139).
165
+ # Valid only on a `terminal` state, and only acts when the
166
+ # top-level `pr:` engine block (below) has `enabled: true`. This is
167
+ # where the routing the engine used to name by string now lives —
168
+ # on the state it describes. Two shapes:
169
+ # • Merge state: pr: { auto_merge: squash|merge|rebase,
170
+ # on_conflict: { route_to: <active state> } }
171
+ # A MERGEABLE PR for an issue in this state has GitHub
172
+ # auto-merge armed with `auto_merge`; a CONFLICTING one is
173
+ # routed back to `on_conflict.route_to` (defaults to the first
174
+ # declared active state when omitted) for the dispatched agent
175
+ # to rebase. While the engine is enabled, transitions INTO this
176
+ # state do not fire the standard terminal workspace cleanup —
177
+ # the pr resource owns the workspace until the PR merges/closes.
178
+ # • Close state: pr: { close: true }
179
+ # An open PR for an issue in this state is closed without merge
180
+ # and its remote branch is best-effort-deleted. Standard
181
+ # terminal cleanup still runs (the close path needs no workspace).
182
+ # The merge/close/route targets are DERIVED by scanning states for
183
+ # this field — there is no named-string sibling block. At most one
184
+ # terminal state may declare `auto_merge`, and at most one may
185
+ # declare `close`; an `on_conflict.route_to` naming an undeclared
186
+ # state is rejected at parse time.
187
+ # spawn (map, optional): recurring-spawn trigger for this state (the sleep
188
+ # cycle as a recurring STATE). Valid only on an `active` state. When
189
+ # a trigger fires the orchestrator MINTS a FRESH ephemeral issue into
190
+ # this state (it runs once and terminates in a dedicated terminal),
191
+ # instead of re-arming one immortal parked ticket. Fields:
192
+ # • on_idle (bool): spawn when the orchestrator is idle AND >=1
193
+ # issue reached a terminal state since the last cycle (the ">=1
194
+ # since last cycle" gate is load-bearing — without it an idle
195
+ # orchestrator re-spawns in a tight loop with nothing new to
196
+ # mine). Default false.
197
+ # • after_terminal (int): a backstop for busy stretches that never
198
+ # go idle — spawn once this many issues have reached a terminal
199
+ # state (Done/Cancelled/the spawn's terminal) since the last
200
+ # cycle. 0 disables the count trigger. Default 0.
201
+ # • title (string): title template for the minted issue;
202
+ # `{{ stamp }}` → the mint timestamp (e.g. "2026-06-03 12:00").
203
+ # Default "Reflection {{ stamp }}".
204
+ # • body (string, optional): lead-in line for the GENERATED
205
+ # provenance body (the trigger + counter + timestamp the static
206
+ # prompt can't know are always appended). Default: a generic
207
+ # "Auto-spawned reflection cycle." lead-in.
208
+ # • max_in_flight (int): never mint a second issue while this many
209
+ # are already live in the spawn state. Load-bearing — it replaces
210
+ # the immortal ticket's built-in "one location = one reflection"
211
+ # mutex, so a busy-stretch after_terminal trigger can't pile up
212
+ # cycles. Default 1.
213
+ # The terminal-transition counter resets to 0 on a successful mint
214
+ # and is held in orchestrator memory only (a restart resets it). At
215
+ # most one active state may declare `spawn` (so the spawn state is
216
+ # unambiguous). The spawn's terminal must be a DEDICATED terminal
217
+ # with no `pr:` / `actions:` — a minted issue runs on an empty branch,
218
+ # so a Done-style PR autopilot would try to open a PR off nothing.
219
+ # See the SLEEP CYCLE section below for the full pattern.
220
+ #
221
+ # Declaration order matters: role-filtered listings (active states, terminal
222
+ # states) follow it, and the dashboard renders state columns in the same order.
223
+ # ─────────────────────────────────────────────────────────────────────────────
224
+ states:
225
+ Todo:
226
+ role: active
227
+ prompt_file: prompts/Todo.md # this state's prompt template (see `prompt:` below)
228
+ adapter: claude
229
+ model: claude-opus-4-7
230
+ effort: xhigh
231
+ max_turns: 10
232
+ max_concurrent: 1 # at most one implementer agent at a time
233
+ Review:
234
+ role: active
235
+ prompt_file: prompts/Review.md
236
+ adapter: codex
237
+ model: gpt-5-codex
238
+ max_turns: 4
239
+ allowed_transitions: [Todo, Done]
240
+ Done:
241
+ role: terminal
242
+ # PR autopilot merge state (issue 139). Acts only when the top-level `pr:`
243
+ # engine block (below) is enabled. Derived as the merge state because it
244
+ # declares `auto_merge`.
245
+ # pr:
246
+ # auto_merge: squash
247
+ # on_conflict: { route_to: Todo }
248
+ # Terminal-state handoff via typed actions (issue 36). On transition into
249
+ # Done, push the branch (if SYMPHONY_REPO is set → $repo non-empty) and
250
+ # open a PR if one does not already exist. Templates resolve against the
251
+ # fixed ActionContext namespace; the orchestrator stages $pr_title and
252
+ # $pr_body_file from the issue file before firing.
253
+ # A handoff that fails after its retries reroutes the issue OUT of Done into
254
+ # the HandoffFailed holding state (issue 235) via `on_error.then.route_to`,
255
+ # so a stranded handoff is operator-visible instead of silently terminal-Done.
256
+ # actions:
257
+ # - kind: push_branch
258
+ # remote: origin
259
+ # ref: $branch
260
+ # if: $repo
261
+ # on_error: { then: { route_to: HandoffFailed } }
262
+ # - kind: create_pr_if_missing
263
+ # base: $base_branch
264
+ # head: $branch
265
+ # title_from: $pr_title
266
+ # body_from: $pr_body_file
267
+ # if: $repo
268
+ # on_error: { then: { route_to: HandoffFailed } }
269
+ Cancelled:
270
+ role: terminal
271
+ # PR autopilot close state (issue 139). Derived as the close state because
272
+ # it declares `close: true` — an open PR is closed without merge.
273
+ # pr:
274
+ # close: true
275
+ Triage:
276
+ role: holding
277
+ HandoffFailed:
278
+ # Landing state for a Done handoff that FAILED (issue 235): the failing
279
+ # action's `on_error.then.route_to` (above) reroutes the issue here with the
280
+ # reason appended. Holding → never dispatched; it parks for an operator to
281
+ # push the branch by hand (the workspace is retained by issue 234's marker)
282
+ # and move the issue back to Done, or to Cancelled. Declared AFTER Triage so
283
+ # Triage stays the `propose_issue` landing (the first declared holding state).
284
+ role: holding
285
+
286
+ # ─────────────────────────────────────────────────────────────────────────────
287
+ # prompt — shared prompt-assembly files (per-state prompt split). OPTIONAL.
288
+ #
289
+ # When an active state declares a `prompt_file` (see the states block above), the
290
+ # shell loader builds that state's prompt by concatenating, in order:
291
+ #
292
+ # prompt.preamble_file (shared header)
293
+ # + the state's prompt_file (the state-specific body)
294
+ # + prompt.footer_file (shared footer)
295
+ #
296
+ # and renders the result — so the config reads as the shape of the workflow and
297
+ # each state's prompt lives in its own file under `prompts/`. The Liquid context
298
+ # is `{ issue, attempt }`; each file is an ordinary Liquid template fragment.
299
+ #
300
+ # Both keys are paths resolved relative to the workflow file, and both are
301
+ # optional: omit `preamble_file`/`footer_file` to wrap a state body in nothing.
302
+ # A workflow file is pure YAML config — there is no inline prompt body. Every
303
+ # active state names its own `prompt_file`; a state without one renders the
304
+ # generic fallback prompt, so give each dispatched state a `prompt_file`. See
305
+ # WORKFLOW.minimal.yaml for the smallest example and WORKFLOW.yaml in this repo
306
+ # for a full worked example.
307
+ #
308
+ # prompt:
309
+ # preamble_file: prompts/_preamble.md
310
+ # footer_file: prompts/_footer.md
311
+ # ─────────────────────────────────────────────────────────────────────────────
312
+
313
+ # ─────────────────────────────────────────────────────────────────────────────
314
+ # SLEEP CYCLE — a recurring reflection STATE that mines finished work for harness
315
+ # improvements (issue 122). Optional, opt-in pattern; layered on top of the
316
+ # states block above. The shipped smol-symphony WORKFLOW.yaml wires it for the
317
+ # dogfooding (symphony-on-symphony) setup.
318
+ #
319
+ # The idea: every dispatch starts from the same static prompt + config, no
320
+ # matter what the last 100 issues taught us about where agents stall, get
321
+ # rejected, burn their turn budget, or fight the harness. A periodic
322
+ # "reflection" turn closes that feedback loop — it reads completed-task history
323
+ # (the read-only mounts `eval_mode` exposes), distils *recurring* friction, and
324
+ # files improvement proposals against the HARNESS (this WORKFLOW.yaml's prompt
325
+ # branches and per-state model/max_turns/allowed_transitions/effort/actions, the
326
+ # gondolin image config, acceptance criteria, timeouts) — never the product code under
327
+ # review. Proposals land in Triage via `propose_issue`, so a human stays the
328
+ # gate. This is the "self-improving agent" pattern aimed at the harness rather
329
+ # than the product.
330
+ #
331
+ # SHAPE: recurring STATE, NOT an immortal ticket. The cycle is modelled as a
332
+ # recurring state that SPAWNS ephemeral reflection issues. There is no parked
333
+ # "Sleep cycle" ticket that ping-pongs between two states forever: when a trigger
334
+ # fires the orchestrator MINTS a fresh issue into the spawn state; it runs once
335
+ # and terminates in a dedicated terminal, like every other ticket. Two states
336
+ # implement it:
337
+ #
338
+ # Reflect (role: active, eval_mode: true, spawn: {...}):
339
+ # - eval_mode binds /symphony/issues (all state dirs, incl. the Done/*.md
340
+ # handoff transcripts) + /symphony/logs (per-issue JSONL run logs)
341
+ # read-only into the VM. No extra mount plumbing — it reuses the existing
342
+ # eval_mode mounts.
343
+ # - Give it a capable adapter/model (large context helps: a reflection turn
344
+ # reads many transcripts + logs) and a higher max_turns than your
345
+ # implement/review states.
346
+ # - allowed_transitions: [Reflected] — the reflector may ONLY terminate in
347
+ # its dedicated terminal. It cannot route itself into the
348
+ # implement/review/done flow. Filing improvements goes through
349
+ # propose_issue (→ Triage), which is independent of allowed_transitions.
350
+ # - The `spawn:` block (see the states-field docs above) declares the
351
+ # triggers (on_idle / after_terminal), the minted-issue title template, and
352
+ # max_in_flight. This is what makes the cycle recurring — no cron needed.
353
+ # - The Reflect state's `prompt_file` encodes the
354
+ # read → distil → propose loop and the GUARDRAILS below.
355
+ #
356
+ # Reflected (role: terminal):
357
+ # - DEDICATED terminal for a completed reflection — NOT Done. Done carries the
358
+ # PR autopilot `actions:` (push_branch + create_pr_if_missing); a minted
359
+ # reflection runs on an empty branch (it only files propose_issue), so
360
+ # terminating in Done would try to open a PR off nothing. So `Reflected`
361
+ # declares NO `pr:` and NO `actions:`. Each cycle lands here as a browsable,
362
+ # per-run audit trail — one issue per cycle — instead of one ever-growing
363
+ # immortal ticket body.
364
+ #
365
+ # GUARDRAILS (this is a self-modifying loop — keep the human in it):
366
+ # - Output is proposals into Triage (holding, never auto-dispatched). The
367
+ # operator approves/discards. Do not bypass this gate.
368
+ # - Constrain the proposal surface to harness config. Forbid any proposal that
369
+ # weakens the Review state, the test/lint gates, or the Triage gate itself.
370
+ # - Each proposal must cite the issue ids that motivated it, so the operator
371
+ # checks the lesson against the evidence rather than the reflector's summary.
372
+ #
373
+ # CADENCE: the orchestrator auto-spawns a fresh reflection issue from the
374
+ # Reflect state's `spawn:` block — `on_idle` (idle with >=1 issue finished since
375
+ # the last cycle) or `after_terminal` (a backstop once N issues have reached a
376
+ # terminal state since the last cycle). The counter resets on a successful mint;
377
+ # `max_in_flight: 1` prevents a second cycle while one is live. An operator can
378
+ # also mint a cycle by hand by creating a Reflect issue (a real dashboard
379
+ # "reflect now" button or a `symphony reflect` verb is a natural fit) — a clean
380
+ # replacement for the old "re-arm by mv/cron" path.
381
+ #
382
+ # Example states to add (names are yours to choose):
383
+ #
384
+ # states:
385
+ # # ... your active/terminal states ...
386
+ # Reflect:
387
+ # role: active
388
+ # adapter: claude
389
+ # model: claude-opus-4-8[1m] # large context for reading transcripts
390
+ # max_turns: 20 # higher than implement/review
391
+ # eval_mode: true
392
+ # allowed_transitions: [Reflected]
393
+ # spawn:
394
+ # on_idle: true
395
+ # after_terminal: 10 # backstop for busy stretches (0 disables)
396
+ # title: "Reflection {{ stamp }}"
397
+ # max_in_flight: 1 # never two reflections live at once
398
+ # Triage:
399
+ # role: holding
400
+ # Reflected:
401
+ # role: terminal # dedicated terminal — NO pr:/actions:
402
+ # ─────────────────────────────────────────────────────────────────────────────
403
+
404
+ # ─────────────────────────────────────────────────────────────────────────────
405
+ # pr — PR autopilot engine toggle: arm GitHub auto-merge when a terminal-state
406
+ # PR is mergeable; route non-mergeable PRs back to the implementing state.
407
+ #
408
+ # Optional. This is the SLIM host-global half only — the on/off switch and the
409
+ # per-PR `gh pr view` cache TTL. The merge/close/route targets and the
410
+ # auto-merge strategy live ON the terminal states they describe, as
411
+ # `states.<name>.pr` (see the `pr:` field doc in the states block above):
412
+ # • the MERGE state declares `pr: { auto_merge: <strategy>,
413
+ # on_conflict: { route_to: <active state> } }`
414
+ # • the CLOSE state declares `pr: { close: true }`
415
+ # The reconciler derives the targets by scanning states for that field — there
416
+ # is no named-string sibling block here.
417
+ #
418
+ # When `enabled: true` the reconciler grows a `pr` resource that, on every tick,
419
+ # looks up each issue in the merge state via `gh pr list --head agent/<id>`,
420
+ # fetches its detail with `gh pr view`, and:
421
+ #
422
+ # • Arms GitHub's auto-merge when the PR is `mergeable: MERGEABLE`
423
+ # (`gh pr merge --auto --<strategy> --delete-branch`, where `<strategy>` is
424
+ # the merge state's `pr.auto_merge`). GitHub merges as soon as required
425
+ # checks pass and review requirements are satisfied.
426
+ # • When the PR is `mergeable: CONFLICTING`, appends a structured notes block
427
+ # to the issue file and routes the issue from the merge state back to that
428
+ # state's `pr.on_conflict.route_to` (default: the first declared
429
+ # `role: active` state). The workspace + `agent/<id>` branch are preserved.
430
+ # Before the next dispatch symphony runs `git fetch origin <base>` so
431
+ # `origin/<base>` is current, and the Todo prompt's first step is
432
+ # `git rebase origin/<base>` — resolving the conflict is the agent's normal
433
+ # flow, not an out-of-band autopilot operation.
434
+ # • For an issue in the close state with an open PR, closes the PR without
435
+ # merge and best-effort-deletes the remote branch.
436
+ #
437
+ # Requires `gh` authenticated on the host (`gh auth status` clean). The token
438
+ # never enters the VM. Auto-merge ALSO requires at least one branch protection
439
+ # rule on the base branch, or `gh pr merge --auto` will error — set one in
440
+ # the repo's GitHub settings before flipping `enabled: true`.
441
+ #
442
+ # When `enabled: false` (or the block is absent) the autopilot is fully inert:
443
+ # the resource is never constructed and the orchestrator's existing Done-state
444
+ # behavior (workspace cleanup + the Done-state `actions:` block that pushes the
445
+ # branch and opens the PR + operator-merge) is unchanged.
446
+ #
447
+ # Workspace lifecycle gotcha: when `enabled: true`, transitions into the merge
448
+ # state no longer fire the standard terminal workspace cleanup. The pr resource
449
+ # owns the workspace from that point on and removes it once the PR has merged
450
+ # (or been closed). Transitions into the close state (and any other terminal
451
+ # state) keep the standard cleanup-on-transition behavior.
452
+ # ─────────────────────────────────────────────────────────────────────────────
453
+ pr:
454
+ # enabled (bool): master switch. Default false.
455
+ enabled: false
456
+
457
+ # poll_interval_ms (int): per-PR GitHub view cache TTL, milliseconds. The
458
+ # reconciler may tick more often than this; a single PR view is reused
459
+ # within the window. Default 30000.
460
+ poll_interval_ms: 30000
461
+ # ----------------------------------------------------------------------------
462
+ # SLEEP CYCLE (recurring spawn) — the recurrence lives in the `spawn:` block on
463
+ # the active reflection state (see the states-field docs + the SLEEP CYCLE
464
+ # section above). There is NO top-level `sleep_cycle:` block and NO `arm:` block;
465
+ # instead of re-arming one immortal parked ticket, the orchestrator MINTS a fresh
466
+ # ephemeral reflection issue when a trigger fires:
467
+ #
468
+ # states:
469
+ # Reflect:
470
+ # role: active
471
+ # allowed_transitions: [Reflected]
472
+ # spawn:
473
+ # on_idle: true # spawn when idle with >=1 terminal since last cycle
474
+ # after_terminal: 10 # spawn after N terminal transitions (0 disables)
475
+ # title: "Reflection {{ stamp }}"
476
+ # max_in_flight: 1 # never two reflections live at once
477
+ # Reflected:
478
+ # role: terminal # dedicated terminal — NO pr:/actions:
479
+ #
480
+ # SEMANTICS:
481
+ # - on_idle: spawn when the orchestrator is idle (nothing running, claimed, or
482
+ # pending retry, and no active candidate this poll) AND >=1 issue has reached
483
+ # a terminal state since the last cycle. The ">=1 since last cycle" gate is
484
+ # load-bearing: without it an idle orchestrator re-spawns in a tight loop with
485
+ # nothing new to mine.
486
+ # - after_terminal: a backstop for busy stretches that never go idle — spawn
487
+ # once this many issues have reached a terminal state since the last cycle.
488
+ # 0 disables the count trigger.
489
+ # - max_in_flight: never mint a second issue while this many are already live in
490
+ # the spawn state (replaces the immortal ticket's built-in one-at-a-time
491
+ # mutex). Default 1.
492
+ # The terminal-transition counter resets to 0 on a successful mint (and is
493
+ # restored if the mint fails) and is held in orchestrator memory only (a restart
494
+ # resets it). A finished reflection's OWN terminal move (out of the spawn state)
495
+ # is excluded from the counter, so a completed cycle never counts toward spawning
496
+ # the next.
497
+ #
498
+ # VALIDATION (structural, from each state's own role — no dedicated re-validator):
499
+ # `spawn:` is only valid on an `active` state, and at most one active state may
500
+ # declare `spawn`. Unlike the retired `arm:` block it needs no parked issue and
501
+ # no from-state.
502
+ #
503
+ # GUARDRAILS: spawning ONLY mints the reflection issue. The proposals the
504
+ # reflector files still land in Triage and still require human approve/discard —
505
+ # spawning does not bypass the human gate.
506
+ # ----------------------------------------------------------------------------
507
+
508
+ # ─────────────────────────────────────────────────────────────────────────────
509
+ # polling — how often to poll the tracker.
510
+ # ─────────────────────────────────────────────────────────────────────────────
511
+ polling:
512
+ # interval_ms (int): tick interval, milliseconds.
513
+ # Default: 30000
514
+ interval_ms: 5000
515
+
516
+ # ─────────────────────────────────────────────────────────────────────────────
517
+ # workspace — per-issue working directory.
518
+ # ─────────────────────────────────────────────────────────────────────────────
519
+ workspace:
520
+ # root (path): parent directory holding `<issue-id>/` working trees.
521
+ # An explicit value resolves absolute as-is, `~`-expanded against $HOME, or
522
+ # (relative) against the workflow file's directory.
523
+ # Default (unset): ~/.symphony/workspaces/<project> (same <project> derivation
524
+ # as tracker.root above).
525
+ # root: ~/.symphony/workspaces/my-project
526
+
527
+ # github_repo (string | null): the GitHub `owner/repo` slug symphony pushes the
528
+ # per-issue `agent/<id>` branch to and opens a PR against on the Done-state
529
+ # transition. This is the config-file home for what was previously the
530
+ # `SYMPHONY_REPO` env var only.
531
+ # • unset / null / "none" → LOCAL-ONLY mode: no `origin` is restored, nothing
532
+ # is pushed, and the branch is left in the workspace for `git log agent/<id>`.
533
+ # • "owner/repo" → PR mode: the canonical workspace setup restores an
534
+ # `origin` at https://github.com/<owner>/<repo>.git and the Done-state
535
+ # actions push + `gh pr create`. Requires `gh auth status` clean on the host
536
+ # (the token never enters the VM).
537
+ # GitHub-only by construction (the origin URL and `gh` are hardwired). There is
538
+ # NO auto-detection — set the literal slug or leave it local-only. A malformed
539
+ # value (a URL, a bare name, extra slashes/whitespace) is REJECTED at parse time
540
+ # rather than silently falling back to local-only.
541
+ #
542
+ # Precedence: the `SYMPHONY_REPO` env var, when set non-empty, OVERRIDES this
543
+ # field (so existing exports keep working); otherwise this value is used.
544
+ # Default: null.
545
+ # github_repo: owner/repo
546
+
547
+ # base_branch (string): the branch the per-issue workspace clones from and
548
+ # (in PR mode) targets as the PR base. The source repo's local
549
+ # `<base_branch>` is the canonical base ref — symphony does NOT implicitly
550
+ # fetch `origin/<base_branch>` at setup time; update the source repo before
551
+ # the next dispatch to move the base.
552
+ # Precedence: the `SYMPHONY_BASE_BRANCH` env var, when set non-empty,
553
+ # OVERRIDES this field; otherwise this value is used.
554
+ # Default: main.
555
+ base_branch: main
556
+
557
+ # ─────────────────────────────────────────────────────────────────────────────
558
+ # logs — per-issue JSONL run logs (everything to/from the VM, plus typed-action
559
+ # output) AND the orchestrator-side text log mirrored to disk for offline debugging.
560
+ #
561
+ # Per-issue: one file per issue at `<root>/<sanitized-identifier>.jsonl`,
562
+ # appended across attempts AND across symphony process restarts. Each line is
563
+ # a self-describing JSON object with `ts`, `issue_id`, `attempt`, and a
564
+ # `channel` discriminator:
565
+ #
566
+ # channel: "acp" — JSON-RPC frame between host and the in-VM adapter.
567
+ # `direction` ("host_to_vm" | "vm_to_host") and `frame`
568
+ # (parsed JSON) — or `kind: "unparseable"` + `raw`.
569
+ # channel: "stderr" — raw byte chunk from the adapter / VM stderr.
570
+ # channel: "hook" — stdout/stderr chunk from a terminal-state `actions:`
571
+ # run, plus a final `kind: "result"` line (exit_code,
572
+ # signal, timed_out). The `hook` field names the action
573
+ # group (e.g. `actions`). (The channel name is a holdover
574
+ # from the retired workflow-hook surface.)
575
+ # channel: "system" — orchestrator lifecycle events (attempt_started — which
576
+ # also carries the per-state `max_turns` budget,
577
+ # attempt_ended, transition, reconciliation_terminating,
578
+ # etc.). The `transition` event records each state move
579
+ # (from_state, to_state, notes, actor, terminal,
580
+ # rerouted) so the trajectory is reconstructable.
581
+ #
582
+ # Per-issue run summary (for the sleep-cycle reflector): alongside each
583
+ # `<root>/<key>.jsonl`, the orchestrator writes a compact, versioned
584
+ # `<root>/<key>.summary.json` at the issue's terminal unwind. It is a pure
585
+ # REDUCTION over the lifecycle (`system`) events already in the JSONL — no extra
586
+ # hot-path logging — so a reflection turn can read dozens of summaries without
587
+ # re-parsing multi-MB frame logs. Fields (schema_version 1):
588
+ # • state_path — distinct states visited, terminal appended
589
+ # (e.g. ["Todo","Review","Todo","Review","Done"]);
590
+ # • attempts — total dispatched attempts;
591
+ # • per_state[] — {state, attempts, turns_used, max_turns,
592
+ # budget_exhausted, wall_clock_ms};
593
+ # • review_rejections + rejection_notes[] — count and each reviewer kick-back's
594
+ # notes (a non-reroute transition back to the INITIAL
595
+ # implementing state, i.e. a Review→Todo rework);
596
+ # • turn_budget_exhausted, timeouts[] (stall / prompt_timeout / transport);
597
+ # • conflict_routes[] — PR-autopilot / action reroutes (rebase churn);
598
+ # • terminal_state + terminal_outcome (completed | cancelled | incomplete);
599
+ # • pr_number / pr_url (best-effort, scraped from the Done-state actions
600
+ # stdout; null when unavailable);
601
+ # • first/last_event_at, wall_clock_ms_total, generated_at.
602
+ # Graceful absence / backfill: the summary is best-effort. Issues that closed
603
+ # BEFORE this feature shipped have no `*.summary.json`; a write failure or a
604
+ # mid-issue process restart (the in-memory accumulator only sees post-restart
605
+ # attempts) can leave it missing or partial. The reflector MUST treat an absent
606
+ # or partial summary as "no signal for this issue" and fall back to the raw
607
+ # JSONL (or skip the issue) — never as an error. No backfill job is run; old
608
+ # issues simply carry no summary.
609
+ #
610
+ # Orchestrator-side: a single `<root>/symphony.log` (created on demand) gets
611
+ # every structured log line symphony emits — workflow loads, dispatch
612
+ # decisions, action results, reconciler ticks, shutdown — in `key=value` text
613
+ # format. Lets an agent reviewing a finished run (typically with
614
+ # `.symphony/logs/` mounted into a VM) replay orchestrator-side events
615
+ # alongside the per-issue JSONL traces in the same directory. Set the
616
+ # `SYMPHONY_LOG_FILE` env var to override the path; set it to the empty
617
+ # string to disable the file sink entirely (stderr remains).
618
+ #
619
+ # Console routing: while the file sink is active (the default), the structured
620
+ # stream goes to the file ONLY — the console shows just the startup banner
621
+ # (workflow, tracker root, dashboard URL, MCP endpoint, ACP `/acp` WebSocket
622
+ # endpoint, log-file path). The dashboard/MCP/ACP all share the one HTTP port,
623
+ # which defaults to ephemeral, so the banner is how the operator learns which
624
+ # port got bound. `tail -f` the
625
+ # log file to follow the detail. Pass `--verbose` (alias `--foreground`) to mirror
626
+ # the structured stream back onto the console for interactive debugging. With
627
+ # no file sink configured, the structured stream stays on stderr.
628
+ #
629
+ # Intended for later evaluation — typically by another agent running inside a VM
630
+ # — so the schema is verbose on purpose. Writes are best-effort: a failure to
631
+ # write a log line never crashes the orchestrator.
632
+ # ─────────────────────────────────────────────────────────────────────────────
633
+ logs:
634
+ # root (path): directory holding per-issue JSONL files and symphony.log.
635
+ # An explicit value resolves absolute as-is, `~`-expanded against $HOME, or
636
+ # (relative) against the workflow file's directory.
637
+ # Default (unset): ~/.symphony/logs/<project> (same <project> derivation as
638
+ # tracker.root above) — durable run logs live OUTSIDE the repo tree by default.
639
+ # root: ~/.symphony/logs/my-project
640
+
641
+ # ─────────────────────────────────────────────────────────────────────────────
642
+ # workspace lifecycle — no shell `hooks:` surface.
643
+ #
644
+ # Symphony has no `hooks:` block (workflow-level or per-state). The behaviors
645
+ # the old `after_create` / `before_run` / `after_run` / `before_remove` hooks
646
+ # covered now live in typed, testable homes:
647
+ #
648
+ # • First-creation workspace setup (clone + base checkout + `agent/<id>`
649
+ # branch cut + origin/identity) is owned by the orchestrator's TypeScript
650
+ # `setupWorkspaceDir`. The workspace arrives at the agent with: a hardlinked
651
+ # `git clone --local --no-tags` of the source repo (`SYMPHONY_SOURCE_REPO`,
652
+ # default: the dir containing WORKFLOW.yaml) on the base branch
653
+ # (`SYMPHONY_BASE_BRANCH`, default `main`); all network remotes stripped; an
654
+ # `origin` restored to the canonical HTTPS URL when `workspace.github_repo`
655
+ # (or the `SYMPHONY_REPO` env override) is set (so the Done-state push can
656
+ # reach a remote — `gh auth setup-git` runs host-side; the token never
657
+ # enters the VM); pinned `--local` git identity;
658
+ # and `agent/<id>` checked out off the base SHA.
659
+ # • Per-VM tooling (extra CLIs, language runtimes, dependency bootstrap) is
660
+ # baked into the agent image — see `gondolin.image` and `images/agents/`.
661
+ # • Arbitrary in-sandbox commands run from a state's `actions:` via a
662
+ # `run_in_vm` action (executes inside the per-issue VM, not on the host).
663
+ # • The post-attempt handoff (push branch, open PR) is the Done state's
664
+ # `actions:` block — see `states.Done.actions` above for the canonical pair
665
+ # (push_branch + create_pr_if_missing). The action executor exposes
666
+ # `$branch`, `$base_branch`, `$pr_title`, `$pr_body_file`, `$repo`.
667
+ #
668
+ # Workspace removal is a plain best-effort `rm -rf` of the per-issue dir once
669
+ # the run unwinds; there is no pre-removal artifact-rescue hook (rescue what you
670
+ # need via a terminal-state action before transitioning).
671
+ # ─────────────────────────────────────────────────────────────────────────────
672
+
673
+ # ─────────────────────────────────────────────────────────────────────────────
674
+ # agent — concurrency and turn budget.
675
+ # ─────────────────────────────────────────────────────────────────────────────
676
+ agent:
677
+ # max_concurrent_agents (int): GLOBAL host ceiling on simultaneously-running
678
+ # agents across the whole workflow. This is the cross-state RAM bound that
679
+ # memory admission clamps (see memory_admission_enabled below); the sum of
680
+ # every state's `max_concurrent` is validated against it at startup. It stays
681
+ # top-level — not on a state — because it bounds total host memory across all
682
+ # VMs at once. Default: 10
683
+ max_concurrent_agents: 2
684
+
685
+ # max_turns (int): hard ceiling on autonomous turns per issue. Steering-reply
686
+ # turns are free; only autonomous turns count. Default: 20
687
+ max_turns: 6
688
+
689
+ # max_retry_backoff_ms (int): exponential backoff cap for retried dispatches
690
+ # after recoverable failures. Default: 300000
691
+ max_retry_backoff_ms: 120000
692
+
693
+ # memory_admission_enabled (bool): when true, before each dispatch the
694
+ # orchestrator reads `/proc/meminfo` (MemAvailable) and clamps the effective
695
+ # concurrency cap to what currently fits at `gondolin.mem_mib` per VM after
696
+ # subtracting `host_memory_reserve_mib`. This is a defense-in-depth backstop
697
+ # for hosts where the static `max_concurrent_agents` is set generously: when
698
+ # MemAvailable drops, new dispatches are gated so a misconfigured cap can't
699
+ # walk the host into OOM (issue 27). On hosts without `/proc/meminfo`
700
+ # (macOS, BSD) the probe degrades gracefully and the static cap is used
701
+ # unchanged. Default: true.
702
+ memory_admission_enabled: true
703
+
704
+ # host_memory_reserve_mib (int): headroom (MiB) the memory admission cap
705
+ # keeps for the orchestrator process itself, the per-VM Gondolin runners, and
706
+ # the kernel's own working set. Only consulted when
707
+ # `memory_admission_enabled` is true. Raise on hosts with heavy non-symphony
708
+ # workloads; lower on dedicated worker hosts. Default: 2048.
709
+ host_memory_reserve_mib: 2048
710
+
711
+ # circuit_breaker_threshold (int): after this many CONSECUTIVE dispatch
712
+ # attempts fail with the *same* (normalized) reason, the orchestrator stops
713
+ # retrying the issue and routes it to a holding state (the first declared
714
+ # `role: holding` state) for a human to inspect, instead of looping forever
715
+ # on a deterministically-failing dispatch (issue 128 — a persistent
716
+ # `401 invalid_api_key` once looped ~324 attempts over ~13h). The streak
717
+ # resets the moment an attempt fails with a different reason or exits
718
+ # cleanly, so transient/varied failures still retry under the normal backoff
719
+ # (`max_retry_backoff_ms`). The tripped issue's body gets a diagnostic note
720
+ # explaining the trip so the dashboard shows "stuck on identical failure"
721
+ # rather than a silent loop. Set to 0 to disable; must otherwise be >= 2
722
+ # (1 would trip on the first failure, never retrying). Default: 5.
723
+ circuit_breaker_threshold: 5
724
+
725
+ # ─────────────────────────────────────────────────────────────────────────────
726
+ # acp — Agent Client Protocol adapter selection.
727
+ # ─────────────────────────────────────────────────────────────────────────────
728
+ acp:
729
+ # adapter (string): one of symphony's known profiles. Default: 'claude'.
730
+ # claude — claude-agent-acp. The guest holds a placeholder bearer; the host
731
+ # substitutes the real Anthropic OAuth token at Gondolin egress.
732
+ # No real credential enters the VM.
733
+ # codex — codex-acp (issue 116). Same model: the guest holds a placeholder
734
+ # bearer (in a fake ~/.codex/auth.json); the host substitutes the
735
+ # real OpenAI/ChatGPT token at egress. No real credential — and no
736
+ # real OPENAI_API_KEY — enters the VM.
737
+ adapter: claude
738
+
739
+ # Credentials never enter the VM (issue 113; codex generalized in 116). The
740
+ # guest holds only a token-shaped placeholder; on every outbound request the
741
+ # host substitutes the real upstream credential at Gondolin egress (TLS-MITM,
742
+ # see src/agent/credential-secrets.ts). The real refresh/durable token always
743
+ # stays host-side.
744
+ #
745
+ # For claude: the host reads the live access token from
746
+ # ~/.claude/.credentials.json (refreshing host-side via `claude -p "ok"` under
747
+ # flock when the cache is stale) and injects it at egress to api.anthropic.com.
748
+ # A minimal ~/.claude.json is staged for identity only — NO refreshToken, NO
749
+ # accessToken on the VM.
750
+ #
751
+ # For codex: the host reads the live credential (`tokens.access_token` from
752
+ # ~/.codex/auth.json, with an OPENAI_API_KEY env fallback — NEVER the refresh
753
+ # token) and injects it at egress to chatgpt.com / api.openai.com. A COMPLETE
754
+ # fake ~/.codex/auth.json is staged (JWT-shaped placeholder tokens + the
755
+ # non-secret account_id/auth_mode/last_refresh codex's completeness check needs),
756
+ # so codex-acp runs in its native mode without an in-VM OAuth handshake or
757
+ # refresh (both stay host-side). Every credential-bearing var is stripped from
758
+ # the forwarded VM boot env.
759
+
760
+ # model (string | null): optional model selector forwarded to the chosen adapter.
761
+ # Each adapter profile knows how to surface it natively:
762
+ # claude — exported as ANTHROPIC_MODEL on the adapter process. Accepts anything
763
+ # claude-agent-acp would (aliases like "opus", "sonnet", or full IDs
764
+ # like "claude-opus-4-7").
765
+ # codex — passed as `-c model="<value>"` argv to codex-acp (parsed as TOML).
766
+ # Leave unset / null to use the adapter's own default model. Default: null.
767
+ # model: claude-opus-4-7
768
+
769
+ # effort (string | null): optional reasoning-effort lever forwarded to the chosen
770
+ # adapter. Profile-specific surface:
771
+ # claude — written into a staged `settings.json` ({"effortLevel": "<value>"})
772
+ # copied to /root/.claude/settings.json in the VM before claude-agent-acp
773
+ # starts. Valid values are `low|medium|high|xhigh|max`, gated per-model
774
+ # by claude-agent-acp's `supportedEffortLevels` (Opus supports `xhigh` and
775
+ # `max`; Haiku does not). Symphony does not validate the value — the
776
+ # adapter rejects unsupported choices at startup, which keeps symphony
777
+ # from drifting from the adapter's own supported list.
778
+ # codex — not wired (codex-acp has no first-class effort knob on the wrapper);
779
+ # setting `acp.effort` for a codex-backed state is a no-op.
780
+ # Leave unset / null for the adapter's own default. Default: null.
781
+ # effort: xhigh
782
+
783
+ # NOTE: the launch shape is fixed (an in-VM agent dials back the `/acp` WebSocket
784
+ # and spawns the chosen adapter). Customizing what the agent spawns requires
785
+ # forking that agent and rebuilding the VM image with the fork in place.
786
+
787
+ # shell (string): shell used to run the ACP launch command. Default: 'bash'.
788
+ shell: bash
789
+
790
+ # prompt_timeout_ms (int): max wall time for a single ACP `session/prompt`
791
+ # call (one symphony turn). Default: 3600000 (1 hour).
792
+ prompt_timeout_ms: 1800000
793
+
794
+ # read_timeout_ms (int): max time between bytes on the ACP stdio. Bumped from
795
+ # a small default because VM cold-boot + adapter startup can take ~10s on
796
+ # first use. Default: 30000
797
+ read_timeout_ms: 30000
798
+
799
+ # stall_timeout_ms (int): max time the adapter can be idle (no events) before
800
+ # the turn is killed and retried. Default: 300000
801
+ stall_timeout_ms: 300000
802
+
803
+ # ACP transport — the in-VM agent dials back a `/acp` WebSocket on symphony's unified
804
+ # HTTP server (the SAME listener that serves the dashboard + MCP), reached through the
805
+ # SAME `tcp.hosts` tunnel MCP uses (the guest dials `ws://symphony-mcp:7001/acp`). It
806
+ # authenticates with a per-dispatch bearer sent as the first WebSocket message, then ACP
807
+ # JSON-RPC frames flow as WebSocket frames; the in-VM agent (`/opt/symphony/vm-agent.mjs`)
808
+ # spawns the adapter via `child_process.spawn` with kernel pipes and pumps the frames to
809
+ # the adapter's stdio. There is NO separate ACP listener / bind port — the raw-TCP bridge
810
+ # (`acp.bridge.*`: bind_host/bind_port/reach_host/reach_url) was retired; those keys are
811
+ # ignored if present.
812
+
813
+ # connect_timeout_ms (int): how long to wait for the in-VM agent to dial back the `/acp`
814
+ # WebSocket after the sandbox is launched, before failing the attempt. Default: 30000
815
+ connect_timeout_ms: 30000
816
+
817
+ # ─────────────────────────────────────────────────────────────────────────────
818
+ # credentials — host credential lifecycle (issue 113). The host substitutes the
819
+ # real OAuth access token into each VM's outbound request at Gondolin egress; the
820
+ # ticker keeps the host's cached access token warm by periodically running
821
+ # `claude -p "ok"` — Claude Code's own OAuth path detects the stale token,
822
+ # refreshes against Anthropic, and atomically writes the rotated tuple back to
823
+ # `~/.claude/.credentials.json`. Symphony never implements OAuth; Anthropic's own
824
+ # client does.
825
+ # ─────────────────────────────────────────────────────────────────────────────
826
+ credentials:
827
+ # ticker_interval_ms (int): how often the host ticker spawns `claude -p "ok"`
828
+ # to refresh the OAuth cache. Each live VM also refreshes proactively before its
829
+ # cached token expires, so the ticker is belt-to-the-braces for idle periods.
830
+ # Set to 0 to disable the in-symphony ticker entirely (operator runs their own
831
+ # systemd timer instead). Default: 21600000 (6 hours).
832
+ ticker_interval_ms: 21600000
833
+
834
+ # refresh_margin_ms (int): how far ahead of a token's expiry the host refreshes it.
835
+ # Drives BOTH the per-VM proactive tick (scheduled at `expiresAt - margin`) AND the
836
+ # egress request-side lazy refresh (a request bound for a credential host within this
837
+ # margin of expiry awaits a fresh token before going out). Must comfortably cover the
838
+ # refresh round-trip (`claude -p ok` rotate + re-read + fan-out) plus any in-flight long
839
+ # request, or a token expiring mid-dispatch loses the refresh/request race and 401s the
840
+ # egress (issue 214). Default: 300000 (5 minutes).
841
+ refresh_margin_ms: 300000
842
+
843
+ # ─────────────────────────────────────────────────────────────────────────────
844
+ # gondolin — microVM execution environment (Gondolin substrate).
845
+ # ─────────────────────────────────────────────────────────────────────────────
846
+ gondolin:
847
+ # image: the agent rootfs source. THREE forms, in precedence order
848
+ # (image > oci_image > managed):
849
+ #
850
+ # 1. `managed` (the DEFAULT when neither `image` nor `oci_image` is set, and what a
851
+ # fresh scaffold emits): FETCH a prebuilt Gondolin asset published to symphony's
852
+ # OWN GitHub releases for the running symphony version + host arch (issue 224).
853
+ # No docker, no `npm run build:image`, no local convert — onboarding is
854
+ # `scaffold → run`. On the first poll the orchestrator downloads + verifies (against
855
+ # the published `.sha256`) + imports the asset ONCE (the dashboard shows the
856
+ # one-time fetch) and HOLDS VM dispatch until it's ready, then boots the cached
857
+ # asset. Published for x86_64 and arm64/aarch64 (the `release.yml` agent-image
858
+ # matrix); any OTHER arch makes the managed source inert and the doctor FAILs
859
+ # with an unsupported-arch message.
860
+ # Override `SYMPHONY_RELEASE_REPO` to fetch from a fork/staging release.
861
+ #
862
+ # 2. a PRE-CONVERTED asset selector — the low-level escape hatch. Build it ONCE with
863
+ # `npm run build:image` (see images/agents/); the build prints a content-addressed
864
+ # build id (a digest) — pin that here for an immutable reference. A `name:tag` ref
865
+ # (e.g. `symphony-agents:latest`) or a path to an exported asset directory also
866
+ # work. WINS over `oci_image` and the managed source when set.
867
+ #
868
+ # 3. `oci_image` (below): a normal OCI ref symphony auto-converts on first reconcile.
869
+ #
870
+ # The image ships only `mise`; node + the ACP CLIs (claude-agent-acp, codex-acp)
871
+ # are mise-installed at dispatch and the in-VM launcher is staged at
872
+ # /opt/symphony/vm-agent.mjs — so dispatch needs no runtime mounts. Default: managed.
873
+ image: managed
874
+
875
+ # oci_image (string | null): a NORMAL OCI image reference (a docker/podman repo
876
+ # string, optionally `:tag` or `@sha256:…`) that symphony auto-converts + caches into
877
+ # a Gondolin asset as part of the reconcile loop (issue 206) — no manual
878
+ # `npm run build:image` step. On the first poll where the asset isn't cached, the
879
+ # orchestrator converts it ONCE (the dashboard shows a "building microVM image
880
+ # (one-time setup)" banner) and HOLDS VM dispatch until it's ready; the cache key is
881
+ # the resolved OCI digest, re-checked cheaply each reconcile tick (a read-only LOCAL
882
+ # resolve, no pull), so a moving tag whose locally-resolved digest changes (e.g. after
883
+ # a re-pull) reconverts before dispatch resumes — readiness tracks the live digest, not
884
+ # a ref string that converted once. `oci_pull_policy` governs the one-time conversion
885
+ # pull, not this readiness check. Once converted, dispatch boots the cached asset
886
+ # automatically — set ONLY
887
+ # this key and go; you do NOT also need to set `image`. The MVP targets the "golden"
888
+ # path — a published ref that already bakes the guest contract (Node ≥ 21 + the ACP
889
+ # CLIs on PATH; the launcher is staged at dispatch). `image` stays the escape hatch
890
+ # for an asset you converted yourself, and WINS when both are set: when `image` is
891
+ # set the reconcile gate boots IT and the `oci_image` auto-convert path is bypassed
892
+ # entirely — dispatch is never held and no one-time build runs. Default: null.
893
+ # oci_image: ghcr.io/your-org/symphony-agents:1.0.0
894
+
895
+ # oci_pull_policy (if-not-present | always | never): how the OCI image is pulled
896
+ # while resolving its digest for the one-time conversion. `if-not-present` pulls
897
+ # only when the ref isn't already in the local container store; `always` re-pulls
898
+ # even when it IS present locally, so a moving tag (`repo:latest`) picks up its
899
+ # fresh registry digest (and reconverts when that digest changed); `never` never
900
+ # pulls and requires the image to already be in the local store. The cache key and
901
+ # the converted asset are always keyed on the POST-pull digest, so `always` can't
902
+ # cache against a stale local one. Only consulted when `oci_image` is set (and not
903
+ # by the per-tick readiness probe, which is always pull-free). Default: if-not-present.
904
+ # oci_pull_policy: if-not-present
905
+
906
+ # oci_runtime (docker | podman | null): container runtime used for the conversion
907
+ # pull/export. null = auto-detect (prefers docker, falls back to podman). Only
908
+ # consulted when `oci_image` is set. Default: null (auto-detect).
909
+ # oci_runtime: docker
910
+
911
+ # cpus (int): vCPU count per VM. Default: 2.
912
+ cpus: 2
913
+
914
+ # mem_mib (int): RAM per VM in MiB. Default: 2048.
915
+ mem_mib: 4096
916
+
917
+ # rootfs_size (string): guest rootfs virtual-disk size (qemu size syntax, e.g. "3G"),
918
+ # passed to the VM substrate as the MINIMUM rootfs size. The agent toolchain installs
919
+ # onto the guest ROOTFS each dispatch (chmod works there; the sandboxfs bind mount
920
+ # silently drops it — issue 222), so the rootfs must be big enough for the ephemeral
921
+ # install: node alone is ~123 MB plus the agent CLIs, and the default ~593 MB rootfs
922
+ # leaves too little headroom. Default: "3G".
923
+ # rootfs_size: 3G
924
+
925
+ # volumes (list): additional host:guest VFS mounts beyond the auto-mounted
926
+ # workspace. Gondolin's VFS is programmable (no hard per-VM mount cap), but keep
927
+ # this lean — if ANY state sets `eval_mode: true` it adds two read-only mounts
928
+ # (/symphony/issues + /symphony/logs) on top of the workspace. Prefer baking
929
+ # static tooling into the image over a runtime mount. Each entry:
930
+ # { host: path, guest: path, readonly?: bool }. Default: [].
931
+ volumes:
932
+ - host: ~/.cache/npm
933
+ guest: /root/.npm
934
+ readonly: false
935
+
936
+ # forward_env (string[]): host env vars forwarded into the VM exec.
937
+ # Default: [OPENAI_API_KEY, ANTHROPIC_API_KEY]
938
+ # NOTE: the runner strips EVERY credential-bearing var from the forwarded boot
939
+ # env before launch — the guest holds only a token-shaped placeholder that
940
+ # Gondolin substitutes with the real token at egress — so listing a credential
941
+ # var here does NOT plant the real key in the VM's PID-1 env.
942
+ forward_env:
943
+ - OPENAI_API_KEY
944
+ - ANTHROPIC_API_KEY
945
+
946
+ # mise (jdx/mise) toolchain provisioning. This is ALWAYS ON and is the ONLY toolchain
947
+ # path (issue 233) — there is no enable/disable toggle. The agent image ships ONLY the
948
+ # `mise` binary (+ glibc essentials); node + the agent CLIs (claude-code, codex,
949
+ # claude-agent-acp, codex-acp) come from a mise SYSTEM config symphony
950
+ # STAGES into the guest at /etc/mise/config.toml at dispatch, and a consuming repo's
951
+ # own `mise.toml` / `.tool-versions` (higher precedence) ADDS project toolchains
952
+ # (rust, go, kotlin+gradle, a different node, …) on top. `mise install` runs once
953
+ # per dispatch, installing the toolchain onto the guest ROOTFS at /opt/symphony/mise-data
954
+ # (EPHEMERAL per VM — see below); the agent runtime node is resolved system-scoped via
955
+ # `mise which node` (so a project mise.toml can't downgrade the agent below the Node ≥ 21
956
+ # the launcher needs). Bumping an agent CLI is a one-line edit to
957
+ # assets/symphony-mise.system.toml + a restart — NO image rebuild + digest repin.
958
+ #
959
+ # WHY the rootfs, not a bind mount (issue 222): the gondolin sandboxfs bind mount has no
960
+ # chmod opcode (chmod is a silent no-op there), so mise's create-then-chmod extractor +
961
+ # npm postinstall would produce NON-executable binaries on it; the guest rootfs (ext4)
962
+ # honors chmod, so installs land there executable. Grow it via `gondolin.rootfs_size`
963
+ # (default 3G) so the per-VM install fits. Only the pure downloads (node tarballs + the
964
+ # npm package cache) persist across dispatches, on the bind-mounted `data_dir` below.
965
+ #
966
+ # The only tunable here is `data_dir` (the download cache location).
967
+ mise:
968
+ # data_dir (string): host directory bind-mounted RW → /opt/symphony/mise-cache as the
969
+ # persistent mise DOWNLOAD CACHE (node tarballs + the npm package cache) — NOT the
970
+ # install dir (issue 222): installs need chmod, which the sandboxfs bind mount silently
971
+ # drops, so the toolchain installs onto the ephemeral guest rootfs and only the pure
972
+ # downloads live here (so a cold install isn't re-downloaded every dispatch). SHARED
973
+ # across every VM; host-isolated from your own ~/.local/share/mise. Absolute as-is,
974
+ # `~`/`$VAR`-expanded, relative → resolved against the workflow-file dir.
975
+ # Default: ~/.symphony/mise.
976
+ # data_dir: ~/.symphony/mise
977
+
978
+ # ─────────────────────────────────────────────────────────────────────────────
979
+ # egress — general dev-tooling firewall for the in-VM agent.
980
+ #
981
+ # Gondolin denies guest egress to non-allowlisted hosts by default. The agent can
982
+ # always reach its own inference host (that is handled by the credential layer,
983
+ # which substitutes the real upstream token at egress). This block additionally
984
+ # opens the dev-tooling hosts the agent needs so gates can run inside the VM —
985
+ # `npm install`, git-based dependencies, release-binary downloads.
986
+ #
987
+ # SECURITY: this is the firewall ONLY. No credential is ever substituted for a host
988
+ # listed here — the real token substitutes solely on each adapter's inference host
989
+ # (see src/agent/credential-secrets.ts `substitutionHosts`). The effective
990
+ # per-adapter allowlist handed to Gondolin is THIS list UNION that adapter's
991
+ # substitution host(s). Listing a host therefore grants plain network egress, never
992
+ # a token. Keep the list tight — every entry widens the network surface of
993
+ # semi-trusted in-VM code.
994
+ # ─────────────────────────────────────────────────────────────────────────────
995
+ egress:
996
+ # allowed_hosts (string[]): hostnames the in-VM agent may reach for dev tooling.
997
+ # Default: the mise provisioning hosts [nodejs.org, registry.npmjs.org, mise.jdx.dev,
998
+ # mise-versions.jdx.dev] — ALWAYS, since mise provisioning is unconditional (issue 233)
999
+ # — so `mise install` of the staged SYSTEM config isn't firewall-blocked. Bare
1000
+ # hostnames ONLY — no scheme, port, or path (`github.com`, not
1001
+ # `https://github.com/...`). A malformed entry fails safe (the host simply stays
1002
+ # blocked, never opened). Each entry is matched against the request host exactly.
1003
+ #
1004
+ # An explicit list REPLACES the default — so if you set `allowed_hosts`, re-list all
1005
+ # four mise hosts here alongside your own (omitting mise-versions.jdx.dev re-hits the
1006
+ # cold-cache node@<N> resolution failure issue 223 documents). PROJECT toolchains pull
1007
+ # from their own hosts; add those too (rust → static.rust-lang.org, gradle →
1008
+ # services.gradle.org, JDK → api.adoptium.net, …).
1009
+ allowed_hosts:
1010
+ - nodejs.org # mise node prebuilts
1011
+ - registry.npmjs.org # npm install + mise npm: backend (agent CLIs)
1012
+ - mise.jdx.dev # mise registry redirect
1013
+ - mise-versions.jdx.dev # mise precompiled tool version lists (node@N, etc.)
1014
+ - github.com # git-based deps / release pages
1015
+ - codeload.github.com # GitHub tarball fetch
1016
+ - objects.githubusercontent.com # release-binary downloads
1017
+
1018
+ # ─────────────────────────────────────────────────────────────────────────────
1019
+ # server — HTTP dashboard + MCP endpoint listener.
1020
+ # ─────────────────────────────────────────────────────────────────────────────
1021
+ server:
1022
+ # port (int | null): HTTP listener port. `--port <n>` on the CLI overrides
1023
+ # this. When both are unset symphony binds an ephemeral port (0 → the kernel
1024
+ # picks a free port), so it "just works" with no config and two concurrent
1025
+ # instances never collide; the actually-bound port + dashboard URL are printed
1026
+ # to stdout at startup. Set a value here to pin a fixed port. Default: null
1027
+ # (ephemeral).
1028
+ # port: 8787
1029
+
1030
+ # host (string): bind address. Default: '127.0.0.1'. Bind to '0.0.0.0' only
1031
+ # inside a trusted network boundary; the dashboard has no built-in auth.
1032
+ host: 0.0.0.0
1033
+
1034
+ # ─────────────────────────────────────────────────────────────────────────────
1035
+ # mcp — Model Context Protocol server exposed to in-VM agents.
1036
+ #
1037
+ # The orchestrator runs a JSON-RPC endpoint scoped to each active issue at
1038
+ # /api/v1/issues/<id>/mcp, gated by a per-dispatch bearer token. Three tools
1039
+ # live there:
1040
+ #
1041
+ # • symphony.transition({ to_state, notes? })
1042
+ # — canonical (and only) exit verb. Moves the issue into another declared
1043
+ # state, optionally appending `notes` (markdown) to the issue body before
1044
+ # the move so the next agent (in `to_state`) reads them as part of
1045
+ # `issue.description`. Terminal targets clean the workspace; active and
1046
+ # holding targets preserve it so the same `agent/<id>` git branch
1047
+ # survives the handoff. Rejected transitions return MCP tool-result
1048
+ # errors (isError:true) the agent can read and retry.
1049
+ # • symphony.request_human_steering({ question, context? })
1050
+ # • symphony.propose_issue({ title, description?, labels?, priority? })
1051
+ # — drops a new issue into the first declared `role: holding` state
1052
+ # directory (literal Triage if none declared). The orchestrator does NOT
1053
+ # dispatch it; the operator approves or discards from the dashboard. The
1054
+ # calling issue is recorded as proposed_by in the new file's
1055
+ # front-matter.
1056
+ # ─────────────────────────────────────────────────────────────────────────────
1057
+ mcp:
1058
+ # enabled (bool): when false, the orchestrator refuses to dispatch (MCP is
1059
+ # required for completion signaling). Default: true.
1060
+ enabled: true
1061
+
1062
+ # host (string): hostname or IP the agent uses to reach the orchestrator
1063
+ # from inside the VM. The port is resolved at runtime from the
1064
+ # actually-bound HTTP server. Default: '127.0.0.1' (Gondolin maps VM
1065
+ # loopback to host loopback; verified empirically).
1066
+ host: 127.0.0.1
1067
+
1068
+ # host_url (string | null): full-URL override. When set, used verbatim and
1069
+ # `host` + bound port are ignored. Use only when the VM cannot reach the
1070
+ # orchestrator through the host gateway (e.g. bridge networking with a
1071
+ # fixed reverse-proxy URL). Default: null.
1072
+ host_url: null
1073
+
1074
+ # ─────────────────────────────────────────────────────────────────────────────
1075
+ # prompts — per-state files (there is NO inline prompt body).
1076
+ #
1077
+ # A workflow file is pure YAML config; it carries no prompt body. Each active
1078
+ # state's prompt is its `states.<name>.prompt_file` (a Liquid template) wrapped
1079
+ # in the shared `prompt.preamble_file` / `prompt.footer_file` (see the `prompt:`
1080
+ # and `states:` sections above). The runner renders the matching state's
1081
+ # assembled template fresh on every dispatch, so each state's agent sees only its
1082
+ # own instructions plus the shared preamble / footer.
1083
+ #
1084
+ # Liquid context available to every prompt file ({ issue, attempt }):
1085
+ # issue.identifier — the issue's external id (e.g. "DEMO-42").
1086
+ # issue.title — issue title (string).
1087
+ # issue.state — current state (string, matches a key in `states:`).
1088
+ # issue.description — body text (string or empty). `symphony.transition`
1089
+ # appends its `notes` block here before the file moves, so
1090
+ # the next state's agent reads the previous state's handoff
1091
+ # message verbatim.
1092
+ # issue.priority — number or null.
1093
+ # issue.labels — list of strings (lowercased).
1094
+ # attempt — int, 1-based attempt counter; absent on first attempt.
1095
+ #
1096
+ # Available Liquid filters: standard Shopify Liquid plus `escape_once`.
1097
+ #
1098
+ # See prompts/ alongside WORKFLOW.yaml in this repo for worked prompt files, and
1099
+ # WORKFLOW.minimal.yaml for the smallest single-state example.
1100
+ # ─────────────────────────────────────────────────────────────────────────────