smol-symphony 0.2.0 → 0.3.1

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 (540) hide show
  1. package/AGENTS.md +41 -22
  2. package/DESIGN.md +494 -273
  3. package/README.md +109 -57
  4. package/SPEC.md +33 -24
  5. package/WORKFLOW.minimal.yaml +34 -0
  6. package/{WORKFLOW.template.md → WORKFLOW.template.yaml} +409 -256
  7. package/WORKFLOW.yaml +487 -0
  8. package/assets/skills/symphony-issues/SKILL.md +136 -0
  9. package/assets/symphony-mise.system.toml +68 -0
  10. package/dist/bin/symphony.js +22 -786
  11. package/dist/bin/symphony.js.map +1 -1
  12. package/dist/core/actions/context.js +109 -0
  13. package/dist/core/actions/context.js.map +1 -0
  14. package/dist/{actions/parsing.js → core/actions/parse.js} +33 -114
  15. package/dist/core/actions/parse.js.map +1 -0
  16. package/dist/core/actions/plan.js +197 -0
  17. package/dist/core/actions/plan.js.map +1 -0
  18. package/dist/core/actions/predicates.js +111 -0
  19. package/dist/core/actions/predicates.js.map +1 -0
  20. package/dist/core/actions/run-fold.js +248 -0
  21. package/dist/core/actions/run-fold.js.map +1 -0
  22. package/dist/core/actions/template.js +118 -0
  23. package/dist/core/actions/template.js.map +1 -0
  24. package/dist/core/cli/args.js +116 -0
  25. package/dist/core/cli/args.js.map +1 -0
  26. package/dist/core/coerce.js +75 -0
  27. package/dist/core/coerce.js.map +1 -0
  28. package/dist/core/credential/account-id.js +20 -0
  29. package/dist/core/credential/account-id.js.map +1 -0
  30. package/dist/core/credential/adapter-config.js +136 -0
  31. package/dist/core/credential/adapter-config.js.map +1 -0
  32. package/dist/core/credential/availability.js +98 -0
  33. package/dist/core/credential/availability.js.map +1 -0
  34. package/dist/core/credential/extract.js +228 -0
  35. package/dist/core/credential/extract.js.map +1 -0
  36. package/dist/core/credential/fake-creds.js +171 -0
  37. package/dist/core/credential/fake-creds.js.map +1 -0
  38. package/dist/core/credential/identity.js +125 -0
  39. package/dist/core/credential/identity.js.map +1 -0
  40. package/dist/core/credential/shape.js +230 -0
  41. package/dist/core/credential/shape.js.map +1 -0
  42. package/dist/core/credential/strings.js +15 -0
  43. package/dist/core/credential/strings.js.map +1 -0
  44. package/dist/core/doctor/checks.js +303 -0
  45. package/dist/core/doctor/checks.js.map +1 -0
  46. package/dist/core/git/result.js +107 -0
  47. package/dist/core/git/result.js.map +1 -0
  48. package/dist/core/http/decisions.js +225 -0
  49. package/dist/core/http/decisions.js.map +1 -0
  50. package/dist/{http.js → core/http/render.js} +472 -738
  51. package/dist/core/http/render.js.map +1 -0
  52. package/dist/{http-handlers.js → core/http/routes.js} +52 -87
  53. package/dist/core/http/routes.js.map +1 -0
  54. package/dist/core/http/views.js +181 -0
  55. package/dist/core/http/views.js.map +1 -0
  56. package/dist/core/image/managed-image.js +95 -0
  57. package/dist/core/image/managed-image.js.map +1 -0
  58. package/dist/core/issue/file.js +149 -0
  59. package/dist/core/issue/file.js.map +1 -0
  60. package/dist/core/issue/parse.js +210 -0
  61. package/dist/core/issue/parse.js.map +1 -0
  62. package/dist/core/mcp/dispatch.js +239 -0
  63. package/dist/core/mcp/dispatch.js.map +1 -0
  64. package/dist/core/mcp/post-move.js +92 -0
  65. package/dist/core/mcp/post-move.js.map +1 -0
  66. package/dist/core/mcp/protocol.js +293 -0
  67. package/dist/core/mcp/protocol.js.map +1 -0
  68. package/dist/core/mcp/url.js +162 -0
  69. package/dist/core/mcp/url.js.map +1 -0
  70. package/dist/core/path.js +63 -0
  71. package/dist/core/path.js.map +1 -0
  72. package/dist/core/reconcile/image-decide.js +48 -0
  73. package/dist/core/reconcile/image-decide.js.map +1 -0
  74. package/dist/core/reconcile/ledger.js +142 -0
  75. package/dist/core/reconcile/ledger.js.map +1 -0
  76. package/dist/core/reconcile/pr-classify.js +62 -0
  77. package/dist/core/reconcile/pr-classify.js.map +1 -0
  78. package/dist/{reconciler → core/reconcile}/pr-decide.js +25 -12
  79. package/dist/core/reconcile/pr-decide.js.map +1 -0
  80. package/dist/core/reconcile/pr-loop.js +161 -0
  81. package/dist/core/reconcile/pr-loop.js.map +1 -0
  82. package/dist/core/reconcile/pr-notes.js +35 -0
  83. package/dist/core/reconcile/pr-notes.js.map +1 -0
  84. package/dist/core/reconcile/vm-decide.js +70 -0
  85. package/dist/core/reconcile/vm-decide.js.map +1 -0
  86. package/dist/core/reconcile/vm-reap.js +207 -0
  87. package/dist/core/reconcile/vm-reap.js.map +1 -0
  88. package/dist/core/reconcile/workspace-decide.js +162 -0
  89. package/dist/core/reconcile/workspace-decide.js.map +1 -0
  90. package/dist/core/runlog/summary.js +231 -0
  91. package/dist/core/runlog/summary.js.map +1 -0
  92. package/dist/core/runner/dispatch-config.js +95 -0
  93. package/dist/core/runner/dispatch-config.js.map +1 -0
  94. package/dist/core/runner/injection.js +61 -0
  95. package/dist/core/runner/injection.js.map +1 -0
  96. package/dist/core/runner/mise.js +210 -0
  97. package/dist/core/runner/mise.js.map +1 -0
  98. package/dist/core/runner/prompt.js +720 -0
  99. package/dist/core/runner/prompt.js.map +1 -0
  100. package/dist/core/runner/turn.js +242 -0
  101. package/dist/core/runner/turn.js.map +1 -0
  102. package/dist/core/runner/vm-plan.js +390 -0
  103. package/dist/core/runner/vm-plan.js.map +1 -0
  104. package/dist/core/schedule/admission.js +123 -0
  105. package/dist/core/schedule/admission.js.map +1 -0
  106. package/dist/core/schedule/circuit-breaker.js +111 -0
  107. package/dist/core/schedule/circuit-breaker.js.map +1 -0
  108. package/dist/core/schedule/eligibility.js +83 -0
  109. package/dist/core/schedule/eligibility.js.map +1 -0
  110. package/dist/core/schedule/reconcile-issue.js +82 -0
  111. package/dist/core/schedule/reconcile-issue.js.map +1 -0
  112. package/dist/core/schedule/retry.js +96 -0
  113. package/dist/core/schedule/retry.js.map +1 -0
  114. package/dist/core/schedule/sleep-cycle.js +133 -0
  115. package/dist/core/schedule/sleep-cycle.js.map +1 -0
  116. package/dist/core/schedule/slots.js +124 -0
  117. package/dist/core/schedule/slots.js.map +1 -0
  118. package/dist/core/schedule/tick.js +553 -0
  119. package/dist/core/schedule/tick.js.map +1 -0
  120. package/dist/core/schedule/token-fold.js +181 -0
  121. package/dist/core/schedule/token-fold.js.map +1 -0
  122. package/dist/core/state-resolve.js +86 -0
  123. package/dist/core/state-resolve.js.map +1 -0
  124. package/dist/core/vm-guards.js +278 -0
  125. package/dist/core/vm-guards.js.map +1 -0
  126. package/dist/core/workflow/derive.js +107 -0
  127. package/dist/core/workflow/derive.js.map +1 -0
  128. package/dist/core/workflow/parse.js +687 -0
  129. package/dist/core/workflow/parse.js.map +1 -0
  130. package/dist/core/workflow/prompt-probe.js +78 -0
  131. package/dist/core/workflow/prompt-probe.js.map +1 -0
  132. package/dist/core/workflow/validate.js +189 -0
  133. package/dist/core/workflow/validate.js.map +1 -0
  134. package/dist/core/workspace-key.js +19 -0
  135. package/dist/core/workspace-key.js.map +1 -0
  136. package/dist/shell/actions-runner.js +356 -0
  137. package/dist/shell/actions-runner.js.map +1 -0
  138. package/dist/shell/adapter/adapter-registry.js +45 -0
  139. package/dist/shell/adapter/adapter-registry.js.map +1 -0
  140. package/dist/shell/adapter/clock-random.js +96 -0
  141. package/dist/shell/adapter/clock-random.js.map +1 -0
  142. package/dist/shell/adapter/gondolin-dispatch-helpers.js +158 -0
  143. package/dist/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
  144. package/dist/shell/adapter/gondolin-dispatch.js +385 -0
  145. package/dist/shell/adapter/gondolin-dispatch.js.map +1 -0
  146. package/dist/shell/adapter/gondolin-image-converter.js +233 -0
  147. package/dist/shell/adapter/gondolin-image-converter.js.map +1 -0
  148. package/dist/shell/adapter/gondolin-image-fetch.js +180 -0
  149. package/dist/shell/adapter/gondolin-image-fetch.js.map +1 -0
  150. package/dist/shell/adapter/launcher-asset.js +57 -0
  151. package/dist/shell/adapter/launcher-asset.js.map +1 -0
  152. package/dist/shell/adapter/mise-config-asset.js +65 -0
  153. package/dist/shell/adapter/mise-config-asset.js.map +1 -0
  154. package/dist/shell/adapter/workflow-loader.js +304 -0
  155. package/dist/shell/adapter/workflow-loader.js.map +1 -0
  156. package/dist/shell/cli/doctor.js +268 -0
  157. package/dist/shell/cli/doctor.js.map +1 -0
  158. package/dist/shell/effect-interpreter-families.js +314 -0
  159. package/dist/shell/effect-interpreter-families.js.map +1 -0
  160. package/dist/shell/effect-interpreter.js +29 -0
  161. package/dist/shell/effect-interpreter.js.map +1 -0
  162. package/dist/shell/interp/acp-frame.js +137 -0
  163. package/dist/shell/interp/acp-frame.js.map +1 -0
  164. package/dist/shell/interp/acp-ws-conn.js +320 -0
  165. package/dist/shell/interp/acp-ws-conn.js.map +1 -0
  166. package/dist/shell/interp/acp-ws-frames.js +159 -0
  167. package/dist/shell/interp/acp-ws-frames.js.map +1 -0
  168. package/dist/shell/interp/acp-ws.js +197 -0
  169. package/dist/shell/interp/acp-ws.js.map +1 -0
  170. package/dist/shell/interp/acp.js +319 -0
  171. package/dist/shell/interp/acp.js.map +1 -0
  172. package/dist/shell/interp/credential-defaults.js +128 -0
  173. package/dist/shell/interp/credential-defaults.js.map +1 -0
  174. package/dist/shell/interp/credential-hooks.js +149 -0
  175. package/dist/shell/interp/credential-hooks.js.map +1 -0
  176. package/dist/shell/interp/credential-registry.js +226 -0
  177. package/dist/shell/interp/credential-registry.js.map +1 -0
  178. package/dist/shell/interp/credential.js +103 -0
  179. package/dist/shell/interp/credential.js.map +1 -0
  180. package/dist/shell/interp/gh.js +163 -0
  181. package/dist/shell/interp/gh.js.map +1 -0
  182. package/dist/shell/interp/git.js +28 -0
  183. package/dist/shell/interp/git.js.map +1 -0
  184. package/dist/shell/interp/log.js +213 -0
  185. package/dist/shell/interp/log.js.map +1 -0
  186. package/dist/shell/interp/process.js +178 -0
  187. package/dist/shell/interp/process.js.map +1 -0
  188. package/dist/shell/interp/runlog.js +193 -0
  189. package/dist/shell/interp/runlog.js.map +1 -0
  190. package/dist/shell/interp/timer.js +64 -0
  191. package/dist/shell/interp/timer.js.map +1 -0
  192. package/dist/shell/interp/tracker-disk.js +99 -0
  193. package/dist/shell/interp/tracker-disk.js.map +1 -0
  194. package/dist/shell/interp/tracker-parse.js +71 -0
  195. package/dist/shell/interp/tracker-parse.js.map +1 -0
  196. package/dist/shell/interp/tracker-scan.js +238 -0
  197. package/dist/shell/interp/tracker-scan.js.map +1 -0
  198. package/dist/shell/interp/tracker-write.js +91 -0
  199. package/dist/shell/interp/tracker-write.js.map +1 -0
  200. package/dist/shell/interp/tracker.js +41 -0
  201. package/dist/shell/interp/tracker.js.map +1 -0
  202. package/dist/shell/interp/tty.js +48 -0
  203. package/dist/shell/interp/tty.js.map +1 -0
  204. package/dist/shell/interp/vm.js +199 -0
  205. package/dist/shell/interp/vm.js.map +1 -0
  206. package/dist/shell/interp/workspace.js +310 -0
  207. package/dist/shell/interp/workspace.js.map +1 -0
  208. package/dist/shell/main-acp.js +78 -0
  209. package/dist/shell/main-acp.js.map +1 -0
  210. package/dist/shell/main-adapters.js +222 -0
  211. package/dist/shell/main-adapters.js.map +1 -0
  212. package/dist/shell/main-credential.js +122 -0
  213. package/dist/shell/main-credential.js.map +1 -0
  214. package/dist/shell/main-doctor.js +22 -0
  215. package/dist/shell/main-doctor.js.map +1 -0
  216. package/dist/shell/main-entry.js +46 -0
  217. package/dist/shell/main-entry.js.map +1 -0
  218. package/dist/shell/main-http-csrf.js +45 -0
  219. package/dist/shell/main-http-csrf.js.map +1 -0
  220. package/dist/shell/main-http-handler.js +389 -0
  221. package/dist/shell/main-http-handler.js.map +1 -0
  222. package/dist/shell/main-http-mcp.js +122 -0
  223. package/dist/shell/main-http-mcp.js.map +1 -0
  224. package/dist/shell/main-http-views.js +253 -0
  225. package/dist/shell/main-http-views.js.map +1 -0
  226. package/dist/shell/main-http.js +76 -0
  227. package/dist/shell/main-http.js.map +1 -0
  228. package/dist/shell/main-loops.js +130 -0
  229. package/dist/shell/main-loops.js.map +1 -0
  230. package/dist/shell/main-mcp.js +129 -0
  231. package/dist/shell/main-mcp.js.map +1 -0
  232. package/dist/shell/main-orchestrator.js +120 -0
  233. package/dist/shell/main-orchestrator.js.map +1 -0
  234. package/dist/shell/main-preflight.js +43 -0
  235. package/dist/shell/main-preflight.js.map +1 -0
  236. package/dist/shell/main-reconcilers-helpers.js +244 -0
  237. package/dist/shell/main-reconcilers-helpers.js.map +1 -0
  238. package/dist/shell/main-reconcilers-pr.js +148 -0
  239. package/dist/shell/main-reconcilers-pr.js.map +1 -0
  240. package/dist/shell/main-reconcilers.js +225 -0
  241. package/dist/shell/main-reconcilers.js.map +1 -0
  242. package/dist/shell/main-runner.js +355 -0
  243. package/dist/shell/main-runner.js.map +1 -0
  244. package/dist/shell/main-scaffold.js +116 -0
  245. package/dist/shell/main-scaffold.js.map +1 -0
  246. package/dist/shell/main-shutdown.js +115 -0
  247. package/dist/shell/main-shutdown.js.map +1 -0
  248. package/dist/shell/main-startup.js +48 -0
  249. package/dist/shell/main-startup.js.map +1 -0
  250. package/dist/shell/main-substrates.js +43 -0
  251. package/dist/shell/main-substrates.js.map +1 -0
  252. package/dist/shell/main.js +385 -0
  253. package/dist/shell/main.js.map +1 -0
  254. package/dist/shell/orchestrator-feedback.js +69 -0
  255. package/dist/shell/orchestrator-feedback.js.map +1 -0
  256. package/dist/shell/orchestrator-image.js +167 -0
  257. package/dist/shell/orchestrator-image.js.map +1 -0
  258. package/dist/shell/orchestrator-loop.js +468 -0
  259. package/dist/shell/orchestrator-loop.js.map +1 -0
  260. package/dist/shell/orchestrator-reconcile.js +36 -0
  261. package/dist/shell/orchestrator-reconcile.js.map +1 -0
  262. package/dist/shell/reconciler-loop.js +228 -0
  263. package/dist/shell/reconciler-loop.js.map +1 -0
  264. package/dist/shell/runner-loop-turn.js +301 -0
  265. package/dist/shell/runner-loop-turn.js.map +1 -0
  266. package/dist/shell/runner-loop.js +338 -0
  267. package/dist/shell/runner-loop.js.map +1 -0
  268. package/dist/shell/server/http.js +208 -0
  269. package/dist/shell/server/http.js.map +1 -0
  270. package/dist/shell/server/mcp-runtime-effects.js +237 -0
  271. package/dist/shell/server/mcp-runtime-effects.js.map +1 -0
  272. package/dist/shell/server/mcp-runtime.js +99 -0
  273. package/dist/shell/server/mcp-runtime.js.map +1 -0
  274. package/dist/shell/workspace-key.js +14 -0
  275. package/dist/shell/workspace-key.js.map +1 -0
  276. package/dist/types/acp.js +8 -0
  277. package/dist/types/acp.js.map +1 -0
  278. package/dist/types/actions/plan.js +6 -0
  279. package/dist/types/actions/plan.js.map +1 -0
  280. package/dist/types/actions/predicates.js +6 -0
  281. package/dist/types/actions/predicates.js.map +1 -0
  282. package/dist/types/actions/run-fold.js +8 -0
  283. package/dist/types/actions/run-fold.js.map +1 -0
  284. package/dist/types/actions.js +7 -0
  285. package/dist/types/actions.js.map +1 -0
  286. package/dist/types/adapter/clock-random.js +4 -0
  287. package/dist/types/adapter/clock-random.js.map +1 -0
  288. package/dist/types/adapter/gondolin-image-converter.js +5 -0
  289. package/dist/types/adapter/gondolin-image-converter.js.map +1 -0
  290. package/dist/types/adapter/gondolin-image-fetch.js +5 -0
  291. package/dist/types/adapter/gondolin-image-fetch.js.map +1 -0
  292. package/dist/types/adapter/workflow-loader.js +4 -0
  293. package/dist/types/adapter/workflow-loader.js.map +1 -0
  294. package/dist/types/cli/args.js +8 -0
  295. package/dist/types/cli/args.js.map +1 -0
  296. package/dist/types/config.js +8 -0
  297. package/dist/types/config.js.map +1 -0
  298. package/dist/types/credential-interp.js +6 -0
  299. package/dist/types/credential-interp.js.map +1 -0
  300. package/dist/types/credentials.js +10 -0
  301. package/dist/types/credentials.js.map +1 -0
  302. package/dist/types/doctor.js +7 -0
  303. package/dist/types/doctor.js.map +1 -0
  304. package/dist/types/domain.js +7 -0
  305. package/dist/types/domain.js.map +1 -0
  306. package/dist/types/effect.js +15 -0
  307. package/dist/types/effect.js.map +1 -0
  308. package/dist/types/errors.js +39 -0
  309. package/dist/types/errors.js.map +1 -0
  310. package/dist/types/http/decisions.js +6 -0
  311. package/dist/types/http/decisions.js.map +1 -0
  312. package/dist/types/http/render.js +10 -0
  313. package/dist/types/http/render.js.map +1 -0
  314. package/dist/types/http/views.js +6 -0
  315. package/dist/types/http/views.js.map +1 -0
  316. package/dist/types/http.js +9 -0
  317. package/dist/types/http.js.map +1 -0
  318. package/dist/types/image/managed-image.js +7 -0
  319. package/dist/types/image/managed-image.js.map +1 -0
  320. package/dist/types/interp/effect-interpreter.js +8 -0
  321. package/dist/types/interp/effect-interpreter.js.map +1 -0
  322. package/dist/types/interp/tracker.js +7 -0
  323. package/dist/types/interp/tracker.js.map +1 -0
  324. package/dist/types/issue/file.js +6 -0
  325. package/dist/types/issue/file.js.map +1 -0
  326. package/dist/types/issue/parse.js +8 -0
  327. package/dist/types/issue/parse.js.map +1 -0
  328. package/dist/types/main-acp.js +13 -0
  329. package/dist/types/main-acp.js.map +1 -0
  330. package/dist/types/main-adapters.js +5 -0
  331. package/dist/types/main-adapters.js.map +1 -0
  332. package/dist/types/main-credential.js +21 -0
  333. package/dist/types/main-credential.js.map +1 -0
  334. package/dist/types/main-doctor.js +6 -0
  335. package/dist/types/main-doctor.js.map +1 -0
  336. package/dist/types/main-http-handler.js +12 -0
  337. package/dist/types/main-http-handler.js.map +1 -0
  338. package/dist/types/main-http.js +5 -0
  339. package/dist/types/main-http.js.map +1 -0
  340. package/dist/types/main-loops.js +5 -0
  341. package/dist/types/main-loops.js.map +1 -0
  342. package/dist/types/main-mcp.js +12 -0
  343. package/dist/types/main-mcp.js.map +1 -0
  344. package/dist/types/main-orchestrator.js +5 -0
  345. package/dist/types/main-orchestrator.js.map +1 -0
  346. package/dist/types/main-reconcilers.js +11 -0
  347. package/dist/types/main-reconcilers.js.map +1 -0
  348. package/dist/types/main-runner.js +13 -0
  349. package/dist/types/main-runner.js.map +1 -0
  350. package/dist/types/main-startup.js +5 -0
  351. package/dist/types/main-startup.js.map +1 -0
  352. package/dist/types/main-substrates.js +5 -0
  353. package/dist/types/main-substrates.js.map +1 -0
  354. package/dist/types/mcp/dispatch.js +4 -0
  355. package/dist/types/mcp/dispatch.js.map +1 -0
  356. package/dist/types/mcp/post-move.js +7 -0
  357. package/dist/types/mcp/post-move.js.map +1 -0
  358. package/dist/types/mcp.js +9 -0
  359. package/dist/types/mcp.js.map +1 -0
  360. package/dist/types/ports.js +12 -0
  361. package/dist/types/ports.js.map +1 -0
  362. package/dist/types/reconcile/image-decide.js +5 -0
  363. package/dist/types/reconcile/image-decide.js.map +1 -0
  364. package/dist/types/reconcile/ledger.js +7 -0
  365. package/dist/types/reconcile/ledger.js.map +1 -0
  366. package/dist/types/reconcile/pr-loop.js +8 -0
  367. package/dist/types/reconcile/pr-loop.js.map +1 -0
  368. package/dist/types/reconcile/vm-reap.js +8 -0
  369. package/dist/types/reconcile/vm-reap.js.map +1 -0
  370. package/dist/types/reconcile/workspace-decide.js +7 -0
  371. package/dist/types/reconcile/workspace-decide.js.map +1 -0
  372. package/dist/types/reconcile.js +9 -0
  373. package/dist/types/reconcile.js.map +1 -0
  374. package/dist/types/runlog.js +7 -0
  375. package/dist/types/runlog.js.map +1 -0
  376. package/dist/types/runner/actions-runner.js +12 -0
  377. package/dist/types/runner/actions-runner.js.map +1 -0
  378. package/dist/types/runner/gondolin-dispatch.js +5 -0
  379. package/dist/types/runner/gondolin-dispatch.js.map +1 -0
  380. package/dist/types/runner/injection.js +6 -0
  381. package/dist/types/runner/injection.js.map +1 -0
  382. package/dist/types/runner/runner-loop.js +5 -0
  383. package/dist/types/runner/runner-loop.js.map +1 -0
  384. package/dist/types/runner/turn.js +4 -0
  385. package/dist/types/runner/turn.js.map +1 -0
  386. package/dist/types/runner/vm-plan.js +4 -0
  387. package/dist/types/runner/vm-plan.js.map +1 -0
  388. package/dist/types/runtime.js +9 -0
  389. package/dist/types/runtime.js.map +1 -0
  390. package/dist/types/schedule/admission.js +7 -0
  391. package/dist/types/schedule/admission.js.map +1 -0
  392. package/dist/types/schedule/circuit-breaker.js +2 -0
  393. package/dist/types/schedule/circuit-breaker.js.map +1 -0
  394. package/dist/types/schedule/eligibility.js +9 -0
  395. package/dist/types/schedule/eligibility.js.map +1 -0
  396. package/dist/types/schedule/orchestrator-loop.js +10 -0
  397. package/dist/types/schedule/orchestrator-loop.js.map +1 -0
  398. package/dist/types/schedule/sleep-cycle.js +4 -0
  399. package/dist/types/schedule/sleep-cycle.js.map +1 -0
  400. package/dist/types/schedule/slots.js +8 -0
  401. package/dist/types/schedule/slots.js.map +1 -0
  402. package/dist/types/schedule/tick.js +9 -0
  403. package/dist/types/schedule/tick.js.map +1 -0
  404. package/dist/types/server/mcp-runtime.js +8 -0
  405. package/dist/types/server/mcp-runtime.js.map +1 -0
  406. package/dist/types/workflow/parse.js +4 -0
  407. package/dist/types/workflow/parse.js.map +1 -0
  408. package/package.json +22 -10
  409. package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
  410. package/prompts/Reflect.md +91 -0
  411. package/prompts/Review.md +97 -0
  412. package/prompts/Todo.md +96 -0
  413. package/prompts/_footer.md +41 -0
  414. package/prompts/_preamble.md +42 -0
  415. package/prompts-minimal/Todo.md +26 -0
  416. package/scripts/postinstall.mjs +63 -0
  417. package/scripts/vm-agent.mjs +312 -90
  418. package/WORKFLOW.md +0 -744
  419. package/dist/acp-bridge.js +0 -324
  420. package/dist/acp-bridge.js.map +0 -1
  421. package/dist/actions/cache.js +0 -191
  422. package/dist/actions/cache.js.map +0 -1
  423. package/dist/actions/effects.js +0 -41
  424. package/dist/actions/effects.js.map +0 -1
  425. package/dist/actions/executor.js +0 -570
  426. package/dist/actions/executor.js.map +0 -1
  427. package/dist/actions/index.js +0 -13
  428. package/dist/actions/index.js.map +0 -1
  429. package/dist/actions/parsing.js.map +0 -1
  430. package/dist/actions/predicate-env.js +0 -27
  431. package/dist/actions/predicate-env.js.map +0 -1
  432. package/dist/actions/predicates.js +0 -49
  433. package/dist/actions/predicates.js.map +0 -1
  434. package/dist/actions/templating.js +0 -66
  435. package/dist/actions/templating.js.map +0 -1
  436. package/dist/actions/types.js +0 -15
  437. package/dist/actions/types.js.map +0 -1
  438. package/dist/agent/acp.js +0 -473
  439. package/dist/agent/acp.js.map +0 -1
  440. package/dist/agent/adapter-names.js +0 -159
  441. package/dist/agent/adapter-names.js.map +0 -1
  442. package/dist/agent/adapters.js +0 -511
  443. package/dist/agent/adapters.js.map +0 -1
  444. package/dist/agent/credential-extractors.js +0 -342
  445. package/dist/agent/credential-extractors.js.map +0 -1
  446. package/dist/agent/credential-secrets.js +0 -628
  447. package/dist/agent/credential-secrets.js.map +0 -1
  448. package/dist/agent/credential-ticker.js +0 -57
  449. package/dist/agent/credential-ticker.js.map +0 -1
  450. package/dist/agent/gondolin-creds-staging.js +0 -356
  451. package/dist/agent/gondolin-creds-staging.js.map +0 -1
  452. package/dist/agent/gondolin-dispatch.js +0 -375
  453. package/dist/agent/gondolin-dispatch.js.map +0 -1
  454. package/dist/agent/gondolin.js +0 -124
  455. package/dist/agent/gondolin.js.map +0 -1
  456. package/dist/agent/runner-decisions.js +0 -134
  457. package/dist/agent/runner-decisions.js.map +0 -1
  458. package/dist/agent/runner.js +0 -1456
  459. package/dist/agent/runner.js.map +0 -1
  460. package/dist/agent/tool-call-summary.js +0 -102
  461. package/dist/agent/tool-call-summary.js.map +0 -1
  462. package/dist/agent/vm-acp-mapping.js +0 -73
  463. package/dist/agent/vm-acp-mapping.js.map +0 -1
  464. package/dist/agent/vm-guards.js +0 -262
  465. package/dist/agent/vm-guards.js.map +0 -1
  466. package/dist/agent/vm-port.js +0 -22
  467. package/dist/agent/vm-port.js.map +0 -1
  468. package/dist/agent/vm-process-registry.js +0 -79
  469. package/dist/agent/vm-process-registry.js.map +0 -1
  470. package/dist/bin/cli-args.js +0 -105
  471. package/dist/bin/cli-args.js.map +0 -1
  472. package/dist/errors.js +0 -15
  473. package/dist/errors.js.map +0 -1
  474. package/dist/http-disk.js +0 -135
  475. package/dist/http-disk.js.map +0 -1
  476. package/dist/http-handlers.js.map +0 -1
  477. package/dist/http.js.map +0 -1
  478. package/dist/issues.js +0 -178
  479. package/dist/issues.js.map +0 -1
  480. package/dist/logging.js +0 -203
  481. package/dist/logging.js.map +0 -1
  482. package/dist/mcp.js +0 -706
  483. package/dist/mcp.js.map +0 -1
  484. package/dist/memory.js +0 -85
  485. package/dist/memory.js.map +0 -1
  486. package/dist/orchestrator-decisions.js +0 -331
  487. package/dist/orchestrator-decisions.js.map +0 -1
  488. package/dist/orchestrator.js +0 -1569
  489. package/dist/orchestrator.js.map +0 -1
  490. package/dist/prompt.js +0 -65
  491. package/dist/prompt.js.map +0 -1
  492. package/dist/reconciler/cache.js +0 -65
  493. package/dist/reconciler/cache.js.map +0 -1
  494. package/dist/reconciler/index.js +0 -448
  495. package/dist/reconciler/index.js.map +0 -1
  496. package/dist/reconciler/ledger.js +0 -131
  497. package/dist/reconciler/ledger.js.map +0 -1
  498. package/dist/reconciler/pr-adapters.js +0 -174
  499. package/dist/reconciler/pr-adapters.js.map +0 -1
  500. package/dist/reconciler/pr-decide.js.map +0 -1
  501. package/dist/reconciler/pr.js +0 -422
  502. package/dist/reconciler/pr.js.map +0 -1
  503. package/dist/reconciler/types.js +0 -12
  504. package/dist/reconciler/types.js.map +0 -1
  505. package/dist/reconciler/vm.js +0 -243
  506. package/dist/reconciler/vm.js.map +0 -1
  507. package/dist/reconciler/workspace-defaults.js +0 -83
  508. package/dist/reconciler/workspace-defaults.js.map +0 -1
  509. package/dist/reconciler/workspace.js +0 -272
  510. package/dist/reconciler/workspace.js.map +0 -1
  511. package/dist/runlog.js +0 -403
  512. package/dist/runlog.js.map +0 -1
  513. package/dist/scaffold.js +0 -165
  514. package/dist/scaffold.js.map +0 -1
  515. package/dist/trackers/local.js +0 -445
  516. package/dist/trackers/local.js.map +0 -1
  517. package/dist/trackers/types.js +0 -10
  518. package/dist/trackers/types.js.map +0 -1
  519. package/dist/types.js +0 -3
  520. package/dist/types.js.map +0 -1
  521. package/dist/util/clock.js +0 -12
  522. package/dist/util/clock.js.map +0 -1
  523. package/dist/util/crypto.js +0 -25
  524. package/dist/util/crypto.js.map +0 -1
  525. package/dist/util/frontmatter.js +0 -70
  526. package/dist/util/frontmatter.js.map +0 -1
  527. package/dist/util/fs-issues.js +0 -22
  528. package/dist/util/fs-issues.js.map +0 -1
  529. package/dist/util/process.js +0 -152
  530. package/dist/util/process.js.map +0 -1
  531. package/dist/util/workspace-key.js +0 -10
  532. package/dist/util/workspace-key.js.map +0 -1
  533. package/dist/workflow-loader.js +0 -147
  534. package/dist/workflow-loader.js.map +0 -1
  535. package/dist/workflow.js +0 -822
  536. package/dist/workflow.js.map +0 -1
  537. package/dist/workspace-types.js +0 -8
  538. package/dist/workspace-types.js.map +0 -1
  539. package/dist/workspace.js +0 -443
  540. package/dist/workspace.js.map +0 -1
@@ -0,0 +1,687 @@
1
+ // FCIS rewrite — PURE workflow (WORKFLOW.yaml) parser + typed ServiceConfig builder.
2
+ //
3
+ // Ports the original `src/workflow.ts` parse path (parseWorkflow, expandVar,
4
+ // buildServiceConfig) into the functional
5
+ // core. 100% synchronous, zero IO, imports ONLY from src/types. The legacy
6
+ // fenced front-matter + inline Markdown body form was removed: a workflow file
7
+ // is now pure YAML config (prompts live in per-state `prompt_file`s).
8
+ //
9
+ // How the original's impure dependencies are made pure here:
10
+ // * YAML decode (the `yaml` package in the original's parseFrontMatter) is
11
+ // INJECTED as `deps.parseYaml`. The shell wires in `yaml.parse`; the core
12
+ // never imports the library.
13
+ // * `node:path` / `node:os` (path.resolve/dirname/join/isAbsolute, os.homedir)
14
+ // are replaced with pure POSIX helpers below; the home dir comes from the
15
+ // injected WorkflowEnv (HOME), so the parser reads no real environment.
16
+ // * The `actions:` sub-block parser lives in the sibling core module
17
+ // src/core/actions/parse.ts. It is INJECTABLE as `deps.parseActions` (so the
18
+ // shell can curry it) but defaults to that module's `parseActionsBlock`.
19
+ import { WorkflowError } from '../../types/errors.js';
20
+ import { parseActionsBlock } from '../actions/parse.js';
21
+ import { MISE_EGRESS_HOSTS } from '../runner/mise.js';
22
+ import { asString, asIntOr, asStringListOr } from '../coerce.js';
23
+ import { sanitizeWorkspaceKey } from '../workspace-key.js';
24
+ // ─── pure POSIX path helpers (replace node:path; no IO) ──────────────────────
25
+ //
26
+ // The original used node:path. These are deliberately POSIX-only — the
27
+ // orchestrator runs on Linux/macOS hosts — and operate purely on strings. They
28
+ // faithfully reproduce path.{isAbsolute,join,resolve,dirname} for the inputs the
29
+ // parser sees (absolute workflow paths + relative config values). POSIX-only is
30
+ // intentional and matches the original's runtime targets.
31
+ function isAbsolute(p) {
32
+ return p.startsWith('/');
33
+ }
34
+ function normalizeSegments(segments, allowLeadingDotDot) {
35
+ const out = [];
36
+ for (const seg of segments) {
37
+ if (seg === '' || seg === '.')
38
+ continue;
39
+ if (seg === '..') {
40
+ if (out.length > 0 && out[out.length - 1] !== '..')
41
+ out.pop();
42
+ else if (allowLeadingDotDot)
43
+ out.push('..');
44
+ continue;
45
+ }
46
+ out.push(seg);
47
+ }
48
+ return out;
49
+ }
50
+ function joinPath(...parts) {
51
+ const joined = parts.filter((p) => p.length > 0).join('/');
52
+ if (joined === '')
53
+ return '.';
54
+ const absolute = joined.startsWith('/');
55
+ const segs = normalizeSegments(joined.split('/'), !absolute);
56
+ const body = segs.join('/');
57
+ if (absolute)
58
+ return '/' + body;
59
+ return body === '' ? '.' : body;
60
+ }
61
+ /**
62
+ * Resolve `to` against `from` (defaults to a cwd derived from the env). Faithful
63
+ * to path.resolve for the parser's use: an absolute input is normalized; a
64
+ * relative input is joined onto its base. Unlike node:path.resolve we never read
65
+ * the real cwd — a relative base resolves against the injected `cwd`.
66
+ */
67
+ function resolvePath(cwd, ...parts) {
68
+ let resolved = '';
69
+ let isAbs = false;
70
+ // Walk right-to-left like path.resolve, prepending until we hit an absolute.
71
+ for (let i = parts.length - 1; i >= 0 && !isAbs; i--) {
72
+ const part = parts[i];
73
+ if (part.length === 0)
74
+ continue;
75
+ resolved = resolved.length === 0 ? part : part + '/' + resolved;
76
+ isAbs = part.startsWith('/');
77
+ }
78
+ if (!isAbs) {
79
+ resolved = resolved.length === 0 ? cwd : cwd + '/' + resolved;
80
+ }
81
+ const absolute = resolved.startsWith('/');
82
+ const segs = normalizeSegments(resolved.split('/'), false);
83
+ const body = segs.join('/');
84
+ if (absolute)
85
+ return '/' + body;
86
+ return body === '' ? '.' : body;
87
+ }
88
+ function dirname(p) {
89
+ const norm = p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p;
90
+ const idx = norm.lastIndexOf('/');
91
+ if (idx < 0)
92
+ return '.';
93
+ if (idx === 0)
94
+ return '/';
95
+ return norm.slice(0, idx);
96
+ }
97
+ /** Last path segment (replaces node:path.basename for the project-namespace fallback). */
98
+ function basename(p) {
99
+ const norm = p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p;
100
+ const idx = norm.lastIndexOf('/');
101
+ return idx < 0 ? norm : norm.slice(idx + 1);
102
+ }
103
+ /** Home directory from the injected env (replaces os.homedir(); no IO). */
104
+ function homeDir(env) {
105
+ const h = env['HOME'] ?? env['USERPROFILE'];
106
+ return h && h.length > 0 ? h : '/root';
107
+ }
108
+ /** The base cwd a relative workflow path resolves against (env PWD, else "/"). */
109
+ function cwdOf(env) {
110
+ const pwd = env['PWD'];
111
+ return pwd && isAbsolute(pwd) ? pwd : '/';
112
+ }
113
+ // ─── home-based path defaults (issue 210) ────────────────────────────────────
114
+ //
115
+ // When a `*.root` is UNSET, durable operator state defaults under a per-project
116
+ // home base (~/.symphony/<kind>/<project>) instead of inside the repo working
117
+ // tree or tmpdir — so it survives a `rm -rf`/re-clone of the repo (or a reboot,
118
+ // for the old tmpdir workspaces). An EXPLICIT root is unaffected: it still
119
+ // resolves exactly as before (absolute as-is, `~`-expanded, relative →
120
+ // workflowDir). Only the unset-default branch routes through here.
121
+ /**
122
+ * The `<project>` namespace for the home-based path defaults. Derived from the
123
+ * `workspace.github_repo` slug (owner/repo → repo), falling back to the
124
+ * workflow-dir basename. Sanitized with the canonical workspace-key sanitizer so
125
+ * two different projects on one machine never collide under the shared
126
+ * `~/.symphony/<kind>/` base (the supported topology is one instance per project,
127
+ * many across DIFFERENT projects). An empty result falls back to "symphony".
128
+ */
129
+ function deriveProjectNamespace(githubRepo, workflowDir) {
130
+ const raw = githubRepo ? (githubRepo.split('/').pop() ?? '') : basename(workflowDir);
131
+ const sanitized = sanitizeWorkspaceKey(raw.trim());
132
+ return sanitized.length > 0 ? sanitized : 'symphony';
133
+ }
134
+ /** Home-based default root for an unset `*.root`: ~/.symphony/<kind>/<project>. */
135
+ function homeDefaultRoot(env, kind, project) {
136
+ return joinPath(homeDir(env), '.symphony', kind, project);
137
+ }
138
+ // ─── workflow YAML decode (§4.2) ──────────────────────────────────────────────
139
+ /**
140
+ * Decode a workflow file (pure YAML) to a config map. A workflow file is pure
141
+ * YAML config — the whole file decodes to the config map and prompts come from
142
+ * per-state `prompt_file`s (+ the shared `prompt.preamble_file`/`footer_file`).
143
+ * Empty/whitespace decodes to `{}`; a non-map (list, scalar) throws
144
+ * `workflow_front_matter_not_a_map`; any other decode failure throws
145
+ * `workflow_parse_error`.
146
+ */
147
+ export function decodeYamlMap(text, parseYaml) {
148
+ let parsed;
149
+ try {
150
+ parsed = text.trim().length === 0 ? {} : parseYaml(text);
151
+ }
152
+ catch (err) {
153
+ throw new WorkflowError('workflow_parse_error', `invalid workflow YAML: ${err.message}`);
154
+ }
155
+ if (parsed === null || parsed === undefined)
156
+ parsed = {};
157
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
158
+ throw new WorkflowError('workflow_front_matter_not_a_map', 'workflow YAML must decode to a map');
159
+ }
160
+ return parsed;
161
+ }
162
+ /**
163
+ * Pure entry point: decode the workflow YAML, build the typed view, and return
164
+ * both shapes. `env` supplies the `$VAR`/`~` variable map (the shell loader
165
+ * passes process.env; tests pass an explicit shape). `deps.parseYaml` decodes
166
+ * the config; `deps.parseActions` parses per-state `actions:` blocks.
167
+ */
168
+ export function parseWorkflow(text, workflowPath, deps, env = {}) {
169
+ // A workflow file is pure YAML config: the whole file decodes to the config
170
+ // map and each active state renders its own `prompt_file` (wrapped in the
171
+ // shared `prompt.preamble_file`/`footer_file`). The legacy fenced
172
+ // front-matter + inline Markdown body form was removed (no backward compat).
173
+ const raw = decodeYamlMap(text, deps.parseYaml);
174
+ const definition = { config: raw };
175
+ const built = buildServiceConfig(raw, workflowPath, deps, env);
176
+ return { definition, config: built.config };
177
+ }
178
+ // ─── $VAR / ~ expansion (pure) ────────────────────────────────────────────────
179
+ /**
180
+ * `$VAR` / `~` expansion for path/command fields. `~`/`~/x` expand against the
181
+ * injected env's HOME; a bare `$NAME` resolves from `env` (empty string when
182
+ * unset, matching the original). Other strings pass through unchanged.
183
+ */
184
+ export function expandVar(value, env = {}) {
185
+ if (typeof value !== 'string')
186
+ return value;
187
+ let s = value;
188
+ if (s.startsWith('~/') || s === '~') {
189
+ s = s === '~' ? homeDir(env) : joinPath(homeDir(env), s.slice(2));
190
+ }
191
+ const m = s.match(/^\$([A-Z_][A-Z0-9_]*)$/);
192
+ if (m) {
193
+ const envVal = env[m[1]];
194
+ return envVal ?? '';
195
+ }
196
+ return s;
197
+ }
198
+ // ─── scalar coercion helpers ─────────────────────────────────────────────────
199
+ // asString / asInt / asStringList are the canonical coercers from core/coerce.ts
200
+ // (asString imported directly; the fallback-taking config forms are local
201
+ // aliases over asIntOr / asStringListOr so the 20+ call sites are unchanged).
202
+ const asInt = asIntOr;
203
+ const asStringList = asStringListOr;
204
+ /** A non-empty (post-trim) string, or the fallback (so a blank config value defaults). */
205
+ function nonEmptyOr(v, fallback) {
206
+ if (typeof v === 'string' && v.trim().length > 0)
207
+ return v.trim();
208
+ return fallback;
209
+ }
210
+ function getObject(parent, key) {
211
+ const v = parent[key];
212
+ if (v && typeof v === 'object' && !Array.isArray(v))
213
+ return v;
214
+ return {};
215
+ }
216
+ // workspace.github_repo validation: a literal `owner/repo` slug, or null. See the
217
+ // original for the rationale (reject whole-URL/SSH/bare names at parse time).
218
+ function parseGithubRepo(input) {
219
+ if (input === undefined || input === null)
220
+ return null;
221
+ if (typeof input !== 'string') {
222
+ throw new WorkflowError('workflow_parse_error', `workspace.github_repo must be a string "owner/repo" slug or "none" (got ${typeof input})`);
223
+ }
224
+ const trimmed = input.trim();
225
+ if (trimmed === '' || trimmed.toLowerCase() === 'none')
226
+ return null;
227
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/.test(trimmed)) {
228
+ throw new WorkflowError('workflow_parse_error', `workspace.github_repo must be a GitHub "owner/repo" slug or "none" (got: ${input})`);
229
+ }
230
+ return trimmed;
231
+ }
232
+ // gondolin.oci_pull_policy: the OCI pull behavior before conversion (issue 206).
233
+ // Defaults to `if-not-present`; rejects any other string at parse time so a typo
234
+ // surfaces here, not at the first reconcile build.
235
+ function parseOciPullPolicy(input) {
236
+ if (input === undefined || input === null)
237
+ return 'if-not-present';
238
+ const s = asString(input);
239
+ if (s === 'if-not-present' || s === 'always' || s === 'never')
240
+ return s;
241
+ throw new WorkflowError('workflow_parse_error', `gondolin.oci_pull_policy must be one of if-not-present|always|never (got: ${String(input)})`);
242
+ }
243
+ // gondolin.oci_runtime: the container runtime for the conversion pull/export
244
+ // (issue 206). Null/unset = auto-detect; rejects any value other than docker/podman.
245
+ function parseOciRuntime(input) {
246
+ if (input === undefined || input === null)
247
+ return null;
248
+ const s = asString(input);
249
+ if (s === null || s.trim().length === 0)
250
+ return null;
251
+ const trimmed = s.trim();
252
+ if (trimmed === 'docker' || trimmed === 'podman')
253
+ return trimmed;
254
+ throw new WorkflowError('workflow_parse_error', `gondolin.oci_runtime must be docker or podman (got: ${String(input)})`);
255
+ }
256
+ /**
257
+ * Build a fully typed ServiceConfig from a parsed workflow YAML config map. Pure. The
258
+ * `env` supplies the `$VAR`/`~` variable map; `deps.parseActions` parses per-state
259
+ * `actions:` blocks.
260
+ */
261
+ export function buildServiceConfig(raw, workflowPath, deps, env = {}) {
262
+ const cwd = cwdOf(env);
263
+ const parseActions = deps.parseActions ?? parseActionsBlock;
264
+ const workflowAbs = resolvePath(cwd, workflowPath);
265
+ const workflowDir = dirname(workflowAbs);
266
+ // Home-default project namespace (issue 210): parse workspace.github_repo once
267
+ // here so the unset-root defaults of tracker / workspaces / logs can land under
268
+ // ~/.symphony/<kind>/<project>. (workspaceRaw + githubRepo are reused by the
269
+ // workspace block below.)
270
+ const workspaceRaw = getObject(raw, 'workspace');
271
+ const githubRepo = parseGithubRepo(workspaceRaw['github_repo']);
272
+ const projectNamespace = deriveProjectNamespace(githubRepo, workflowDir);
273
+ // tracker (§4.3.1)
274
+ const trackerRaw = getObject(raw, 'tracker');
275
+ const trackerKind = (asString(trackerRaw['kind']) ?? '').trim();
276
+ const trackerRootRaw = asString(trackerRaw['root']);
277
+ let trackerRoot = null;
278
+ if (trackerRootRaw) {
279
+ const expanded = expandVar(trackerRootRaw, env);
280
+ if (expanded === '') {
281
+ throw new WorkflowError('workflow_parse_error', `tracker.root references an unset variable: ${trackerRootRaw}`);
282
+ }
283
+ trackerRoot = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
284
+ }
285
+ else if (trackerKind === 'local') {
286
+ trackerRoot = homeDefaultRoot(env, 'trackers', projectNamespace);
287
+ }
288
+ const states = parseStatesBlock(raw['states'], parseActions);
289
+ const tracker = { kind: trackerKind, states, root: trackerRoot };
290
+ // prompt files (per-state prompt split). Resolve a workflow-relative or
291
+ // `$VAR`/`~` path to an absolute path the shell loader reads off disk. Shared
292
+ // by the per-state `prompt_file` and the `prompt.{preamble,footer}_file`.
293
+ const resolvePromptPath = (value, label) => {
294
+ const expanded = expandVar(value, env);
295
+ if (expanded === '') {
296
+ throw new WorkflowError('workflow_parse_error', `${label} references an unset variable: ${value}`);
297
+ }
298
+ return isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
299
+ };
300
+ for (const [name, sc] of Object.entries(states)) {
301
+ if (sc.prompt_file)
302
+ sc.prompt_file = resolvePromptPath(sc.prompt_file, `state "${name}": prompt_file`);
303
+ }
304
+ const promptRaw = getObject(raw, 'prompt');
305
+ const preambleFileRaw = asString(promptRaw['preamble_file']);
306
+ const footerFileRaw = asString(promptRaw['footer_file']);
307
+ const prompt = {
308
+ preamble_file: preambleFileRaw && preambleFileRaw.trim().length > 0
309
+ ? resolvePromptPath(preambleFileRaw.trim(), 'prompt.preamble_file')
310
+ : null,
311
+ footer_file: footerFileRaw && footerFileRaw.trim().length > 0
312
+ ? resolvePromptPath(footerFileRaw.trim(), 'prompt.footer_file')
313
+ : null,
314
+ };
315
+ // polling (§4.3.2)
316
+ const pollingRaw = getObject(raw, 'polling');
317
+ const polling = { interval_ms: asInt(pollingRaw['interval_ms'], 30_000) };
318
+ // workspace (§4.3.3). workspaceRaw + githubRepo were parsed above (the home
319
+ // project namespace needs github_repo before the tracker default).
320
+ const wsRootInput = asString(workspaceRaw['root']);
321
+ let workspaceRoot;
322
+ if (wsRootInput) {
323
+ const expanded = expandVar(wsRootInput, env);
324
+ if (expanded === '') {
325
+ throw new WorkflowError('workflow_parse_error', `workspace.root references an unset variable: ${wsRootInput}`);
326
+ }
327
+ workspaceRoot = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
328
+ }
329
+ else {
330
+ workspaceRoot = homeDefaultRoot(env, 'workspaces', projectNamespace);
331
+ }
332
+ const baseBranchInput = asString(workspaceRaw['base_branch']);
333
+ const baseBranch = baseBranchInput && baseBranchInput.trim().length > 0 ? baseBranchInput.trim() : 'main';
334
+ const workspace = {
335
+ root: resolvePath(cwd, workspaceRoot),
336
+ github_repo: githubRepo,
337
+ base_branch: baseBranch,
338
+ };
339
+ // logs (symphony extension): per-issue JSONL run logs.
340
+ const logsRaw = getObject(raw, 'logs');
341
+ const logsRootInput = asString(logsRaw['root']);
342
+ let logsRoot;
343
+ if (logsRootInput) {
344
+ const expanded = expandVar(logsRootInput, env);
345
+ if (expanded === '') {
346
+ throw new WorkflowError('workflow_parse_error', `logs.root references an unset variable: ${logsRootInput}`);
347
+ }
348
+ logsRoot = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
349
+ }
350
+ else {
351
+ logsRoot = homeDefaultRoot(env, 'logs', projectNamespace);
352
+ }
353
+ const logs = { root: resolvePath(cwd, logsRoot) };
354
+ // agent (§4.3.4)
355
+ const agentRaw = getObject(raw, 'agent');
356
+ const maxTurns = asInt(agentRaw['max_turns'], 20);
357
+ if (maxTurns <= 0) {
358
+ throw new WorkflowError('workflow_parse_error', 'agent.max_turns must be positive');
359
+ }
360
+ const memAdmissionEnabledRaw = agentRaw['memory_admission_enabled'];
361
+ const memoryAdmissionEnabled = memAdmissionEnabledRaw === undefined ? true : memAdmissionEnabledRaw !== false;
362
+ const hostMemoryReserveMib = asInt(agentRaw['host_memory_reserve_mib'], 2048);
363
+ if (hostMemoryReserveMib < 0) {
364
+ throw new WorkflowError('workflow_parse_error', 'agent.host_memory_reserve_mib must be a non-negative integer');
365
+ }
366
+ const circuitBreakerThreshold = asInt(agentRaw['circuit_breaker_threshold'], 5);
367
+ if (circuitBreakerThreshold < 0 || circuitBreakerThreshold === 1) {
368
+ throw new WorkflowError('workflow_parse_error', 'agent.circuit_breaker_threshold must be 0 (disabled) or an integer >= 2');
369
+ }
370
+ const agent = {
371
+ max_concurrent_agents: asInt(agentRaw['max_concurrent_agents'], 10),
372
+ max_turns: maxTurns,
373
+ max_retry_backoff_ms: asInt(agentRaw['max_retry_backoff_ms'], 300_000),
374
+ memory_admission_enabled: memoryAdmissionEnabled,
375
+ host_memory_reserve_mib: hostMemoryReserveMib,
376
+ circuit_breaker_threshold: circuitBreakerThreshold,
377
+ };
378
+ // acp (Symphony extension; see §4.3.5)
379
+ const acpRaw = getObject(raw, 'acp');
380
+ const modelRaw = asString(acpRaw['model']);
381
+ const modelTrimmed = modelRaw === null ? null : modelRaw.trim();
382
+ const effortRaw = asString(acpRaw['effort']);
383
+ const effortTrimmed = effortRaw === null ? null : effortRaw.trim();
384
+ const acp = {
385
+ adapter: asString(acpRaw['adapter']) ?? 'claude',
386
+ model: modelTrimmed && modelTrimmed.length > 0 ? modelTrimmed : null,
387
+ effort: effortTrimmed && effortTrimmed.length > 0 ? effortTrimmed : null,
388
+ shell: asString(acpRaw['shell']) ?? 'bash',
389
+ prompt_timeout_ms: asInt(acpRaw['prompt_timeout_ms'], 3_600_000),
390
+ read_timeout_ms: asInt(acpRaw['read_timeout_ms'], 30_000),
391
+ stall_timeout_ms: asInt(acpRaw['stall_timeout_ms'], 300_000),
392
+ connect_timeout_ms: asInt(acpRaw['connect_timeout_ms'], 30_000),
393
+ heartbeat_interval_ms: asInt(acpRaw['heartbeat_interval_ms'], 15_000),
394
+ heartbeat_timeout_ms: asInt(acpRaw['heartbeat_timeout_ms'], 45_000),
395
+ };
396
+ // credentials extension (issue 113)
397
+ const credentialsRaw = getObject(raw, 'credentials');
398
+ const credentials = {
399
+ ticker_interval_ms: asInt(credentialsRaw['ticker_interval_ms'], 6 * 60 * 60 * 1000),
400
+ refresh_margin_ms: asInt(credentialsRaw['refresh_margin_ms'], 5 * 60 * 1000),
401
+ };
402
+ // gondolin VM extension
403
+ const gondolinRaw = getObject(raw, 'gondolin');
404
+ const volumesRaw = gondolinRaw['volumes'];
405
+ const volumes = Array.isArray(volumesRaw)
406
+ ? volumesRaw.flatMap((v) => {
407
+ if (!v || typeof v !== 'object' || Array.isArray(v))
408
+ return [];
409
+ const m = v;
410
+ const hostRaw = asString(m['host']);
411
+ const guest = asString(m['guest']);
412
+ if (!hostRaw || !guest)
413
+ return [];
414
+ const expandedHost = expandVar(hostRaw, env);
415
+ if (expandedHost === '')
416
+ return [];
417
+ const host = isAbsolute(expandedHost)
418
+ ? expandedHost
419
+ : resolvePath(workflowDir, expandedHost);
420
+ const readonly = m['readonly'] === true;
421
+ return [{ host, guest, readonly }];
422
+ })
423
+ : [];
424
+ const ociImageRaw = asString(gondolinRaw['oci_image']);
425
+ // gondolin.mise (issue 209): mise provisioning is UNCONDITIONAL (issue 233) — the
426
+ // base image ships only `mise`, so this is the sole toolchain path; only `data_dir`
427
+ // is configurable. data_dir defaults to a flat, home-based SHARED store
428
+ // ~/.symphony/mise (NOT per-project — mise dedups installs by tool@version across
429
+ // VMs). An explicit data_dir resolves like any other root (absolute as-is,
430
+ // `~`/`$VAR`-expanded, relative → workflowDir). A stale `enabled:` key is simply
431
+ // ignored (consistent with the parser's unknown-key tolerance), never a hard error.
432
+ const miseRaw = getObject(gondolinRaw, 'mise');
433
+ const miseDataDirInput = asString(miseRaw['data_dir']);
434
+ let miseDataDir;
435
+ if (miseDataDirInput && miseDataDirInput.trim().length > 0) {
436
+ const expanded = expandVar(miseDataDirInput.trim(), env);
437
+ if (expanded === '') {
438
+ throw new WorkflowError('workflow_parse_error', `gondolin.mise.data_dir references an unset variable: ${miseDataDirInput}`);
439
+ }
440
+ miseDataDir = isAbsolute(expanded) ? expanded : resolvePath(workflowDir, expanded);
441
+ }
442
+ else {
443
+ miseDataDir = joinPath(homeDir(env), '.symphony', 'mise');
444
+ }
445
+ // gondolin image source precedence (issue 224): explicit `image` > `oci_image` >
446
+ // managed. `image: managed` is a SENTINEL that selects the managed prebuilt-asset
447
+ // source (parsed to `managed_image: true` + `image: null` so the sentinel never
448
+ // reaches `resolveImagePath`). An unset image+oci_image DEFAULTS to managed so a
449
+ // fresh scaffold is `scaffold → run`. An explicit `image` (escape hatch) or
450
+ // `oci_image` (auto-convert) keeps `managed_image` false.
451
+ const rawImage = asString(gondolinRaw['image']);
452
+ const trimmedImage = rawImage && rawImage.trim().length > 0 ? rawImage.trim() : null;
453
+ const ociImage = ociImageRaw && ociImageRaw.trim().length > 0 ? ociImageRaw.trim() : null;
454
+ const managedSentinel = trimmedImage !== null && trimmedImage.toLowerCase() === 'managed';
455
+ const escapeHatchImage = managedSentinel ? null : trimmedImage;
456
+ const managedImage = managedSentinel || (escapeHatchImage === null && ociImage === null);
457
+ const gondolin = {
458
+ image: escapeHatchImage,
459
+ managed_image: managedImage,
460
+ oci_image: ociImage,
461
+ oci_pull_policy: parseOciPullPolicy(gondolinRaw['oci_pull_policy']),
462
+ oci_runtime: parseOciRuntime(gondolinRaw['oci_runtime']),
463
+ cpus: asInt(gondolinRaw['cpus'], 2),
464
+ mem_mib: asInt(gondolinRaw['mem_mib'], 2048),
465
+ // rootfs_size (issue 222): the ephemeral mise install lives on the guest rootfs, which
466
+ // the ~593 MB default is too small for — grow it (qemu size syntax). Default 3G.
467
+ rootfs_size: nonEmptyOr(asString(gondolinRaw['rootfs_size']), '3G'),
468
+ volumes,
469
+ forward_env: asStringList(gondolinRaw['forward_env'], ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']),
470
+ guest_agent_path: nonEmptyOr(asString(gondolinRaw['guest_agent_path']), '/opt/symphony/vm-agent.mjs'),
471
+ node_bin: nonEmptyOr(asString(gondolinRaw['node_bin']), 'node'),
472
+ mise: { data_dir: miseDataDir },
473
+ };
474
+ // egress firewall. mise provisioning is unconditional (issue 233), so when the
475
+ // workflow declares NO explicit allowed_hosts the default is ALWAYS the mise
476
+ // provisioning hosts (nodejs.org / npm registry / mise registry) so `mise install`
477
+ // of the staged SYSTEM config isn't silently firewall-blocked (issue 209). An
478
+ // explicit `allowed_hosts` REPLACES this default — a consuming repo adding
479
+ // project-toolchain hosts must re-list the mise ones.
480
+ const egressRaw = getObject(raw, 'egress');
481
+ const egress = {
482
+ allowed_hosts: asStringList(egressRaw['allowed_hosts'], [...MISE_EGRESS_HOSTS]),
483
+ };
484
+ // server extension (§9.5)
485
+ const serverRaw = getObject(raw, 'server');
486
+ const server = {
487
+ port: typeof serverRaw['port'] === 'number' ? serverRaw['port'] : null,
488
+ host: asString(serverRaw['host']) ?? '127.0.0.1',
489
+ };
490
+ // mcp extension
491
+ const mcpRaw = getObject(raw, 'mcp');
492
+ const mcpEnabledRaw = mcpRaw['enabled'];
493
+ const mcpEnabled = mcpEnabledRaw === undefined ? true : mcpEnabledRaw !== false;
494
+ const mcp = {
495
+ enabled: mcpEnabled,
496
+ host: asString(mcpRaw['host']) ?? '127.0.0.1',
497
+ explicit_host_url: asString(mcpRaw['host_url']),
498
+ };
499
+ // pr (issue 38, slimmed in issue 139)
500
+ const prRaw = getObject(raw, 'pr');
501
+ const pr = {
502
+ enabled: prRaw['enabled'] === true,
503
+ poll_interval_ms: asInt(prRaw['poll_interval_ms'], 30_000),
504
+ };
505
+ if (pr.poll_interval_ms < 0) {
506
+ throw new WorkflowError('workflow_parse_error', 'pr.poll_interval_ms must be non-negative');
507
+ }
508
+ const config = {
509
+ workflow_path: workflowAbs,
510
+ workflow_dir: workflowDir,
511
+ tracker,
512
+ prompt,
513
+ polling,
514
+ workspace,
515
+ logs,
516
+ agent,
517
+ acp,
518
+ gondolin,
519
+ egress,
520
+ server,
521
+ mcp,
522
+ pr,
523
+ credentials,
524
+ states,
525
+ };
526
+ return { config };
527
+ }
528
+ // ─── per-state spawn block (recurring reflection spawn) ───────────────────────
529
+ function parseStateSpawnBlock(stateName, raw) {
530
+ if (raw === undefined || raw === null)
531
+ return undefined;
532
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
533
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": spawn must be a map (on_idle / after_terminal / title / body / max_in_flight)`);
534
+ }
535
+ const m = raw;
536
+ const afterTerminal = asInt(m['after_terminal'], 0);
537
+ if (afterTerminal < 0) {
538
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": spawn.after_terminal must be a non-negative integer (0 disables the terminal-count trigger)`);
539
+ }
540
+ const maxInFlight = asInt(m['max_in_flight'], 1);
541
+ if (maxInFlight < 1) {
542
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": spawn.max_in_flight must be a positive integer (>= 1)`);
543
+ }
544
+ const titleRaw = asString(m['title']);
545
+ const title = titleRaw && titleRaw.trim().length > 0 ? titleRaw.trim() : 'Reflection {{ stamp }}';
546
+ const bodyRaw = asString(m['body']);
547
+ const body = bodyRaw && bodyRaw.trim().length > 0 ? bodyRaw.trim() : null;
548
+ return {
549
+ on_idle: m['on_idle'] === true,
550
+ after_terminal: afterTerminal,
551
+ title,
552
+ body,
553
+ max_in_flight: maxInFlight,
554
+ };
555
+ }
556
+ // ─── states block (§4.3, mandatory) ──────────────────────────────────────────
557
+ function parseStatesBlock(raw, parseActions) {
558
+ if (raw === undefined || raw === null) {
559
+ throw new WorkflowError('workflow_parse_error', 'workflow YAML must declare a top-level `states:` block with at least one active, one terminal, and one holding state. See WORKFLOW.template.yaml for the schema.');
560
+ }
561
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
562
+ throw new WorkflowError('workflow_parse_error', 'states: must be a map of name → config');
563
+ }
564
+ const entries = Object.entries(raw);
565
+ if (entries.length === 0) {
566
+ throw new WorkflowError('workflow_parse_error', 'workflow YAML `states:` block is empty; declare at least one active, one terminal, and one holding state. See WORKFLOW.template.yaml for the schema.');
567
+ }
568
+ const out = {};
569
+ for (const [name, value] of entries) {
570
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
571
+ throw new WorkflowError('workflow_parse_error', `state "${name}": value must be a map`);
572
+ }
573
+ const m = value;
574
+ const roleRaw = asString(m['role']);
575
+ if (roleRaw !== 'active' && roleRaw !== 'terminal' && roleRaw !== 'holding') {
576
+ throw new WorkflowError('workflow_parse_error', `state "${name}": role must be one of active|terminal|holding (got: ${String(m['role'])})`);
577
+ }
578
+ const adapter = asString(m['adapter']);
579
+ const modelRaw = asString(m['model']);
580
+ const modelTrimmed = modelRaw === null ? undefined : modelRaw.trim();
581
+ const model = modelTrimmed === undefined ? undefined : modelTrimmed.length > 0 ? modelTrimmed : null;
582
+ const effortRaw = asString(m['effort']);
583
+ const effortTrimmed = effortRaw === null ? undefined : effortRaw.trim();
584
+ const effort = effortTrimmed === undefined ? undefined : effortTrimmed.length > 0 ? effortTrimmed : null;
585
+ let maxTurns;
586
+ if (m['max_turns'] !== undefined) {
587
+ const n = asInt(m['max_turns'], -1);
588
+ if (n <= 0) {
589
+ throw new WorkflowError('workflow_parse_error', `state "${name}": max_turns must be a positive integer`);
590
+ }
591
+ maxTurns = n;
592
+ }
593
+ let maxConcurrent;
594
+ if (m['max_concurrent'] !== undefined) {
595
+ const n = asInt(m['max_concurrent'], -1);
596
+ if (n <= 0) {
597
+ throw new WorkflowError('workflow_parse_error', `state "${name}": max_concurrent must be a positive integer`);
598
+ }
599
+ maxConcurrent = n;
600
+ }
601
+ let allowed;
602
+ if (m['allowed_transitions'] === undefined) {
603
+ allowed = undefined;
604
+ }
605
+ else if (m['allowed_transitions'] === null) {
606
+ allowed = null;
607
+ }
608
+ else if (Array.isArray(m['allowed_transitions'])) {
609
+ allowed = m['allowed_transitions'].filter((x) => typeof x === 'string');
610
+ }
611
+ else {
612
+ throw new WorkflowError('workflow_parse_error', `state "${name}": allowed_transitions must be a list of state names (or null/omitted)`);
613
+ }
614
+ const stateActions = parseActions(name, m['actions']);
615
+ const evalModeRaw = m['eval_mode'];
616
+ if (evalModeRaw !== undefined && typeof evalModeRaw !== 'boolean') {
617
+ throw new WorkflowError('workflow_parse_error', `state "${name}": eval_mode must be a boolean (true/false)`);
618
+ }
619
+ const statePr = parseStatePrBlock(name, m['pr']);
620
+ const stateSpawn = parseStateSpawnBlock(name, m['spawn']);
621
+ const promptFileRaw = asString(m['prompt_file']);
622
+ const sc = { role: roleRaw };
623
+ if (adapter !== null)
624
+ sc.adapter = adapter;
625
+ // Stored as the raw (trimmed) string here; buildServiceConfig resolves it to
626
+ // an absolute path against the workflow dir (it owns the env + path helpers).
627
+ if (promptFileRaw && promptFileRaw.trim().length > 0)
628
+ sc.prompt_file = promptFileRaw.trim();
629
+ if (model !== undefined)
630
+ sc.model = model;
631
+ if (effort !== undefined)
632
+ sc.effort = effort;
633
+ if (maxTurns !== undefined)
634
+ sc.max_turns = maxTurns;
635
+ if (maxConcurrent !== undefined)
636
+ sc.max_concurrent = maxConcurrent;
637
+ if (allowed !== undefined)
638
+ sc.allowed_transitions = allowed;
639
+ if (stateActions !== undefined)
640
+ sc.actions = stateActions;
641
+ if (evalModeRaw === true)
642
+ sc.eval_mode = true;
643
+ if (statePr !== undefined)
644
+ sc.pr = statePr;
645
+ if (stateSpawn !== undefined)
646
+ sc.spawn = stateSpawn;
647
+ out[name] = sc;
648
+ }
649
+ return out;
650
+ }
651
+ // ─── per-state pr block (issue 139) ────────────────────────────────────────────
652
+ function parseStatePrBlock(stateName, raw) {
653
+ if (raw === undefined || raw === null)
654
+ return undefined;
655
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
656
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr must be a map (auto_merge / on_conflict / close)`);
657
+ }
658
+ const m = raw;
659
+ const out = {};
660
+ if (m['auto_merge'] !== undefined && m['auto_merge'] !== null) {
661
+ const s = asString(m['auto_merge']);
662
+ if (s !== 'squash' && s !== 'merge' && s !== 'rebase') {
663
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.auto_merge must be one of squash|merge|rebase`);
664
+ }
665
+ out.auto_merge = s;
666
+ }
667
+ if (m['on_conflict'] !== undefined && m['on_conflict'] !== null) {
668
+ const oc = m['on_conflict'];
669
+ if (typeof oc !== 'object' || Array.isArray(oc)) {
670
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.on_conflict must be a map with a route_to field`);
671
+ }
672
+ const routeTo = asString(oc['route_to']);
673
+ if (!routeTo || routeTo.trim().length === 0) {
674
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.on_conflict.route_to must be a non-empty state name`);
675
+ }
676
+ out.on_conflict = { route_to: routeTo.trim() };
677
+ }
678
+ if (m['close'] !== undefined && m['close'] !== null) {
679
+ if (typeof m['close'] !== 'boolean') {
680
+ throw new WorkflowError('workflow_parse_error', `state "${stateName}": pr.close must be a boolean`);
681
+ }
682
+ if (m['close'] === true)
683
+ out.close = true;
684
+ }
685
+ return Object.keys(out).length > 0 ? out : undefined;
686
+ }
687
+ //# sourceMappingURL=parse.js.map