smol-symphony 0.2.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 (716) 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/src/bin/symphony.js +30 -0
  11. package/dist/src/bin/symphony.js.map +1 -0
  12. package/dist/src/core/actions/context.js +109 -0
  13. package/dist/src/core/actions/context.js.map +1 -0
  14. package/dist/{actions/parsing.js → src/core/actions/parse.js} +33 -114
  15. package/dist/src/core/actions/parse.js.map +1 -0
  16. package/dist/src/core/actions/plan.js +197 -0
  17. package/dist/src/core/actions/plan.js.map +1 -0
  18. package/dist/src/core/actions/predicates.js +111 -0
  19. package/dist/src/core/actions/predicates.js.map +1 -0
  20. package/dist/src/core/actions/run-fold.js +248 -0
  21. package/dist/src/core/actions/run-fold.js.map +1 -0
  22. package/dist/src/core/actions/template.js +118 -0
  23. package/dist/src/core/actions/template.js.map +1 -0
  24. package/dist/src/core/cli/args.js +116 -0
  25. package/dist/src/core/cli/args.js.map +1 -0
  26. package/dist/src/core/coerce.js +75 -0
  27. package/dist/src/core/coerce.js.map +1 -0
  28. package/dist/src/core/credential/account-id.js +20 -0
  29. package/dist/src/core/credential/account-id.js.map +1 -0
  30. package/dist/src/core/credential/adapter-config.js +136 -0
  31. package/dist/src/core/credential/adapter-config.js.map +1 -0
  32. package/dist/src/core/credential/availability.js +98 -0
  33. package/dist/src/core/credential/availability.js.map +1 -0
  34. package/dist/src/core/credential/extract.js +228 -0
  35. package/dist/src/core/credential/extract.js.map +1 -0
  36. package/dist/src/core/credential/fake-creds.js +171 -0
  37. package/dist/src/core/credential/fake-creds.js.map +1 -0
  38. package/dist/src/core/credential/identity.js +125 -0
  39. package/dist/src/core/credential/identity.js.map +1 -0
  40. package/dist/src/core/credential/shape.js +230 -0
  41. package/dist/src/core/credential/shape.js.map +1 -0
  42. package/dist/src/core/credential/strings.js +15 -0
  43. package/dist/src/core/credential/strings.js.map +1 -0
  44. package/dist/src/core/doctor/checks.js +303 -0
  45. package/dist/src/core/doctor/checks.js.map +1 -0
  46. package/dist/src/core/git/result.js +107 -0
  47. package/dist/src/core/git/result.js.map +1 -0
  48. package/dist/src/core/http/decisions.js +225 -0
  49. package/dist/src/core/http/decisions.js.map +1 -0
  50. package/dist/{http.js → src/core/http/render.js} +472 -738
  51. package/dist/src/core/http/render.js.map +1 -0
  52. package/dist/{http-handlers.js → src/core/http/routes.js} +52 -87
  53. package/dist/src/core/http/routes.js.map +1 -0
  54. package/dist/src/core/http/views.js +181 -0
  55. package/dist/src/core/http/views.js.map +1 -0
  56. package/dist/src/core/image/managed-image.js +95 -0
  57. package/dist/src/core/image/managed-image.js.map +1 -0
  58. package/dist/src/core/issue/file.js +149 -0
  59. package/dist/src/core/issue/file.js.map +1 -0
  60. package/dist/src/core/issue/parse.js +210 -0
  61. package/dist/src/core/issue/parse.js.map +1 -0
  62. package/dist/src/core/mcp/dispatch.js +239 -0
  63. package/dist/src/core/mcp/dispatch.js.map +1 -0
  64. package/dist/src/core/mcp/post-move.js +92 -0
  65. package/dist/src/core/mcp/post-move.js.map +1 -0
  66. package/dist/src/core/mcp/protocol.js +293 -0
  67. package/dist/src/core/mcp/protocol.js.map +1 -0
  68. package/dist/src/core/mcp/url.js +162 -0
  69. package/dist/src/core/mcp/url.js.map +1 -0
  70. package/dist/src/core/path.js +63 -0
  71. package/dist/src/core/path.js.map +1 -0
  72. package/dist/src/core/reconcile/image-decide.js +48 -0
  73. package/dist/src/core/reconcile/image-decide.js.map +1 -0
  74. package/dist/src/core/reconcile/ledger.js +142 -0
  75. package/dist/src/core/reconcile/ledger.js.map +1 -0
  76. package/dist/src/core/reconcile/pr-classify.js +62 -0
  77. package/dist/src/core/reconcile/pr-classify.js.map +1 -0
  78. package/dist/{reconciler → src/core/reconcile}/pr-decide.js +25 -12
  79. package/dist/src/core/reconcile/pr-decide.js.map +1 -0
  80. package/dist/src/core/reconcile/pr-loop.js +161 -0
  81. package/dist/src/core/reconcile/pr-loop.js.map +1 -0
  82. package/dist/src/core/reconcile/pr-notes.js +35 -0
  83. package/dist/src/core/reconcile/pr-notes.js.map +1 -0
  84. package/dist/src/core/reconcile/vm-decide.js +70 -0
  85. package/dist/src/core/reconcile/vm-decide.js.map +1 -0
  86. package/dist/src/core/reconcile/vm-reap.js +207 -0
  87. package/dist/src/core/reconcile/vm-reap.js.map +1 -0
  88. package/dist/src/core/reconcile/workspace-decide.js +162 -0
  89. package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
  90. package/dist/src/core/runlog/summary.js +231 -0
  91. package/dist/src/core/runlog/summary.js.map +1 -0
  92. package/dist/src/core/runner/dispatch-config.js +95 -0
  93. package/dist/src/core/runner/dispatch-config.js.map +1 -0
  94. package/dist/src/core/runner/injection.js +61 -0
  95. package/dist/src/core/runner/injection.js.map +1 -0
  96. package/dist/src/core/runner/mise.js +210 -0
  97. package/dist/src/core/runner/mise.js.map +1 -0
  98. package/dist/src/core/runner/prompt.js +720 -0
  99. package/dist/src/core/runner/prompt.js.map +1 -0
  100. package/dist/src/core/runner/turn.js +242 -0
  101. package/dist/src/core/runner/turn.js.map +1 -0
  102. package/dist/src/core/runner/vm-plan.js +390 -0
  103. package/dist/src/core/runner/vm-plan.js.map +1 -0
  104. package/dist/src/core/schedule/admission.js +123 -0
  105. package/dist/src/core/schedule/admission.js.map +1 -0
  106. package/dist/src/core/schedule/circuit-breaker.js +111 -0
  107. package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
  108. package/dist/src/core/schedule/eligibility.js +83 -0
  109. package/dist/src/core/schedule/eligibility.js.map +1 -0
  110. package/dist/src/core/schedule/reconcile-issue.js +82 -0
  111. package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
  112. package/dist/src/core/schedule/retry.js +96 -0
  113. package/dist/src/core/schedule/retry.js.map +1 -0
  114. package/dist/src/core/schedule/sleep-cycle.js +133 -0
  115. package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
  116. package/dist/src/core/schedule/slots.js +124 -0
  117. package/dist/src/core/schedule/slots.js.map +1 -0
  118. package/dist/src/core/schedule/tick.js +553 -0
  119. package/dist/src/core/schedule/tick.js.map +1 -0
  120. package/dist/src/core/schedule/token-fold.js +181 -0
  121. package/dist/src/core/schedule/token-fold.js.map +1 -0
  122. package/dist/src/core/state-resolve.js +86 -0
  123. package/dist/src/core/state-resolve.js.map +1 -0
  124. package/dist/src/core/vm-guards.js +278 -0
  125. package/dist/src/core/vm-guards.js.map +1 -0
  126. package/dist/src/core/workflow/derive.js +107 -0
  127. package/dist/src/core/workflow/derive.js.map +1 -0
  128. package/dist/src/core/workflow/parse.js +687 -0
  129. package/dist/src/core/workflow/parse.js.map +1 -0
  130. package/dist/src/core/workflow/prompt-probe.js +78 -0
  131. package/dist/src/core/workflow/prompt-probe.js.map +1 -0
  132. package/dist/src/core/workflow/validate.js +189 -0
  133. package/dist/src/core/workflow/validate.js.map +1 -0
  134. package/dist/src/core/workspace-key.js +19 -0
  135. package/dist/src/core/workspace-key.js.map +1 -0
  136. package/dist/src/shell/actions-runner.js +356 -0
  137. package/dist/src/shell/actions-runner.js.map +1 -0
  138. package/dist/src/shell/adapter/adapter-registry.js +45 -0
  139. package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
  140. package/dist/src/shell/adapter/clock-random.js +96 -0
  141. package/dist/src/shell/adapter/clock-random.js.map +1 -0
  142. package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
  143. package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
  144. package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
  145. package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
  146. package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
  147. package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
  148. package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
  149. package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
  150. package/dist/src/shell/adapter/launcher-asset.js +57 -0
  151. package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
  152. package/dist/src/shell/adapter/mise-config-asset.js +65 -0
  153. package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
  154. package/dist/src/shell/adapter/workflow-loader.js +304 -0
  155. package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
  156. package/dist/src/shell/cli/doctor.js +268 -0
  157. package/dist/src/shell/cli/doctor.js.map +1 -0
  158. package/dist/src/shell/effect-interpreter-families.js +314 -0
  159. package/dist/src/shell/effect-interpreter-families.js.map +1 -0
  160. package/dist/src/shell/effect-interpreter.js +29 -0
  161. package/dist/src/shell/effect-interpreter.js.map +1 -0
  162. package/dist/src/shell/interp/acp-frame.js +137 -0
  163. package/dist/src/shell/interp/acp-frame.js.map +1 -0
  164. package/dist/src/shell/interp/acp-ws-conn.js +320 -0
  165. package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
  166. package/dist/src/shell/interp/acp-ws-frames.js +159 -0
  167. package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
  168. package/dist/src/shell/interp/acp-ws.js +197 -0
  169. package/dist/src/shell/interp/acp-ws.js.map +1 -0
  170. package/dist/src/shell/interp/acp.js +319 -0
  171. package/dist/src/shell/interp/acp.js.map +1 -0
  172. package/dist/src/shell/interp/credential-defaults.js +128 -0
  173. package/dist/src/shell/interp/credential-defaults.js.map +1 -0
  174. package/dist/src/shell/interp/credential-hooks.js +149 -0
  175. package/dist/src/shell/interp/credential-hooks.js.map +1 -0
  176. package/dist/src/shell/interp/credential-registry.js +226 -0
  177. package/dist/src/shell/interp/credential-registry.js.map +1 -0
  178. package/dist/src/shell/interp/credential.js +103 -0
  179. package/dist/src/shell/interp/credential.js.map +1 -0
  180. package/dist/src/shell/interp/gh.js +163 -0
  181. package/dist/src/shell/interp/gh.js.map +1 -0
  182. package/dist/src/shell/interp/git.js +28 -0
  183. package/dist/src/shell/interp/git.js.map +1 -0
  184. package/dist/src/shell/interp/log.js +213 -0
  185. package/dist/src/shell/interp/log.js.map +1 -0
  186. package/dist/src/shell/interp/process.js +178 -0
  187. package/dist/src/shell/interp/process.js.map +1 -0
  188. package/dist/src/shell/interp/runlog.js +193 -0
  189. package/dist/src/shell/interp/runlog.js.map +1 -0
  190. package/dist/src/shell/interp/timer.js +64 -0
  191. package/dist/src/shell/interp/timer.js.map +1 -0
  192. package/dist/src/shell/interp/tracker-disk.js +99 -0
  193. package/dist/src/shell/interp/tracker-disk.js.map +1 -0
  194. package/dist/src/shell/interp/tracker-parse.js +71 -0
  195. package/dist/src/shell/interp/tracker-parse.js.map +1 -0
  196. package/dist/src/shell/interp/tracker-scan.js +238 -0
  197. package/dist/src/shell/interp/tracker-scan.js.map +1 -0
  198. package/dist/src/shell/interp/tracker-write.js +91 -0
  199. package/dist/src/shell/interp/tracker-write.js.map +1 -0
  200. package/dist/src/shell/interp/tracker.js +41 -0
  201. package/dist/src/shell/interp/tracker.js.map +1 -0
  202. package/dist/src/shell/interp/tty.js +48 -0
  203. package/dist/src/shell/interp/tty.js.map +1 -0
  204. package/dist/src/shell/interp/vm.js +199 -0
  205. package/dist/src/shell/interp/vm.js.map +1 -0
  206. package/dist/src/shell/interp/workspace.js +310 -0
  207. package/dist/src/shell/interp/workspace.js.map +1 -0
  208. package/dist/src/shell/main-acp.js +78 -0
  209. package/dist/src/shell/main-acp.js.map +1 -0
  210. package/dist/src/shell/main-adapters.js +222 -0
  211. package/dist/src/shell/main-adapters.js.map +1 -0
  212. package/dist/src/shell/main-credential.js +122 -0
  213. package/dist/src/shell/main-credential.js.map +1 -0
  214. package/dist/src/shell/main-doctor.js +22 -0
  215. package/dist/src/shell/main-doctor.js.map +1 -0
  216. package/dist/src/shell/main-entry.js +46 -0
  217. package/dist/src/shell/main-entry.js.map +1 -0
  218. package/dist/src/shell/main-http-csrf.js +45 -0
  219. package/dist/src/shell/main-http-csrf.js.map +1 -0
  220. package/dist/src/shell/main-http-handler.js +389 -0
  221. package/dist/src/shell/main-http-handler.js.map +1 -0
  222. package/dist/src/shell/main-http-mcp.js +122 -0
  223. package/dist/src/shell/main-http-mcp.js.map +1 -0
  224. package/dist/src/shell/main-http-views.js +253 -0
  225. package/dist/src/shell/main-http-views.js.map +1 -0
  226. package/dist/src/shell/main-http.js +76 -0
  227. package/dist/src/shell/main-http.js.map +1 -0
  228. package/dist/src/shell/main-loops.js +130 -0
  229. package/dist/src/shell/main-loops.js.map +1 -0
  230. package/dist/src/shell/main-mcp.js +129 -0
  231. package/dist/src/shell/main-mcp.js.map +1 -0
  232. package/dist/src/shell/main-orchestrator.js +120 -0
  233. package/dist/src/shell/main-orchestrator.js.map +1 -0
  234. package/dist/src/shell/main-preflight.js +43 -0
  235. package/dist/src/shell/main-preflight.js.map +1 -0
  236. package/dist/src/shell/main-reconcilers-helpers.js +244 -0
  237. package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
  238. package/dist/src/shell/main-reconcilers-pr.js +148 -0
  239. package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
  240. package/dist/src/shell/main-reconcilers.js +225 -0
  241. package/dist/src/shell/main-reconcilers.js.map +1 -0
  242. package/dist/src/shell/main-runner.js +355 -0
  243. package/dist/src/shell/main-runner.js.map +1 -0
  244. package/dist/src/shell/main-scaffold.js +116 -0
  245. package/dist/src/shell/main-scaffold.js.map +1 -0
  246. package/dist/src/shell/main-shutdown.js +115 -0
  247. package/dist/src/shell/main-shutdown.js.map +1 -0
  248. package/dist/src/shell/main-startup.js +48 -0
  249. package/dist/src/shell/main-startup.js.map +1 -0
  250. package/dist/src/shell/main-substrates.js +43 -0
  251. package/dist/src/shell/main-substrates.js.map +1 -0
  252. package/dist/src/shell/main.js +385 -0
  253. package/dist/src/shell/main.js.map +1 -0
  254. package/dist/src/shell/orchestrator-feedback.js +69 -0
  255. package/dist/src/shell/orchestrator-feedback.js.map +1 -0
  256. package/dist/src/shell/orchestrator-image.js +167 -0
  257. package/dist/src/shell/orchestrator-image.js.map +1 -0
  258. package/dist/src/shell/orchestrator-loop.js +468 -0
  259. package/dist/src/shell/orchestrator-loop.js.map +1 -0
  260. package/dist/src/shell/orchestrator-reconcile.js +36 -0
  261. package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
  262. package/dist/src/shell/reconciler-loop.js +228 -0
  263. package/dist/src/shell/reconciler-loop.js.map +1 -0
  264. package/dist/src/shell/runner-loop-turn.js +301 -0
  265. package/dist/src/shell/runner-loop-turn.js.map +1 -0
  266. package/dist/src/shell/runner-loop.js +338 -0
  267. package/dist/src/shell/runner-loop.js.map +1 -0
  268. package/dist/src/shell/server/http.js +208 -0
  269. package/dist/src/shell/server/http.js.map +1 -0
  270. package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
  271. package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
  272. package/dist/src/shell/server/mcp-runtime.js +99 -0
  273. package/dist/src/shell/server/mcp-runtime.js.map +1 -0
  274. package/dist/src/shell/workspace-key.js +14 -0
  275. package/dist/src/shell/workspace-key.js.map +1 -0
  276. package/dist/src/types/acp.js +8 -0
  277. package/dist/src/types/acp.js.map +1 -0
  278. package/dist/src/types/actions/plan.js +6 -0
  279. package/dist/src/types/actions/plan.js.map +1 -0
  280. package/dist/src/types/actions/predicates.js +6 -0
  281. package/dist/src/types/actions/predicates.js.map +1 -0
  282. package/dist/src/types/actions/run-fold.js +8 -0
  283. package/dist/src/types/actions/run-fold.js.map +1 -0
  284. package/dist/src/types/actions.js +7 -0
  285. package/dist/src/types/actions.js.map +1 -0
  286. package/dist/src/types/adapter/clock-random.js +4 -0
  287. package/dist/src/types/adapter/clock-random.js.map +1 -0
  288. package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
  289. package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
  290. package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
  291. package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
  292. package/dist/src/types/adapter/workflow-loader.js +4 -0
  293. package/dist/src/types/adapter/workflow-loader.js.map +1 -0
  294. package/dist/src/types/cli/args.js +8 -0
  295. package/dist/src/types/cli/args.js.map +1 -0
  296. package/dist/src/types/config.js +8 -0
  297. package/dist/src/types/config.js.map +1 -0
  298. package/dist/src/types/credential-interp.js +6 -0
  299. package/dist/src/types/credential-interp.js.map +1 -0
  300. package/dist/src/types/credentials.js +10 -0
  301. package/dist/src/types/credentials.js.map +1 -0
  302. package/dist/src/types/doctor.js +7 -0
  303. package/dist/src/types/doctor.js.map +1 -0
  304. package/dist/src/types/domain.js +7 -0
  305. package/dist/src/types/domain.js.map +1 -0
  306. package/dist/src/types/effect.js +15 -0
  307. package/dist/src/types/effect.js.map +1 -0
  308. package/dist/src/types/errors.js +39 -0
  309. package/dist/src/types/errors.js.map +1 -0
  310. package/dist/src/types/http/decisions.js +6 -0
  311. package/dist/src/types/http/decisions.js.map +1 -0
  312. package/dist/src/types/http/render.js +10 -0
  313. package/dist/src/types/http/render.js.map +1 -0
  314. package/dist/src/types/http/views.js +6 -0
  315. package/dist/src/types/http/views.js.map +1 -0
  316. package/dist/src/types/http.js +9 -0
  317. package/dist/src/types/http.js.map +1 -0
  318. package/dist/src/types/image/managed-image.js +7 -0
  319. package/dist/src/types/image/managed-image.js.map +1 -0
  320. package/dist/src/types/interp/effect-interpreter.js +8 -0
  321. package/dist/src/types/interp/effect-interpreter.js.map +1 -0
  322. package/dist/src/types/interp/tracker.js +7 -0
  323. package/dist/src/types/interp/tracker.js.map +1 -0
  324. package/dist/src/types/issue/file.js +6 -0
  325. package/dist/src/types/issue/file.js.map +1 -0
  326. package/dist/src/types/issue/parse.js +8 -0
  327. package/dist/src/types/issue/parse.js.map +1 -0
  328. package/dist/src/types/main-acp.js +13 -0
  329. package/dist/src/types/main-acp.js.map +1 -0
  330. package/dist/src/types/main-adapters.js +5 -0
  331. package/dist/src/types/main-adapters.js.map +1 -0
  332. package/dist/src/types/main-credential.js +21 -0
  333. package/dist/src/types/main-credential.js.map +1 -0
  334. package/dist/src/types/main-doctor.js +6 -0
  335. package/dist/src/types/main-doctor.js.map +1 -0
  336. package/dist/src/types/main-http-handler.js +12 -0
  337. package/dist/src/types/main-http-handler.js.map +1 -0
  338. package/dist/src/types/main-http.js +5 -0
  339. package/dist/src/types/main-http.js.map +1 -0
  340. package/dist/src/types/main-loops.js +5 -0
  341. package/dist/src/types/main-loops.js.map +1 -0
  342. package/dist/src/types/main-mcp.js +12 -0
  343. package/dist/src/types/main-mcp.js.map +1 -0
  344. package/dist/src/types/main-orchestrator.js +5 -0
  345. package/dist/src/types/main-orchestrator.js.map +1 -0
  346. package/dist/src/types/main-reconcilers.js +11 -0
  347. package/dist/src/types/main-reconcilers.js.map +1 -0
  348. package/dist/src/types/main-runner.js +13 -0
  349. package/dist/src/types/main-runner.js.map +1 -0
  350. package/dist/src/types/main-startup.js +5 -0
  351. package/dist/src/types/main-startup.js.map +1 -0
  352. package/dist/src/types/main-substrates.js +5 -0
  353. package/dist/src/types/main-substrates.js.map +1 -0
  354. package/dist/src/types/mcp/dispatch.js +4 -0
  355. package/dist/src/types/mcp/dispatch.js.map +1 -0
  356. package/dist/src/types/mcp/post-move.js +7 -0
  357. package/dist/src/types/mcp/post-move.js.map +1 -0
  358. package/dist/src/types/mcp.js +9 -0
  359. package/dist/src/types/mcp.js.map +1 -0
  360. package/dist/src/types/ports.js +12 -0
  361. package/dist/src/types/ports.js.map +1 -0
  362. package/dist/src/types/reconcile/image-decide.js +5 -0
  363. package/dist/src/types/reconcile/image-decide.js.map +1 -0
  364. package/dist/src/types/reconcile/ledger.js +7 -0
  365. package/dist/src/types/reconcile/ledger.js.map +1 -0
  366. package/dist/src/types/reconcile/pr-loop.js +8 -0
  367. package/dist/src/types/reconcile/pr-loop.js.map +1 -0
  368. package/dist/src/types/reconcile/vm-reap.js +8 -0
  369. package/dist/src/types/reconcile/vm-reap.js.map +1 -0
  370. package/dist/src/types/reconcile/workspace-decide.js +7 -0
  371. package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
  372. package/dist/src/types/reconcile.js +9 -0
  373. package/dist/src/types/reconcile.js.map +1 -0
  374. package/dist/src/types/runlog.js +7 -0
  375. package/dist/src/types/runlog.js.map +1 -0
  376. package/dist/src/types/runner/actions-runner.js +12 -0
  377. package/dist/src/types/runner/actions-runner.js.map +1 -0
  378. package/dist/src/types/runner/gondolin-dispatch.js +5 -0
  379. package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
  380. package/dist/src/types/runner/injection.js +6 -0
  381. package/dist/src/types/runner/injection.js.map +1 -0
  382. package/dist/src/types/runner/runner-loop.js +5 -0
  383. package/dist/src/types/runner/runner-loop.js.map +1 -0
  384. package/dist/src/types/runner/turn.js +4 -0
  385. package/dist/src/types/runner/turn.js.map +1 -0
  386. package/dist/src/types/runner/vm-plan.js +4 -0
  387. package/dist/src/types/runner/vm-plan.js.map +1 -0
  388. package/dist/src/types/runtime.js +9 -0
  389. package/dist/src/types/runtime.js.map +1 -0
  390. package/dist/src/types/schedule/admission.js +7 -0
  391. package/dist/src/types/schedule/admission.js.map +1 -0
  392. package/dist/src/types/schedule/circuit-breaker.js +2 -0
  393. package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
  394. package/dist/src/types/schedule/eligibility.js +9 -0
  395. package/dist/src/types/schedule/eligibility.js.map +1 -0
  396. package/dist/src/types/schedule/orchestrator-loop.js +10 -0
  397. package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
  398. package/dist/src/types/schedule/sleep-cycle.js +4 -0
  399. package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
  400. package/dist/src/types/schedule/slots.js +8 -0
  401. package/dist/src/types/schedule/slots.js.map +1 -0
  402. package/dist/src/types/schedule/tick.js +9 -0
  403. package/dist/src/types/schedule/tick.js.map +1 -0
  404. package/dist/src/types/server/mcp-runtime.js +8 -0
  405. package/dist/src/types/server/mcp-runtime.js.map +1 -0
  406. package/dist/src/types/workflow/parse.js +4 -0
  407. package/dist/src/types/workflow/parse.js.map +1 -0
  408. package/dist/tests/core/account-id.test.js +35 -0
  409. package/dist/tests/core/account-id.test.js.map +1 -0
  410. package/dist/tests/core/actions-parse.test.js +176 -0
  411. package/dist/tests/core/actions-parse.test.js.map +1 -0
  412. package/dist/tests/core/adapter-config.test.js +133 -0
  413. package/dist/tests/core/adapter-config.test.js.map +1 -0
  414. package/dist/tests/core/admission.test.js +215 -0
  415. package/dist/tests/core/admission.test.js.map +1 -0
  416. package/dist/tests/core/args.test.js +132 -0
  417. package/dist/tests/core/args.test.js.map +1 -0
  418. package/dist/tests/core/availability.test.js +62 -0
  419. package/dist/tests/core/availability.test.js.map +1 -0
  420. package/dist/tests/core/checks.test.js +395 -0
  421. package/dist/tests/core/checks.test.js.map +1 -0
  422. package/dist/tests/core/circuit-breaker.test.js +172 -0
  423. package/dist/tests/core/circuit-breaker.test.js.map +1 -0
  424. package/dist/tests/core/coerce.test.js +87 -0
  425. package/dist/tests/core/coerce.test.js.map +1 -0
  426. package/dist/tests/core/context.test.js +228 -0
  427. package/dist/tests/core/context.test.js.map +1 -0
  428. package/dist/tests/core/decisions.test.js +310 -0
  429. package/dist/tests/core/decisions.test.js.map +1 -0
  430. package/dist/tests/core/derive.test.js +205 -0
  431. package/dist/tests/core/derive.test.js.map +1 -0
  432. package/dist/tests/core/dispatch-config.test.js +164 -0
  433. package/dist/tests/core/dispatch-config.test.js.map +1 -0
  434. package/dist/tests/core/dispatch.test.js +302 -0
  435. package/dist/tests/core/dispatch.test.js.map +1 -0
  436. package/dist/tests/core/eligibility.test.js +163 -0
  437. package/dist/tests/core/eligibility.test.js.map +1 -0
  438. package/dist/tests/core/extract.test.js +139 -0
  439. package/dist/tests/core/extract.test.js.map +1 -0
  440. package/dist/tests/core/fake-creds.test.js +134 -0
  441. package/dist/tests/core/fake-creds.test.js.map +1 -0
  442. package/dist/tests/core/file.test.js +197 -0
  443. package/dist/tests/core/file.test.js.map +1 -0
  444. package/dist/tests/core/git-result.test.js +113 -0
  445. package/dist/tests/core/git-result.test.js.map +1 -0
  446. package/dist/tests/core/identity.test.js +180 -0
  447. package/dist/tests/core/identity.test.js.map +1 -0
  448. package/dist/tests/core/image-decide.test.js +59 -0
  449. package/dist/tests/core/image-decide.test.js.map +1 -0
  450. package/dist/tests/core/injection.test.js +163 -0
  451. package/dist/tests/core/injection.test.js.map +1 -0
  452. package/dist/tests/core/ledger.test.js +218 -0
  453. package/dist/tests/core/ledger.test.js.map +1 -0
  454. package/dist/tests/core/managed-image.test.js +68 -0
  455. package/dist/tests/core/managed-image.test.js.map +1 -0
  456. package/dist/tests/core/mise.test.js +138 -0
  457. package/dist/tests/core/mise.test.js.map +1 -0
  458. package/dist/tests/core/parse.test.js +174 -0
  459. package/dist/tests/core/parse.test.js.map +1 -0
  460. package/dist/tests/core/path.test.js +50 -0
  461. package/dist/tests/core/path.test.js.map +1 -0
  462. package/dist/tests/core/plan.test.js +218 -0
  463. package/dist/tests/core/plan.test.js.map +1 -0
  464. package/dist/tests/core/post-move.test.js +162 -0
  465. package/dist/tests/core/post-move.test.js.map +1 -0
  466. package/dist/tests/core/pr-classify.test.js +117 -0
  467. package/dist/tests/core/pr-classify.test.js.map +1 -0
  468. package/dist/tests/core/pr-decide.test.js +298 -0
  469. package/dist/tests/core/pr-decide.test.js.map +1 -0
  470. package/dist/tests/core/pr-loop.test.js +301 -0
  471. package/dist/tests/core/pr-loop.test.js.map +1 -0
  472. package/dist/tests/core/pr-notes.test.js +165 -0
  473. package/dist/tests/core/pr-notes.test.js.map +1 -0
  474. package/dist/tests/core/predicates.test.js +154 -0
  475. package/dist/tests/core/predicates.test.js.map +1 -0
  476. package/dist/tests/core/prompt.test.js +189 -0
  477. package/dist/tests/core/prompt.test.js.map +1 -0
  478. package/dist/tests/core/protocol.test.js +195 -0
  479. package/dist/tests/core/protocol.test.js.map +1 -0
  480. package/dist/tests/core/reconcile-issue.test.js +116 -0
  481. package/dist/tests/core/reconcile-issue.test.js.map +1 -0
  482. package/dist/tests/core/render.test.js +549 -0
  483. package/dist/tests/core/render.test.js.map +1 -0
  484. package/dist/tests/core/retry.test.js +186 -0
  485. package/dist/tests/core/retry.test.js.map +1 -0
  486. package/dist/tests/core/routes.test.js +247 -0
  487. package/dist/tests/core/routes.test.js.map +1 -0
  488. package/dist/tests/core/run-fold.test.js +299 -0
  489. package/dist/tests/core/run-fold.test.js.map +1 -0
  490. package/dist/tests/core/shape.test.js +185 -0
  491. package/dist/tests/core/shape.test.js.map +1 -0
  492. package/dist/tests/core/sleep-cycle.test.js +150 -0
  493. package/dist/tests/core/sleep-cycle.test.js.map +1 -0
  494. package/dist/tests/core/slots.test.js +201 -0
  495. package/dist/tests/core/slots.test.js.map +1 -0
  496. package/dist/tests/core/state-resolve.test.js +80 -0
  497. package/dist/tests/core/state-resolve.test.js.map +1 -0
  498. package/dist/tests/core/summary.test.js +200 -0
  499. package/dist/tests/core/summary.test.js.map +1 -0
  500. package/dist/tests/core/template.test.js +116 -0
  501. package/dist/tests/core/template.test.js.map +1 -0
  502. package/dist/tests/core/tick.test.js +558 -0
  503. package/dist/tests/core/tick.test.js.map +1 -0
  504. package/dist/tests/core/token-fold.test.js +176 -0
  505. package/dist/tests/core/token-fold.test.js.map +1 -0
  506. package/dist/tests/core/turn.test.js +388 -0
  507. package/dist/tests/core/turn.test.js.map +1 -0
  508. package/dist/tests/core/url.test.js +118 -0
  509. package/dist/tests/core/url.test.js.map +1 -0
  510. package/dist/tests/core/validate.test.js +247 -0
  511. package/dist/tests/core/validate.test.js.map +1 -0
  512. package/dist/tests/core/views.test.js +252 -0
  513. package/dist/tests/core/views.test.js.map +1 -0
  514. package/dist/tests/core/vm-decide.test.js +110 -0
  515. package/dist/tests/core/vm-decide.test.js.map +1 -0
  516. package/dist/tests/core/vm-guards.test.js +153 -0
  517. package/dist/tests/core/vm-guards.test.js.map +1 -0
  518. package/dist/tests/core/vm-plan.test.js +332 -0
  519. package/dist/tests/core/vm-plan.test.js.map +1 -0
  520. package/dist/tests/core/vm-reap.test.js +196 -0
  521. package/dist/tests/core/vm-reap.test.js.map +1 -0
  522. package/dist/tests/core/workflow-parse.test.js +493 -0
  523. package/dist/tests/core/workflow-parse.test.js.map +1 -0
  524. package/dist/tests/core/workspace-decide.test.js +236 -0
  525. package/dist/tests/core/workspace-decide.test.js.map +1 -0
  526. package/dist/tests/helpers/fixtures.js +167 -0
  527. package/dist/tests/helpers/fixtures.js.map +1 -0
  528. package/dist/tests/shell/acp-substrate.test.js +101 -0
  529. package/dist/tests/shell/acp-substrate.test.js.map +1 -0
  530. package/dist/tests/shell/actions-runner-push.test.js +203 -0
  531. package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
  532. package/dist/tests/shell/credential-hooks.test.js +36 -0
  533. package/dist/tests/shell/credential-hooks.test.js.map +1 -0
  534. package/dist/tests/shell/credential-registry.test.js +165 -0
  535. package/dist/tests/shell/credential-registry.test.js.map +1 -0
  536. package/dist/tests/shell/credential-substrate.test.js +179 -0
  537. package/dist/tests/shell/credential-substrate.test.js.map +1 -0
  538. package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
  539. package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
  540. package/dist/tests/shell/doctor.test.js +101 -0
  541. package/dist/tests/shell/doctor.test.js.map +1 -0
  542. package/dist/tests/shell/effect-vm-create.test.js +52 -0
  543. package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
  544. package/dist/tests/shell/gh-port.test.js +63 -0
  545. package/dist/tests/shell/gh-port.test.js.map +1 -0
  546. package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
  547. package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
  548. package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
  549. package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
  550. package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
  551. package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
  552. package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
  553. package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
  554. package/dist/tests/shell/http-handler.test.js +608 -0
  555. package/dist/tests/shell/http-handler.test.js.map +1 -0
  556. package/dist/tests/shell/http-server.test.js +53 -0
  557. package/dist/tests/shell/http-server.test.js.map +1 -0
  558. package/dist/tests/shell/mcp-runtime.test.js +366 -0
  559. package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
  560. package/dist/tests/shell/mise-config-asset.test.js +87 -0
  561. package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
  562. package/dist/tests/shell/orchestrator-loop.test.js +583 -0
  563. package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
  564. package/dist/tests/shell/reconciler-passes.test.js +314 -0
  565. package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
  566. package/dist/tests/shell/runner-loop-turn.test.js +97 -0
  567. package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
  568. package/dist/tests/shell/runner-slice.test.js +536 -0
  569. package/dist/tests/shell/runner-slice.test.js.map +1 -0
  570. package/dist/tests/shell/scaffold.test.js +65 -0
  571. package/dist/tests/shell/scaffold.test.js.map +1 -0
  572. package/dist/tests/shell/tick-config.test.js +83 -0
  573. package/dist/tests/shell/tick-config.test.js.map +1 -0
  574. package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
  575. package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
  576. package/dist/tests/shell/tracker-write-issue.test.js +154 -0
  577. package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
  578. package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
  579. package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
  580. package/dist/tests/shell/workspace-live-config.test.js +140 -0
  581. package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
  582. package/package.json +21 -9
  583. package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
  584. package/prompts/Reflect.md +91 -0
  585. package/prompts/Review.md +97 -0
  586. package/prompts/Todo.md +96 -0
  587. package/prompts/_footer.md +41 -0
  588. package/prompts/_preamble.md +42 -0
  589. package/prompts-minimal/Todo.md +26 -0
  590. package/scripts/postinstall.mjs +63 -0
  591. package/scripts/vm-agent.mjs +312 -90
  592. package/WORKFLOW.md +0 -744
  593. package/dist/acp-bridge.js +0 -324
  594. package/dist/acp-bridge.js.map +0 -1
  595. package/dist/actions/cache.js +0 -191
  596. package/dist/actions/cache.js.map +0 -1
  597. package/dist/actions/effects.js +0 -41
  598. package/dist/actions/effects.js.map +0 -1
  599. package/dist/actions/executor.js +0 -570
  600. package/dist/actions/executor.js.map +0 -1
  601. package/dist/actions/index.js +0 -13
  602. package/dist/actions/index.js.map +0 -1
  603. package/dist/actions/parsing.js.map +0 -1
  604. package/dist/actions/predicate-env.js +0 -27
  605. package/dist/actions/predicate-env.js.map +0 -1
  606. package/dist/actions/predicates.js +0 -49
  607. package/dist/actions/predicates.js.map +0 -1
  608. package/dist/actions/templating.js +0 -66
  609. package/dist/actions/templating.js.map +0 -1
  610. package/dist/actions/types.js +0 -15
  611. package/dist/actions/types.js.map +0 -1
  612. package/dist/agent/acp.js +0 -473
  613. package/dist/agent/acp.js.map +0 -1
  614. package/dist/agent/adapter-names.js +0 -159
  615. package/dist/agent/adapter-names.js.map +0 -1
  616. package/dist/agent/adapters.js +0 -511
  617. package/dist/agent/adapters.js.map +0 -1
  618. package/dist/agent/credential-extractors.js +0 -342
  619. package/dist/agent/credential-extractors.js.map +0 -1
  620. package/dist/agent/credential-secrets.js +0 -628
  621. package/dist/agent/credential-secrets.js.map +0 -1
  622. package/dist/agent/credential-ticker.js +0 -57
  623. package/dist/agent/credential-ticker.js.map +0 -1
  624. package/dist/agent/gondolin-creds-staging.js +0 -356
  625. package/dist/agent/gondolin-creds-staging.js.map +0 -1
  626. package/dist/agent/gondolin-dispatch.js +0 -375
  627. package/dist/agent/gondolin-dispatch.js.map +0 -1
  628. package/dist/agent/gondolin.js +0 -124
  629. package/dist/agent/gondolin.js.map +0 -1
  630. package/dist/agent/runner-decisions.js +0 -134
  631. package/dist/agent/runner-decisions.js.map +0 -1
  632. package/dist/agent/runner.js +0 -1456
  633. package/dist/agent/runner.js.map +0 -1
  634. package/dist/agent/tool-call-summary.js +0 -102
  635. package/dist/agent/tool-call-summary.js.map +0 -1
  636. package/dist/agent/vm-acp-mapping.js +0 -73
  637. package/dist/agent/vm-acp-mapping.js.map +0 -1
  638. package/dist/agent/vm-guards.js +0 -262
  639. package/dist/agent/vm-guards.js.map +0 -1
  640. package/dist/agent/vm-port.js +0 -22
  641. package/dist/agent/vm-port.js.map +0 -1
  642. package/dist/agent/vm-process-registry.js +0 -79
  643. package/dist/agent/vm-process-registry.js.map +0 -1
  644. package/dist/bin/cli-args.js +0 -105
  645. package/dist/bin/cli-args.js.map +0 -1
  646. package/dist/bin/symphony.js +0 -794
  647. package/dist/bin/symphony.js.map +0 -1
  648. package/dist/errors.js +0 -15
  649. package/dist/errors.js.map +0 -1
  650. package/dist/http-disk.js +0 -135
  651. package/dist/http-disk.js.map +0 -1
  652. package/dist/http-handlers.js.map +0 -1
  653. package/dist/http.js.map +0 -1
  654. package/dist/issues.js +0 -178
  655. package/dist/issues.js.map +0 -1
  656. package/dist/logging.js +0 -203
  657. package/dist/logging.js.map +0 -1
  658. package/dist/mcp.js +0 -706
  659. package/dist/mcp.js.map +0 -1
  660. package/dist/memory.js +0 -85
  661. package/dist/memory.js.map +0 -1
  662. package/dist/orchestrator-decisions.js +0 -331
  663. package/dist/orchestrator-decisions.js.map +0 -1
  664. package/dist/orchestrator.js +0 -1569
  665. package/dist/orchestrator.js.map +0 -1
  666. package/dist/prompt.js +0 -65
  667. package/dist/prompt.js.map +0 -1
  668. package/dist/reconciler/cache.js +0 -65
  669. package/dist/reconciler/cache.js.map +0 -1
  670. package/dist/reconciler/index.js +0 -448
  671. package/dist/reconciler/index.js.map +0 -1
  672. package/dist/reconciler/ledger.js +0 -131
  673. package/dist/reconciler/ledger.js.map +0 -1
  674. package/dist/reconciler/pr-adapters.js +0 -174
  675. package/dist/reconciler/pr-adapters.js.map +0 -1
  676. package/dist/reconciler/pr-decide.js.map +0 -1
  677. package/dist/reconciler/pr.js +0 -422
  678. package/dist/reconciler/pr.js.map +0 -1
  679. package/dist/reconciler/types.js +0 -12
  680. package/dist/reconciler/types.js.map +0 -1
  681. package/dist/reconciler/vm.js +0 -243
  682. package/dist/reconciler/vm.js.map +0 -1
  683. package/dist/reconciler/workspace-defaults.js +0 -83
  684. package/dist/reconciler/workspace-defaults.js.map +0 -1
  685. package/dist/reconciler/workspace.js +0 -272
  686. package/dist/reconciler/workspace.js.map +0 -1
  687. package/dist/runlog.js +0 -403
  688. package/dist/runlog.js.map +0 -1
  689. package/dist/scaffold.js +0 -165
  690. package/dist/scaffold.js.map +0 -1
  691. package/dist/trackers/local.js +0 -445
  692. package/dist/trackers/local.js.map +0 -1
  693. package/dist/trackers/types.js +0 -10
  694. package/dist/trackers/types.js.map +0 -1
  695. package/dist/types.js +0 -3
  696. package/dist/types.js.map +0 -1
  697. package/dist/util/clock.js +0 -12
  698. package/dist/util/clock.js.map +0 -1
  699. package/dist/util/crypto.js +0 -25
  700. package/dist/util/crypto.js.map +0 -1
  701. package/dist/util/frontmatter.js +0 -70
  702. package/dist/util/frontmatter.js.map +0 -1
  703. package/dist/util/fs-issues.js +0 -22
  704. package/dist/util/fs-issues.js.map +0 -1
  705. package/dist/util/process.js +0 -152
  706. package/dist/util/process.js.map +0 -1
  707. package/dist/util/workspace-key.js +0 -10
  708. package/dist/util/workspace-key.js.map +0 -1
  709. package/dist/workflow-loader.js +0 -147
  710. package/dist/workflow-loader.js.map +0 -1
  711. package/dist/workflow.js +0 -822
  712. package/dist/workflow.js.map +0 -1
  713. package/dist/workspace-types.js +0 -8
  714. package/dist/workspace-types.js.map +0 -1
  715. package/dist/workspace.js +0 -443
  716. package/dist/workspace.js.map +0 -1
@@ -1,794 +0,0 @@
1
- #!/usr/bin/env node
2
- // CLI entry. Usage:
3
- // symphony [path-to-WORKFLOW.md] [--port <port>]
4
- // symphony reconcile [path-to-WORKFLOW.md] [--force] [--port <port>]
5
- // symphony rerun --check=<name> [path-to-WORKFLOW.md]
6
- //
7
- // Default workflow path is ./WORKFLOW.md.
8
- //
9
- // The `reconcile` subcommand boots symphony exactly the same way as the bare
10
- // form; `--force` additionally requests an immediate reconcile pass (VM /
11
- // workspace / PR janitors) instead of waiting on the backstop tick.
12
- // `--reconcile-force` is kept as a top-level alias for ergonomics.
13
- //
14
- // The `rerun` subcommand (issue 36) invalidates one `run_in_vm` action's
15
- // content-hash cache entries so the next dispatch into the state hosting it
16
- // re-executes. It does not start a long-running process — it scans the
17
- // workflow's state actions for a matching name and `rm`s the per-name cache
18
- // namespace directory under `<cacheRoot>/actions/run_in_vm/<name>/`. The
19
- // per-execution hash is workspace-dependent (the agent's edits change the
20
- // tree); namespacing the cache by action name on disk means the CLI doesn't
21
- // need to know any per-issue workspace state to invalidate the right
22
- // entries.
23
- import path from 'node:path';
24
- import process from 'node:process';
25
- import readline from 'node:readline';
26
- import { existsSync } from 'node:fs';
27
- import { parseCli } from './cli-args.js';
28
- import { loadWorkflow, watchWorkflow } from '../workflow-loader.js';
29
- import { scaffoldWorkflow, ScaffoldError } from '../scaffold.js';
30
- import { invalidateRunInVmByName } from '../actions/index.js';
31
- import { LocalMarkdownTracker } from '../trackers/local.js';
32
- import { WorkspaceManager, resolveGithubRepo } from '../workspace.js';
33
- import { GondolinVmClient } from '../agent/gondolin.js';
34
- import { CredentialSecretRegistry, buildAdapterCredentialSpecs, buildAdapterHooksConfig, } from '../agent/credential-secrets.js';
35
- import { defaultHostIdentityReaders } from '../agent/gondolin-creds-staging.js';
36
- import { KNOWN_ADAPTER_IDS } from '../agent/adapter-names.js';
37
- import { AgentRunner } from '../agent/runner.js';
38
- import { Orchestrator } from '../orchestrator.js';
39
- import { startHttpServer } from '../http.js';
40
- import { McpRegistry } from '../mcp.js';
41
- import { AcpBridge } from '../acp-bridge.js';
42
- import { CredentialTicker } from '../agent/credential-ticker.js';
43
- import { GhCliPrApi, Reconciler } from '../reconciler/index.js';
44
- import { closeLogFile, log, setLogFile, setLogVerbose } from '../logging.js';
45
- import { derivePrRouting } from '../workflow.js';
46
- /**
47
- * Walk every declared state's `actions:` for a run_in_vm whose `name` matches
48
- * `target`. Returns the first match; duplicates would let a single rerun
49
- * invalidate multiple entries, which is rarely intended (operator wants to
50
- * re-run *one* named check).
51
- */
52
- function findRunInVmByName(states, target) {
53
- for (const [stateName, sc] of Object.entries(states)) {
54
- if (!sc.actions)
55
- continue;
56
- for (const a of sc.actions) {
57
- if (a.kind === 'run_in_vm' && a.name === target) {
58
- return { state: stateName, action: a };
59
- }
60
- }
61
- }
62
- return null;
63
- }
64
- async function runRerunCheck(workflowPath, name) {
65
- let cfg;
66
- try {
67
- ({ config: cfg } = await loadWorkflow(workflowPath));
68
- }
69
- catch (err) {
70
- process.stderr.write(`error: failed to load workflow: ${err.message}\n`);
71
- return 1;
72
- }
73
- const match = findRunInVmByName(cfg.states, name);
74
- if (!match) {
75
- process.stderr.write(`error: no run_in_vm action named "${name}" declared in WORKFLOW.md\n`);
76
- return 2;
77
- }
78
- // Drop the per-name cache namespace directory. This invalidates every
79
- // hash entry under that name regardless of which per-issue workspace the
80
- // execution computed its hash against — the orchestrator's next dispatch
81
- // re-executes the check because the namespace is empty.
82
- await invalidateRunInVmByName(match.action);
83
- process.stdout.write(`invalidated run_in_vm "${name}" (state=${match.state})\n`);
84
- return 0;
85
- }
86
- /**
87
- * Read a single line from stdin with the given prompt. Resolves to the
88
- * trimmed input string (without the trailing newline). The readline interface
89
- * is closed before resolving so the process can exit cleanly afterwards.
90
- */
91
- function promptLine(message) {
92
- const rl = readline.createInterface({
93
- input: process.stdin,
94
- output: process.stdout,
95
- });
96
- return new Promise((resolve) => {
97
- rl.question(message, (answer) => {
98
- rl.close();
99
- resolve(answer);
100
- });
101
- });
102
- }
103
- /**
104
- * When the workflow file is missing and the operator is at an interactive
105
- * terminal, ask whether to scaffold a starter file. Returns true if the
106
- * scaffold was written (caller can continue boot), false otherwise (caller
107
- * should fall through to the usual "file not found" error).
108
- *
109
- * Non-interactive invocations (cron jobs, CI, container ENTRYPOINTs) skip the
110
- * prompt entirely and return false — silently scaffolding files into someone
111
- * else's working tree without a confirmed yes is the wrong default for a tool
112
- * that's usually run by an operator who knows where their workflow lives.
113
- */
114
- async function maybeScaffoldMissingWorkflow(workflowPath) {
115
- if (!process.stdin.isTTY || !process.stdout.isTTY)
116
- return false;
117
- const answer = await promptLine(`WORKFLOW.md not found at ${workflowPath}.\nScaffold a starter workflow file here? [Y/n] `);
118
- const normalized = answer.trim().toLowerCase();
119
- // Default-accept: bare enter, "y", "yes". Anything else is "no".
120
- const accept = normalized === '' || normalized === 'y' || normalized === 'yes';
121
- if (!accept)
122
- return false;
123
- try {
124
- const result = await scaffoldWorkflow({ workflowPath });
125
- process.stdout.write(`wrote ${result.workflowPath}\n`);
126
- process.stdout.write(`Edit it to point gondolin.image at your built agent image (npm run build:image), ` +
127
- `then run \`symphony ${path.relative(process.cwd(), result.workflowPath) || workflowPath}\` again.\n`);
128
- return true;
129
- }
130
- catch (err) {
131
- const msg = err instanceof ScaffoldError ? err.message : err.message;
132
- process.stderr.write(`error: scaffold failed: ${msg}\n`);
133
- return false;
134
- }
135
- }
136
- /**
137
- * Centralized startup-failure cleanup. Writes the message, closes the optional
138
- * HTTP listener and workflow watcher, flushes the persistent log sink, and
139
- * exits non-zero. Returns `never` so call sites can treat the path as a hard
140
- * terminator and TypeScript narrows away post-call code.
141
- *
142
- * `flushLogs` matters because `process.exit` does NOT drain pending WriteStream
143
- * writes — without `closeLogFile()` we'd lose the final lines symphony.log was
144
- * about to receive (the startup-failure stderr line itself is unaffected, but
145
- * any buffered `log.*` output would be dropped).
146
- */
147
- async function bailStartup(message, opts) {
148
- process.stderr.write(message);
149
- if (opts.http)
150
- await opts.http.close().catch(() => undefined);
151
- await opts.src.stop().catch(() => undefined);
152
- await closeLogFile().catch(() => undefined);
153
- process.exit(1);
154
- }
155
- /**
156
- * Handle the two early-exit subcommands that don't enter the orchestrator
157
- * graph: a missing workflow file (offer to scaffold, or fail), and the
158
- * `rerun --check=<name>` subcommand (invalidate one action's cache and exit).
159
- * Returns to the caller only on the happy path of `serve`/`reconcile` against
160
- * an existing workflow.
161
- */
162
- async function handlePreflight(cli, workflowPath) {
163
- if (!existsSync(workflowPath)) {
164
- // `rerun` operates on an existing workflow's action namespace; there is
165
- // nothing to scaffold against. Same for `reconcile`, which only makes sense
166
- // when a workflow already exists. Prompt only on the bare `serve` path.
167
- if (cli.subcommand === 'serve') {
168
- const scaffolded = await maybeScaffoldMissingWorkflow(workflowPath);
169
- // Stop here on purpose: the operator hasn't finished filling in
170
- // the gondolin.* / source-of-truth fields yet, and dispatching immediately
171
- // would just fail at the first attempt with a confusing error. The
172
- // scaffold message already tells them how to relaunch.
173
- if (scaffolded)
174
- process.exit(0);
175
- }
176
- process.stderr.write(`error: workflow file not found: ${workflowPath}\n`);
177
- process.exit(2);
178
- }
179
- if (cli.subcommand === 'rerun') {
180
- if (!cli.rerunCheck) {
181
- process.stderr.write(`error: rerun requires --check=<name>\n`);
182
- process.exit(2);
183
- }
184
- const code = await runRerunCheck(workflowPath, cli.rerunCheck);
185
- process.exit(code);
186
- }
187
- }
188
- /**
189
- * Start the workflow watcher, validate that this build can serve the parsed
190
- * tracker (currently `kind=local` only), and resolve the persistent log file
191
- * path. Mirrors stderr to disk so an agent reviewing a run after the fact
192
- * (typically inside a VM with the workspace + .symphony/logs/ mounted in) can
193
- * read orchestrator-side events — workflow reloads, dispatch decisions, action
194
- * results, reconciler ticks — alongside the per-issue JSONL run logs in the
195
- * same directory.
196
- *
197
- * Path resolution: `SYMPHONY_LOG_FILE` env override wins (`""` disables the
198
- * sink); otherwise `<logs.root>/symphony.log`. The directory is created on
199
- * demand. File-sink failure is swallowed: symphony continues on stderr only.
200
- */
201
- async function loadAndValidateConfig(workflowPath) {
202
- let src;
203
- try {
204
- src = await watchWorkflow(workflowPath);
205
- }
206
- catch (err) {
207
- process.stderr.write(`error: failed to load workflow: ${err.message}\n`);
208
- process.exit(1);
209
- }
210
- const { definition, config } = src.current();
211
- if (config.tracker.kind !== 'local') {
212
- process.stderr.write(`error: this build supports tracker.kind=local only (got: ${config.tracker.kind || '<unset>'})\n`);
213
- process.exit(2);
214
- }
215
- const envLogFile = process.env.SYMPHONY_LOG_FILE;
216
- const logFile = envLogFile === undefined
217
- ? path.join(config.logs.root, 'symphony.log')
218
- : envLogFile === ''
219
- ? null
220
- : envLogFile;
221
- setLogFile(logFile);
222
- return { src, definition, config, envLogFile, logFile };
223
- }
224
- /**
225
- * Build the host credential pipeline for the Gondolin secret-substitution model
226
- * (replaces the credential proxy). There is no HTTP proxy server and no base-URL
227
- * injection: per-adapter specs carry the extractor/mint/flock-refresh logic, a
228
- * single shared `CredentialSecretRegistry` owns every live per-VM secretManager
229
- * and seeds it before first egress, and the per-adapter hooks configs (allowlist
230
- * + token-shaped placeholder + request/response hooks) thread into each dispatch's
231
- * `createHttpHooks`. The ticker fans `refreshAdapter` over every live adapter.
232
- */
233
- async function buildCredentialPipeline(config) {
234
- // Resolve the host's NON-SECRET codex `chatgpt_account_id` once and bind it into
235
- // the codex placeholder JWT's auth claim — without it codex-acp attempts a
236
- // mid-turn token refresh (egress-blocked → 403 → refusal; the go-live finding).
237
- // Best-effort: a missing/malformed auth.json yields null (claim omitted).
238
- const codexAccountId = await defaultHostIdentityReaders().readCodexAccountId();
239
- const specs = buildAdapterCredentialSpecs({ codexAccountId });
240
- const adapterHooks = buildAllAdapterHooks(specs, config.egress.allowed_hosts);
241
- const credentialRegistry = new CredentialSecretRegistry({
242
- readToken: (adapterId) => specs[adapterId].readToken(),
243
- refresh: (adapterId) => specs[adapterId].refresh(),
244
- });
245
- const credentialTicker = new CredentialTicker({
246
- intervalMs: config.credentials.ticker_interval_ms,
247
- // Fan a host-side refresh over every adapter; each adapter's flock +
248
- // single-flight collapses concurrent ticks into one host refresh, and the
249
- // registry seeds every live per-VM manager with the fresh value.
250
- refreshAll: () => refreshAllAdapters(credentialRegistry),
251
- });
252
- return { credentialRegistry, adapterHooks, credentialTicker };
253
- }
254
- /**
255
- * Build the per-adapter `createHttpHooks` config map from the credential specs.
256
- * `egressAllowlist` is the general workspace dev-tooling firewall (npm/git/CDNs)
257
- * unioned into every adapter's `allowedHosts` (never into its substitution scope).
258
- */
259
- function buildAllAdapterHooks(specs, egressAllowlist) {
260
- const out = {};
261
- for (const id of KNOWN_ADAPTER_IDS) {
262
- out[id] = buildAdapterHooksConfig(specs[id], egressAllowlist);
263
- }
264
- return out;
265
- }
266
- /** Drive a host-side refresh + fan-out for every adapter (the ticker cadence). */
267
- async function refreshAllAdapters(registry) {
268
- for (const id of KNOWN_ADAPTER_IDS) {
269
- await registry.refreshAdapter(id);
270
- }
271
- }
272
- /**
273
- * Resolve the static Gondolin VM shape from config. Fail fast if no image is set
274
- * so a misconfigured workflow surfaces at boot, not mid-dispatch after the VM
275
- * bring-up cost is sunk.
276
- */
277
- function resolveGondolinVmConfig(config) {
278
- const imagePath = config.gondolin.image;
279
- if (!imagePath || imagePath.length === 0) {
280
- throw new Error('gondolin: no VM image configured. Set gondolin.image (a build id / `name:tag` ref / ' +
281
- 'asset dir exported by `npm run build:image` — see images/agents) in WORKFLOW.md.');
282
- }
283
- return { imagePath, cpus: config.gondolin.cpus, memMib: config.gondolin.mem_mib };
284
- }
285
- /**
286
- * Build the in-process graph: tracker, workspaces, vmClient, mcp, acpBridge,
287
- * reconciler, runner, orchestrator. Wires the post-construction provider
288
- * callbacks (`reconciler.setIntendedVmProvider` / `setWorkspaceProviders` /
289
- * `setPrAutopilotProviders`) and the reload callback that propagates config
290
- * updates through every component.
291
- *
292
- * The Reconciler is constructed before the Orchestrator (the runner needs the
293
- * reconciler at its own construction time), so the vm reaper's
294
- * IntendedVmProvider and workspace providers are plugged in after the
295
- * orchestrator exists. The vm resource is only built when both `vmClient`
296
- * (passed at Reconciler construction) and an intended provider are wired.
297
- */
298
- async function buildOrchestratorGraph(opts) {
299
- const { config, definition, src, envLogFile } = opts;
300
- const tracker = new LocalMarkdownTracker(config.tracker);
301
- // Materialize every declared state directory under tracker.root up front so
302
- // the dashboard sees the full set of columns (including `holding` states like
303
- // Triage) before any issue lands in them.
304
- try {
305
- await tracker.start();
306
- }
307
- catch (err) {
308
- await bailStartup(`error: tracker init failed: ${err.message}\n`, { src });
309
- }
310
- const workspaces = new WorkspaceManager(config);
311
- // Gondolin VM substrate (the in-process VM backend for the dispatch
312
- // path). The runner builds a per-dispatch GondolinDispatcher over this client,
313
- // and the Reconciler's VM reaper observes its session registry / runs its GC.
314
- const vmClient = new GondolinVmClient();
315
- const gondolinVmConfig = resolveGondolinVmConfig(config);
316
- // Always instantiate the registry so a workflow reload that flips mcp.enabled from
317
- // false to true takes effect without a process restart. The runner and HTTP routes
318
- // gate behavior on cfg.mcp.enabled at runtime; an inactive registry holds no entries
319
- // and answers all routes with "not active."
320
- const mcp = new McpRegistry(tracker, {
321
- states: config.states,
322
- // The PR engine owns the merge state's workspace; pass the engine-enabled
323
- // flag + the derived merge-state name so transitions into it defer terminal
324
- // cleanup (issue 144 retired the old derived compatibility view).
325
- prMerge: { enabled: config.pr.enabled, mergeState: derivePrRouting(config.states).mergeState },
326
- now: () => Date.now(),
327
- });
328
- // ACP transport. The bridge listens on a loopback TCP port for the in-VM
329
- // agent's dial-back (raw mapped TCP via Gondolin `tcp.hosts`). `loopbackOnly`
330
- // hard-refuses a wider bind so the bearer-gated control channel can never be
331
- // exposed to the host LAN. Started below alongside the HTTP server so a bind
332
- // failure surfaces before we accept any dispatches.
333
- const acpBridge = new AcpBridge({ loopbackOnly: true });
334
- const { credentialRegistry, adapterHooks, credentialTicker } = await buildCredentialPipeline(config);
335
- // Reconciler (issues 32, 33, 34). Owns the VM reaper (now Gondolin-backed:
336
- // observes `vmClient.listSessions()` + runs `vmClient.gc()`, reaping
337
- // `symphony-`-labelled sessions not in the orchestrator's intended set) + the
338
- // per-issue workspace convergence. Bake is bypassed on the Gondolin dispatch
339
- // path (the runner uses the prebuilt image directly); the bake resource stays
340
- // for now and is deleted in a later PR.
341
- const reconciler = new Reconciler(config, { vmClient });
342
- // Build the runner with stubs first; we attach the orchestrator's provider callbacks after
343
- // construction since they reference the orchestrator instance.
344
- let orch;
345
- const runner = new AgentRunner(config, definition, workspaces, tracker, vmClient, {
346
- onRuntimeEvent: (id, ev) => orch.reportRuntimeEvent(id, ev),
347
- onTokenUsage: (id, u) => orch.reportTokenUsage(id, u),
348
- onRateLimits: (id, s) => orch.reportRateLimits(id, s),
349
- onTurn: (id, turn) => orch.reportTurnStarted(id, turn),
350
- onSessionStarted: (info) => orch.reportSessionStarted(info.issueId, {
351
- sessionId: info.sessionId,
352
- threadId: info.threadId,
353
- pid: info.pid,
354
- }),
355
- }, mcp, acpBridge,
356
- // propose_followup sink (issue 36): orchestrator owns the tracker write
357
- // path, mirroring how the MCP `propose_issue` tool routes through the
358
- // tracker. The runner forwards the parent identifier so provenance is
359
- // recorded the same way.
360
- { proposeFollowup: (input) => orch.proposeFollowup(input) },
361
- // Action snapshot sink (issue 36 AC5): per-attempt ledger surfaces on
362
- // /api/v1/snapshot under reconciler.resources so the dashboard sees
363
- // "Done.actions: …" alongside the bake/vm/workspace resources.
364
- { recordActionResult: (id, snap) => orch.recordActionResult(id, snap) },
365
- // Gondolin credential layer (replaced the credential proxy): the shared
366
- // registry of per-VM secret managers, the per-adapter hooks configs, and the
367
- // static VM shape (image + cpus/mem).
368
- credentialRegistry, adapterHooks, gondolinVmConfig);
369
- orch = new Orchestrator(config, definition, src, tracker, workspaces, runner, undefined, reconciler);
370
- wirePostConstructionProviders({ reconciler, orch, config });
371
- // The tracker view is resolved through a getter so reloaded config (e.g. a moved
372
- // tracker.root, changed active/terminal states) is reflected by both the propagation
373
- // callback and the HTTP UI without rebinding the server.
374
- let liveCfg = config;
375
- orch.setOnConfigReloaded(buildReloadHandler({
376
- tracker,
377
- workspaces,
378
- runner,
379
- mcp,
380
- envLogFile,
381
- onLiveCfg: (cfg) => {
382
- liveCfg = cfg;
383
- },
384
- }));
385
- return {
386
- tracker,
387
- workspaces,
388
- mcp,
389
- acpBridge,
390
- credentialTicker,
391
- reconciler,
392
- runner,
393
- orch,
394
- vmClient,
395
- getLiveCfg: () => liveCfg,
396
- };
397
- }
398
- /**
399
- * Plug the orchestrator into the reconciler as the IntendedVmProvider, the
400
- * workspace intended/baseRef provider (with remove + create delegating back
401
- * through the orchestrator so the canonical workspace setup runs on
402
- * reconciler-driven passes), and the PR autopilot's set of providers (intended
403
- * set, PR/git adapters, transition router, cleanup callback, and workspace
404
- * re-materializer). Kept as a separate function so `buildOrchestratorGraph`
405
- * stays within the imperative-shell statement budget.
406
- */
407
- function wirePostConstructionProviders(opts) {
408
- const { reconciler, orch, config } = opts;
409
- reconciler.setIntendedVmProvider(orch);
410
- // Removal is delegated to WorkspaceManager (a best-effort `rm -rf`) so janitor
411
- // removals reuse the same path the runner does — the closure captures
412
- // `workspaces` (whose config is kept live via updateConfig on reload), so a
413
- // rotated `workspace.root` takes effect without rebuilding the reconciler.
414
- reconciler.setWorkspaceProviders(orch, {
415
- baseRef: orch,
416
- remove: (identifier) => orch.removeWorkspace(identifier),
417
- // Create callback for the reconciler's eager-workspace pass (issue 34).
418
- // Delegates to `WorkspaceManager.ensureFor` via the orchestrator so the
419
- // canonical clone+branch+remote setup fires on reconciler-driven creates
420
- // the same way it does on dispatch. The intended-set provider supplies the
421
- // issue's current state alongside the identifier (used for the merge-state
422
- // guard); the per-identifier ensureFor lock collapses any race with
423
- // concurrent dispatch into one setup pass.
424
- create: (identifier, state) => orch.createWorkspace(identifier, state),
425
- });
426
- // PR autopilot wiring (issue 38). The Reconciler ignores this when
427
- // `pr.enabled` is false (it stays a no-op pass), so we set the
428
- // providers unconditionally — a reload that flips the flag picks them up
429
- // via `updateConfig`'s rebuild path.
430
- reconciler.setPrAutopilotProviders({
431
- intended: orch,
432
- // Pin the autopilot's `gh` calls to the configured target repo (env
433
- // SYMPHONY_REPO or workspace.github_repo) so they don't silently fall back
434
- // to cwd-inference in the out-of-repo layout the github_repo knob enables.
435
- // Captured at startup; retargeting needs a restart (see GhCliPrApi.repo).
436
- pr: new GhCliPrApi({ timeoutMs: 30_000, repo: resolveGithubRepo(config.workspace.github_repo) }),
437
- transition: {
438
- routeIssue: (input) => orch.routeIssueForAutopilot(input),
439
- },
440
- cleanup: {
441
- removeWorkspace: (identifier) => orch.removeWorkspace(identifier),
442
- },
443
- });
444
- }
445
- /**
446
- * Returns the orchestrator's `onConfigReloaded` callback. On every reload it
447
- * forwards the freshly-parsed config to each long-lived component, retargets
448
- * the persistent log sink if `logs.root` rotated (unless the env override
449
- * locked it for the process lifetime), and re-materializes any state
450
- * directory the new workflow introduced. The orchestrator's own onChange
451
- * handler already forwards to the reconciler (so a config change rebinds its
452
- * managed resources); we do not re-forward here.
453
- */
454
- function buildReloadHandler(opts) {
455
- const { tracker, workspaces, runner, mcp, envLogFile, onLiveCfg } = opts;
456
- return (cfg, def) => {
457
- tracker.updateConfig(cfg.tracker);
458
- workspaces.updateConfig(cfg);
459
- runner.updateConfig(cfg, def);
460
- mcp.updateStates(cfg.states, {
461
- enabled: cfg.pr.enabled,
462
- mergeState: derivePrRouting(cfg.states).mergeState,
463
- });
464
- onLiveCfg(cfg);
465
- if (envLogFile === undefined) {
466
- setLogFile(path.join(cfg.logs.root, 'symphony.log'));
467
- }
468
- // Best-effort: a mkdir failure here would normally come from a tracker.root
469
- // rotation that also failed at validateDispatch, so logging is enough.
470
- void tracker.start().catch((err) => {
471
- log.warn('tracker reinit after reload failed', { error: err.message });
472
- });
473
- };
474
- }
475
- /**
476
- * Bind the ACP TCP bridge, the optional HTTP server, and verify that — if MCP
477
- * is enabled — a reachable MCP URL can be constructed. Each bind/precondition
478
- * failure routes through `bailStartup` so the failure mode is uniform: write
479
- * to stderr, close any partial listeners, flush logs, exit non-zero.
480
- *
481
- * The ACP bridge must come up BEFORE we accept any dispatches: a bind failure
482
- * here is fatal because we cannot run agents without their transport. The MCP
483
- * precondition check is hoisted to boot so an in-VM agent doesn't fail mid-
484
- * dispatch (after the VM bring-up cost is sunk) with a misconfiguration the
485
- * operator could have caught at startup.
486
- */
487
- async function startTransports(opts) {
488
- const { config, graph, cli, src, workflowPath } = opts;
489
- // The Gondolin ACP channel is raw mapped TCP: the guest dials a synthetic name
490
- // tunnelled to the host loopback via `tcp.hosts`. So the bridge binds loopback
491
- // (the `reach_host`, default 127.0.0.1) and `loopbackOnly` hard-refuses a wider
492
- // bind — never the config `bind_host` (which defaults to 0.0.0.0 for the old
493
- // slirp gateway).
494
- const bridgeHost = config.acp.bridge.reach_host;
495
- try {
496
- await graph.acpBridge.start(bridgeHost, config.acp.bridge.bind_port);
497
- }
498
- catch (err) {
499
- await bailStartup(`error: failed to bind ACP bridge on ${bridgeHost}:${config.acp.bridge.bind_port}: ${err.message}\n`, { src });
500
- }
501
- startCredentialTicker(graph);
502
- const http = await bindHttpServer({ config, graph, cli, src, workflowPath });
503
- await checkMcpPrecondition({ config, graph, src, http });
504
- return { http };
505
- }
506
- /**
507
- * Start the host credential ticker (Gondolin secret-substitution model). There
508
- * is NO proxy server to bind — the registry seeds each per-VM secret manager at
509
- * dispatch and the ticker drives a periodic host-side refresh fan-out. So this
510
- * is just the ticker timer; nothing here can fail-to-bind.
511
- */
512
- function startCredentialTicker(graph) {
513
- graph.credentialTicker.start();
514
- }
515
- /**
516
- * Resolve the HTTP port (CLI override > workflow `server.port` > none), bind
517
- * if requested, and tell the MCP registry the *actually* bound port. The
518
- * registry needs the live port (not the requested one) so URLs injected into
519
- * agents point at the real listener — with `--port 0` the kernel picks an
520
- * ephemeral port that differs from what we asked for.
521
- */
522
- async function bindHttpServer(opts) {
523
- const { config, graph, cli, src, workflowPath } = opts;
524
- const httpPort = cli.port ?? config.server.port;
525
- if (httpPort === null || httpPort === undefined)
526
- return null;
527
- try {
528
- const http = await startHttpServer(graph.orch, {
529
- port: httpPort,
530
- host: config.server.host,
531
- // Canonical per-state config in workflow declaration order. The HTTP
532
- // dashboard reads role from here for pill colours, declared order for
533
- // the on-disk listing, and approve/discard targets — each consumer
534
- // filters by role on demand. The closure reads `liveCfg.states` on
535
- // every request, and the reload callback reassigns `liveCfg` to the
536
- // freshly-parsed config, so a workflow reload is reflected here
537
- // without rebinding the server. Phase 3 wired the equivalent for the
538
- // MCP registry via `mcp.updateStates`; this view is its dashboard twin.
539
- getTrackerView: () => ({
540
- trackerRoot: graph.getLiveCfg().tracker.root,
541
- states: Object.entries(graph.getLiveCfg().states).map(([name, cfg]) => ({
542
- name,
543
- role: cfg.role,
544
- })),
545
- workflowPath,
546
- }),
547
- mcp: graph.mcp,
548
- tracker: graph.tracker,
549
- });
550
- graph.mcp.setEffectivePort(http.port);
551
- return http;
552
- }
553
- catch (err) {
554
- // `await bailStartup(...)` resolves to `never` at runtime (the helper calls
555
- // process.exit), but TS doesn't propagate `Promise<never>` through `await`
556
- // for unreachability — `return` is what tells the type checker this branch
557
- // doesn't fall through.
558
- return await bailStartup(`error: failed to bind HTTP server on ${config.server.host}:${httpPort}: ${err.message}\n`, { src });
559
- }
560
- }
561
- /**
562
- * MCP precondition check: with mcp.enabled (the default), every dispatch will
563
- * require a reachable MCP URL. Verify NOW that one can be constructed, rather
564
- * than letting each per-issue dispatch fail with the same error after VM
565
- * bring-up costs are sunk. Two failure modes:
566
- *
567
- * 1. mcp.enabled but no HTTP listener bound — `mcp.host_url` lets an operator
568
- * point the in-VM agent at a reverse proxy but symphony itself must still
569
- * serve `/api/v1/issues/<id>/mcp`. Without a listener, the override would
570
- * advertise a URL nothing answers.
571
- * 2. mcp.enabled but the registry cannot build a URL (no port, no host_url).
572
- */
573
- async function checkMcpPrecondition(opts) {
574
- const { config, graph, src, http } = opts;
575
- if (!config.mcp.enabled)
576
- return;
577
- if (http === null) {
578
- await bailStartup(`error: mcp.enabled=true but no HTTP server is configured. Symphony itself\n` +
579
- `must bind a listener (set --port or server.port) so it can serve the MCP\n` +
580
- `endpoint, even when mcp.host_url points the in-VM agent at a reverse proxy.\n`, { src });
581
- }
582
- const probeUrl = graph.mcp.buildUrl('startup-check', {
583
- host: config.mcp.host,
584
- explicit_host_url: config.mcp.explicit_host_url,
585
- });
586
- if (probeUrl === null) {
587
- await bailStartup(`error: mcp.enabled=true but no MCP URL can be constructed. ` +
588
- `Set --port, server.port, or mcp.host_url so the in-VM agent can reach ` +
589
- `the symphony MCP endpoint.\n`, { http, src });
590
- }
591
- }
592
- /**
593
- * Human-facing startup summary on stdout. Once a file sink is active (the
594
- * default), this is the only orchestrator-side console output: structured
595
- * `log.*` lines are routed to the log file, so the operator sees just this
596
- * banner — what's running, where the dashboard is, and where the detailed log
597
- * stream went. `--verbose` additionally mirrors the structured stream to the
598
- * console. The companion `symphony started` structured line carries the same
599
- * facts into the log file (and onto stderr under --verbose).
600
- */
601
- function printStartupBanner(opts) {
602
- const { workflowPath, trackerRoot, host, http, logFile } = opts;
603
- // Map wildcard bind addresses to a clickable loopback host for the URL.
604
- const displayHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
605
- const dashboard = http === null ? '(disabled — pass --port or set server.port)' : `http://${displayHost}:${http.port}/`;
606
- const logs = logFile === null ? '(disabled — structured logs on stderr)' : `${logFile} (tail -f to follow)`;
607
- process.stdout.write(`symphony\n` +
608
- ` workflow ${workflowPath}\n` +
609
- ` tracker root ${trackerRoot ?? '<unset>'}\n` +
610
- ` dashboard ${dashboard}\n` +
611
- ` logs ${logs}\n`);
612
- }
613
- /** Per-step ceiling for each graceful-teardown step (`orch.stop`, the ACP
614
- * bridge, the HTTP listener). One stuck step — a wedged VM teardown, a
615
- * half-open ACP socket — times out and the remaining steps still run, so a
616
- * single hang can't wedge the whole shutdown (issue 152). */
617
- const SHUTDOWN_STEP_TIMEOUT_MS = 8_000;
618
- /** Hard ceiling on the entire graceful path. If teardown hasn't finished by
619
- * here, we stop waiting, force-kill child VMs, and exit non-zero — Ctrl+C
620
- * never appears dead, even when every per-step timeout is also wedged. */
621
- const SHUTDOWN_DEADLINE_MS = 15_000;
622
- /** Short cap on the log-sink flush during a force-quit. The VM SIGKILL itself is
623
- * synchronous, so the only thing the forced path awaits is draining symphony.log;
624
- * cap it so a wedged sink can't keep a second Ctrl+C from exiting promptly. */
625
- const FORCE_QUIT_FLUSH_TIMEOUT_MS = 2_000;
626
- /**
627
- * Race `work` against a `label`ed deadline. Rejects with a timeout Error if the
628
- * deadline wins; the underlying promise is left to settle on its own (we only
629
- * ever call this on the shutdown path, where the process exits shortly after).
630
- * The timer is `unref`'d so it can't by itself hold the event loop open.
631
- */
632
- function withTimeout(work, ms, label) {
633
- return new Promise((resolve, reject) => {
634
- const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
635
- timer.unref();
636
- work.then((v) => {
637
- clearTimeout(timer);
638
- resolve(v);
639
- }, (e) => {
640
- clearTimeout(timer);
641
- reject(e);
642
- });
643
- });
644
- }
645
- /**
646
- * The graceful teardown sequence. Each step gets its own per-step timeout +
647
- * catch so one wedged teardown can't starve the steps below it (the issue's
648
- * `orch.stop()` no-catch/no-timeout wedge). `orch.stop()` signals every worker
649
- * to unwind (each closes its VM) but does NOT await them, so the trailing
650
- * `vmClient.killAllVms()` is the backstop that guarantees no orphaned qemu: a
651
- * no-op once every worker has closed its VM, and a synchronous SIGKILL of any
652
- * qemu still live when a slow/timed-out `orch.stop()` returns early.
653
- */
654
- async function gracefulStop(deps) {
655
- const { graph, http, src } = deps;
656
- await withTimeout(graph.orch.stop(), SHUTDOWN_STEP_TIMEOUT_MS, 'orch.stop').catch((err) => log.warn('shutdown: orch.stop did not finish cleanly', { error: err.message }));
657
- await withTimeout(graph.acpBridge.stop(), SHUTDOWN_STEP_TIMEOUT_MS, 'acpBridge.stop').catch((err) => log.warn('shutdown: acpBridge.stop did not finish cleanly', { error: err.message }));
658
- graph.credentialTicker.stop();
659
- if (http) {
660
- await withTimeout(http.close(), SHUTDOWN_STEP_TIMEOUT_MS, 'http.close').catch((err) => log.warn('shutdown: http.close did not finish cleanly', { error: err.message }));
661
- }
662
- await src.stop().catch(() => undefined);
663
- // Synchronous backstop: SIGKILL any qemu whose VM didn't close in time. No
664
- // await — it can't hang, and a clean stop has already deregistered every VM.
665
- killChildVms(graph);
666
- await closeLogFile().catch(() => undefined);
667
- }
668
- /**
669
- * Force-quit path: SIGKILL any live child VMs (so a forced exit doesn't orphan
670
- * `qemu-system-x86_64`), flush the log sink under a short cap, then exit
671
- * non-zero. The kill is synchronous so a second Ctrl+C terminates immediately —
672
- * the only awaited step is the bounded log flush.
673
- */
674
- async function forceQuit(message, graph) {
675
- process.stdout.write(`\n${message}\n`);
676
- log.warn('shutdown: force-quitting — SIGKILL child VMs then exit');
677
- killChildVms(graph);
678
- await withTimeout(closeLogFile(), FORCE_QUIT_FLUSH_TIMEOUT_MS, 'closeLogFile').catch(() => undefined);
679
- process.exit(1);
680
- }
681
- /**
682
- * Synchronously SIGKILL every live VM's backing qemu (issue 152). The qemu
683
- * processes are direct children of this in-process orchestrator; left alone,
684
- * `process.exit` reparents them to init and they survive as orphans. Bounded and
685
- * best-effort — never throws — so both the graceful backstop and the force path
686
- * can call it on the way out without risking a hang.
687
- */
688
- function killChildVms(graph) {
689
- try {
690
- const killed = graph.vmClient.killAllVms();
691
- if (killed > 0)
692
- log.info('shutdown: SIGKILL child VMs', { count: killed });
693
- }
694
- catch (err) {
695
- log.warn('shutdown: killAllVms threw', { error: err.message });
696
- }
697
- }
698
- /**
699
- * Run the graceful path under a hard deadline. On a clean stop we exit 0; if the
700
- * deadline elapses (teardown wedged past even the per-step timeouts) we fall
701
- * through to the force-quit path so the process never hangs on Ctrl+C.
702
- */
703
- async function runGracefulShutdown(signal, deps) {
704
- process.stdout.write(`\nsymphony: ${signal} received — shutting down gracefully… (press Ctrl+C again to force-quit)\n`);
705
- log.info('shutdown requested', { signal });
706
- try {
707
- await withTimeout(gracefulStop(deps), SHUTDOWN_DEADLINE_MS, 'graceful shutdown');
708
- }
709
- catch (err) {
710
- log.warn('graceful shutdown exceeded deadline; forcing', { error: err.message });
711
- await forceQuit(`symphony: graceful shutdown exceeded ${SHUTDOWN_DEADLINE_MS}ms — force-quitting; killing child VMs.`, deps.graph);
712
- }
713
- process.stdout.write('symphony: shutdown complete.\n');
714
- process.exit(0);
715
- }
716
- /**
717
- * Wire SIGINT/SIGTERM. The first signal runs the deadline-bounded graceful path;
718
- * a SECOND signal force-quits immediately (force-killing child VMs first)
719
- * instead of re-entering the same in-flight teardown (issue 152). With no
720
- * console output the old graceful shutdown looked dead on a wedged VM/bridge
721
- * teardown, so both paths announce on the operator's console.
722
- */
723
- function installShutdownHandlers(deps) {
724
- let shuttingDown = false;
725
- const onSignal = (signal) => {
726
- if (shuttingDown) {
727
- void forceQuit(`symphony: ${signal} again — graceful shutdown is taking too long, force-quitting; killing child VMs.`, deps.graph);
728
- return;
729
- }
730
- shuttingDown = true;
731
- void runGracefulShutdown(signal, deps);
732
- };
733
- process.on('SIGINT', () => onSignal('SIGINT'));
734
- process.on('SIGTERM', () => onSignal('SIGTERM'));
735
- }
736
- async function main() {
737
- const cli = parseCli(process.argv.slice(2));
738
- // --verbose / --foreground: mirror structured logs to the console even when
739
- // the file sink is active. Set before any log.* call so every line honors it.
740
- setLogVerbose(cli.verbose);
741
- const workflowPath = path.resolve(cli.workflow);
742
- await handlePreflight(cli, workflowPath);
743
- const { src, config, definition, envLogFile, logFile } = await loadAndValidateConfig(workflowPath);
744
- const graph = await buildOrchestratorGraph({ config, definition, src, envLogFile });
745
- const { http } = await startTransports({ config, graph, cli, src, workflowPath });
746
- try {
747
- await graph.orch.start();
748
- }
749
- catch (err) {
750
- await bailStartup(`startup failed: ${err.message}\n`, { http, src });
751
- }
752
- if (cli.reconcileForce) {
753
- // `--reconcile-force`: request an immediate reconcile pass (VM/workspace/PR
754
- // janitors) instead of waiting on the backstop tick. The bake artifact this
755
- // flag used to invalidate is gone (the agent image is built ahead of time),
756
- // so it now just triggers an eager pass.
757
- log.info('reconcile --force requested');
758
- void graph.orch.triggerReconcile({ force: true }).catch((err) => log.warn('reconcile --force failed', { error: err.message }));
759
- }
760
- log.info('symphony started', {
761
- workflow: workflowPath,
762
- workspace_root: config.workspace.root,
763
- tracker_root: config.tracker.root,
764
- log_file: logFile ?? '<disabled>',
765
- poll_interval_ms: config.polling.interval_ms,
766
- // Actually-bound port (differs from the requested port with --port 0); null
767
- // when no HTTP listener is configured.
768
- http_port: http?.port ?? null,
769
- });
770
- // Clean human-facing summary on stdout. With a file sink active (the default)
771
- // the structured line above goes to the log file only, so this banner is what
772
- // the operator sees on the console (issue 118).
773
- printStartupBanner({
774
- workflowPath,
775
- trackerRoot: config.tracker.root,
776
- host: config.server.host,
777
- http,
778
- logFile,
779
- });
780
- // Graceful shutdown is async and can stall on a wedged VM/bridge teardown
781
- // (issue 152), so the handlers run the graceful path under a hard deadline,
782
- // announce on the operator's console, and escalate a SECOND signal to an
783
- // immediate force-quit (force-killing child VMs first) instead of re-entering
784
- // the same hanging await.
785
- installShutdownHandlers({ graph, http, src });
786
- }
787
- main().catch(async (err) => {
788
- process.stderr.write(`fatal: ${err.message}\n${err.stack ?? ''}\n`);
789
- // setLogFile() may have been called before main() threw; flush the sink so
790
- // any log.* lines emitted before the fault reach symphony.log.
791
- await closeLogFile().catch(() => undefined);
792
- process.exit(1);
793
- });
794
- //# sourceMappingURL=symphony.js.map