smol-symphony 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (627) hide show
  1. package/AGENTS.md +125 -39
  2. package/DESIGN.md +494 -273
  3. package/PRODUCT.md +2 -1
  4. package/README.md +259 -110
  5. package/SPEC.md +565 -1928
  6. package/WORKFLOW.minimal.yaml +34 -0
  7. package/WORKFLOW.template.yaml +1100 -0
  8. package/WORKFLOW.yaml +487 -0
  9. package/assets/skills/symphony-issues/SKILL.md +136 -0
  10. package/assets/symphony-mise.system.toml +68 -0
  11. package/dist/src/bin/symphony.js +30 -0
  12. package/dist/src/bin/symphony.js.map +1 -0
  13. package/dist/src/core/actions/context.js +109 -0
  14. package/dist/src/core/actions/context.js.map +1 -0
  15. package/dist/src/core/actions/parse.js +192 -0
  16. package/dist/src/core/actions/parse.js.map +1 -0
  17. package/dist/src/core/actions/plan.js +197 -0
  18. package/dist/src/core/actions/plan.js.map +1 -0
  19. package/dist/src/core/actions/predicates.js +111 -0
  20. package/dist/src/core/actions/predicates.js.map +1 -0
  21. package/dist/src/core/actions/run-fold.js +248 -0
  22. package/dist/src/core/actions/run-fold.js.map +1 -0
  23. package/dist/src/core/actions/template.js +118 -0
  24. package/dist/src/core/actions/template.js.map +1 -0
  25. package/dist/src/core/cli/args.js +116 -0
  26. package/dist/src/core/cli/args.js.map +1 -0
  27. package/dist/src/core/coerce.js +75 -0
  28. package/dist/src/core/coerce.js.map +1 -0
  29. package/dist/src/core/credential/account-id.js +20 -0
  30. package/dist/src/core/credential/account-id.js.map +1 -0
  31. package/dist/src/core/credential/adapter-config.js +136 -0
  32. package/dist/src/core/credential/adapter-config.js.map +1 -0
  33. package/dist/src/core/credential/availability.js +98 -0
  34. package/dist/src/core/credential/availability.js.map +1 -0
  35. package/dist/src/core/credential/extract.js +228 -0
  36. package/dist/src/core/credential/extract.js.map +1 -0
  37. package/dist/src/core/credential/fake-creds.js +171 -0
  38. package/dist/src/core/credential/fake-creds.js.map +1 -0
  39. package/dist/src/core/credential/identity.js +125 -0
  40. package/dist/src/core/credential/identity.js.map +1 -0
  41. package/dist/src/core/credential/shape.js +230 -0
  42. package/dist/src/core/credential/shape.js.map +1 -0
  43. package/dist/src/core/credential/strings.js +15 -0
  44. package/dist/src/core/credential/strings.js.map +1 -0
  45. package/dist/src/core/doctor/checks.js +303 -0
  46. package/dist/src/core/doctor/checks.js.map +1 -0
  47. package/dist/src/core/git/result.js +107 -0
  48. package/dist/src/core/git/result.js.map +1 -0
  49. package/dist/src/core/http/decisions.js +225 -0
  50. package/dist/src/core/http/decisions.js.map +1 -0
  51. package/dist/src/core/http/render.js +1635 -0
  52. package/dist/src/core/http/render.js.map +1 -0
  53. package/dist/src/core/http/routes.js +145 -0
  54. package/dist/src/core/http/routes.js.map +1 -0
  55. package/dist/src/core/http/views.js +181 -0
  56. package/dist/src/core/http/views.js.map +1 -0
  57. package/dist/src/core/image/managed-image.js +95 -0
  58. package/dist/src/core/image/managed-image.js.map +1 -0
  59. package/dist/src/core/issue/file.js +149 -0
  60. package/dist/src/core/issue/file.js.map +1 -0
  61. package/dist/src/core/issue/parse.js +210 -0
  62. package/dist/src/core/issue/parse.js.map +1 -0
  63. package/dist/src/core/mcp/dispatch.js +239 -0
  64. package/dist/src/core/mcp/dispatch.js.map +1 -0
  65. package/dist/src/core/mcp/post-move.js +92 -0
  66. package/dist/src/core/mcp/post-move.js.map +1 -0
  67. package/dist/src/core/mcp/protocol.js +293 -0
  68. package/dist/src/core/mcp/protocol.js.map +1 -0
  69. package/dist/src/core/mcp/url.js +162 -0
  70. package/dist/src/core/mcp/url.js.map +1 -0
  71. package/dist/src/core/path.js +63 -0
  72. package/dist/src/core/path.js.map +1 -0
  73. package/dist/src/core/reconcile/image-decide.js +48 -0
  74. package/dist/src/core/reconcile/image-decide.js.map +1 -0
  75. package/dist/src/core/reconcile/ledger.js +142 -0
  76. package/dist/src/core/reconcile/ledger.js.map +1 -0
  77. package/dist/src/core/reconcile/pr-classify.js +62 -0
  78. package/dist/src/core/reconcile/pr-classify.js.map +1 -0
  79. package/dist/src/core/reconcile/pr-decide.js +180 -0
  80. package/dist/src/core/reconcile/pr-decide.js.map +1 -0
  81. package/dist/src/core/reconcile/pr-loop.js +161 -0
  82. package/dist/src/core/reconcile/pr-loop.js.map +1 -0
  83. package/dist/src/core/reconcile/pr-notes.js +35 -0
  84. package/dist/src/core/reconcile/pr-notes.js.map +1 -0
  85. package/dist/src/core/reconcile/vm-decide.js +70 -0
  86. package/dist/src/core/reconcile/vm-decide.js.map +1 -0
  87. package/dist/src/core/reconcile/vm-reap.js +207 -0
  88. package/dist/src/core/reconcile/vm-reap.js.map +1 -0
  89. package/dist/src/core/reconcile/workspace-decide.js +162 -0
  90. package/dist/src/core/reconcile/workspace-decide.js.map +1 -0
  91. package/dist/src/core/runlog/summary.js +231 -0
  92. package/dist/src/core/runlog/summary.js.map +1 -0
  93. package/dist/src/core/runner/dispatch-config.js +95 -0
  94. package/dist/src/core/runner/dispatch-config.js.map +1 -0
  95. package/dist/src/core/runner/injection.js +61 -0
  96. package/dist/src/core/runner/injection.js.map +1 -0
  97. package/dist/src/core/runner/mise.js +210 -0
  98. package/dist/src/core/runner/mise.js.map +1 -0
  99. package/dist/src/core/runner/prompt.js +720 -0
  100. package/dist/src/core/runner/prompt.js.map +1 -0
  101. package/dist/src/core/runner/turn.js +242 -0
  102. package/dist/src/core/runner/turn.js.map +1 -0
  103. package/dist/src/core/runner/vm-plan.js +390 -0
  104. package/dist/src/core/runner/vm-plan.js.map +1 -0
  105. package/dist/src/core/schedule/admission.js +123 -0
  106. package/dist/src/core/schedule/admission.js.map +1 -0
  107. package/dist/src/core/schedule/circuit-breaker.js +111 -0
  108. package/dist/src/core/schedule/circuit-breaker.js.map +1 -0
  109. package/dist/src/core/schedule/eligibility.js +83 -0
  110. package/dist/src/core/schedule/eligibility.js.map +1 -0
  111. package/dist/src/core/schedule/reconcile-issue.js +82 -0
  112. package/dist/src/core/schedule/reconcile-issue.js.map +1 -0
  113. package/dist/src/core/schedule/retry.js +96 -0
  114. package/dist/src/core/schedule/retry.js.map +1 -0
  115. package/dist/src/core/schedule/sleep-cycle.js +133 -0
  116. package/dist/src/core/schedule/sleep-cycle.js.map +1 -0
  117. package/dist/src/core/schedule/slots.js +124 -0
  118. package/dist/src/core/schedule/slots.js.map +1 -0
  119. package/dist/src/core/schedule/tick.js +553 -0
  120. package/dist/src/core/schedule/tick.js.map +1 -0
  121. package/dist/src/core/schedule/token-fold.js +181 -0
  122. package/dist/src/core/schedule/token-fold.js.map +1 -0
  123. package/dist/src/core/state-resolve.js +86 -0
  124. package/dist/src/core/state-resolve.js.map +1 -0
  125. package/dist/src/core/vm-guards.js +278 -0
  126. package/dist/src/core/vm-guards.js.map +1 -0
  127. package/dist/src/core/workflow/derive.js +107 -0
  128. package/dist/src/core/workflow/derive.js.map +1 -0
  129. package/dist/src/core/workflow/parse.js +687 -0
  130. package/dist/src/core/workflow/parse.js.map +1 -0
  131. package/dist/src/core/workflow/prompt-probe.js +78 -0
  132. package/dist/src/core/workflow/prompt-probe.js.map +1 -0
  133. package/dist/src/core/workflow/validate.js +189 -0
  134. package/dist/src/core/workflow/validate.js.map +1 -0
  135. package/dist/src/core/workspace-key.js +19 -0
  136. package/dist/src/core/workspace-key.js.map +1 -0
  137. package/dist/src/shell/actions-runner.js +356 -0
  138. package/dist/src/shell/actions-runner.js.map +1 -0
  139. package/dist/src/shell/adapter/adapter-registry.js +45 -0
  140. package/dist/src/shell/adapter/adapter-registry.js.map +1 -0
  141. package/dist/src/shell/adapter/clock-random.js +96 -0
  142. package/dist/src/shell/adapter/clock-random.js.map +1 -0
  143. package/dist/src/shell/adapter/gondolin-dispatch-helpers.js +158 -0
  144. package/dist/src/shell/adapter/gondolin-dispatch-helpers.js.map +1 -0
  145. package/dist/src/shell/adapter/gondolin-dispatch.js +385 -0
  146. package/dist/src/shell/adapter/gondolin-dispatch.js.map +1 -0
  147. package/dist/src/shell/adapter/gondolin-image-converter.js +233 -0
  148. package/dist/src/shell/adapter/gondolin-image-converter.js.map +1 -0
  149. package/dist/src/shell/adapter/gondolin-image-fetch.js +180 -0
  150. package/dist/src/shell/adapter/gondolin-image-fetch.js.map +1 -0
  151. package/dist/src/shell/adapter/launcher-asset.js +57 -0
  152. package/dist/src/shell/adapter/launcher-asset.js.map +1 -0
  153. package/dist/src/shell/adapter/mise-config-asset.js +65 -0
  154. package/dist/src/shell/adapter/mise-config-asset.js.map +1 -0
  155. package/dist/src/shell/adapter/workflow-loader.js +304 -0
  156. package/dist/src/shell/adapter/workflow-loader.js.map +1 -0
  157. package/dist/src/shell/cli/doctor.js +268 -0
  158. package/dist/src/shell/cli/doctor.js.map +1 -0
  159. package/dist/src/shell/effect-interpreter-families.js +314 -0
  160. package/dist/src/shell/effect-interpreter-families.js.map +1 -0
  161. package/dist/src/shell/effect-interpreter.js +29 -0
  162. package/dist/src/shell/effect-interpreter.js.map +1 -0
  163. package/dist/src/shell/interp/acp-frame.js +137 -0
  164. package/dist/src/shell/interp/acp-frame.js.map +1 -0
  165. package/dist/src/shell/interp/acp-ws-conn.js +320 -0
  166. package/dist/src/shell/interp/acp-ws-conn.js.map +1 -0
  167. package/dist/src/shell/interp/acp-ws-frames.js +159 -0
  168. package/dist/src/shell/interp/acp-ws-frames.js.map +1 -0
  169. package/dist/src/shell/interp/acp-ws.js +197 -0
  170. package/dist/src/shell/interp/acp-ws.js.map +1 -0
  171. package/dist/src/shell/interp/acp.js +319 -0
  172. package/dist/src/shell/interp/acp.js.map +1 -0
  173. package/dist/src/shell/interp/credential-defaults.js +128 -0
  174. package/dist/src/shell/interp/credential-defaults.js.map +1 -0
  175. package/dist/src/shell/interp/credential-hooks.js +149 -0
  176. package/dist/src/shell/interp/credential-hooks.js.map +1 -0
  177. package/dist/src/shell/interp/credential-registry.js +226 -0
  178. package/dist/src/shell/interp/credential-registry.js.map +1 -0
  179. package/dist/src/shell/interp/credential.js +103 -0
  180. package/dist/src/shell/interp/credential.js.map +1 -0
  181. package/dist/src/shell/interp/gh.js +163 -0
  182. package/dist/src/shell/interp/gh.js.map +1 -0
  183. package/dist/src/shell/interp/git.js +28 -0
  184. package/dist/src/shell/interp/git.js.map +1 -0
  185. package/dist/src/shell/interp/log.js +213 -0
  186. package/dist/src/shell/interp/log.js.map +1 -0
  187. package/dist/src/shell/interp/process.js +178 -0
  188. package/dist/src/shell/interp/process.js.map +1 -0
  189. package/dist/src/shell/interp/runlog.js +193 -0
  190. package/dist/src/shell/interp/runlog.js.map +1 -0
  191. package/dist/src/shell/interp/timer.js +64 -0
  192. package/dist/src/shell/interp/timer.js.map +1 -0
  193. package/dist/src/shell/interp/tracker-disk.js +99 -0
  194. package/dist/src/shell/interp/tracker-disk.js.map +1 -0
  195. package/dist/src/shell/interp/tracker-parse.js +71 -0
  196. package/dist/src/shell/interp/tracker-parse.js.map +1 -0
  197. package/dist/src/shell/interp/tracker-scan.js +238 -0
  198. package/dist/src/shell/interp/tracker-scan.js.map +1 -0
  199. package/dist/src/shell/interp/tracker-write.js +91 -0
  200. package/dist/src/shell/interp/tracker-write.js.map +1 -0
  201. package/dist/src/shell/interp/tracker.js +41 -0
  202. package/dist/src/shell/interp/tracker.js.map +1 -0
  203. package/dist/src/shell/interp/tty.js +48 -0
  204. package/dist/src/shell/interp/tty.js.map +1 -0
  205. package/dist/src/shell/interp/vm.js +199 -0
  206. package/dist/src/shell/interp/vm.js.map +1 -0
  207. package/dist/src/shell/interp/workspace.js +310 -0
  208. package/dist/src/shell/interp/workspace.js.map +1 -0
  209. package/dist/src/shell/main-acp.js +78 -0
  210. package/dist/src/shell/main-acp.js.map +1 -0
  211. package/dist/src/shell/main-adapters.js +222 -0
  212. package/dist/src/shell/main-adapters.js.map +1 -0
  213. package/dist/src/shell/main-credential.js +122 -0
  214. package/dist/src/shell/main-credential.js.map +1 -0
  215. package/dist/src/shell/main-doctor.js +22 -0
  216. package/dist/src/shell/main-doctor.js.map +1 -0
  217. package/dist/src/shell/main-entry.js +46 -0
  218. package/dist/src/shell/main-entry.js.map +1 -0
  219. package/dist/src/shell/main-http-csrf.js +45 -0
  220. package/dist/src/shell/main-http-csrf.js.map +1 -0
  221. package/dist/src/shell/main-http-handler.js +389 -0
  222. package/dist/src/shell/main-http-handler.js.map +1 -0
  223. package/dist/src/shell/main-http-mcp.js +122 -0
  224. package/dist/src/shell/main-http-mcp.js.map +1 -0
  225. package/dist/src/shell/main-http-views.js +253 -0
  226. package/dist/src/shell/main-http-views.js.map +1 -0
  227. package/dist/src/shell/main-http.js +76 -0
  228. package/dist/src/shell/main-http.js.map +1 -0
  229. package/dist/src/shell/main-loops.js +130 -0
  230. package/dist/src/shell/main-loops.js.map +1 -0
  231. package/dist/src/shell/main-mcp.js +129 -0
  232. package/dist/src/shell/main-mcp.js.map +1 -0
  233. package/dist/src/shell/main-orchestrator.js +120 -0
  234. package/dist/src/shell/main-orchestrator.js.map +1 -0
  235. package/dist/src/shell/main-preflight.js +43 -0
  236. package/dist/src/shell/main-preflight.js.map +1 -0
  237. package/dist/src/shell/main-reconcilers-helpers.js +244 -0
  238. package/dist/src/shell/main-reconcilers-helpers.js.map +1 -0
  239. package/dist/src/shell/main-reconcilers-pr.js +148 -0
  240. package/dist/src/shell/main-reconcilers-pr.js.map +1 -0
  241. package/dist/src/shell/main-reconcilers.js +225 -0
  242. package/dist/src/shell/main-reconcilers.js.map +1 -0
  243. package/dist/src/shell/main-runner.js +355 -0
  244. package/dist/src/shell/main-runner.js.map +1 -0
  245. package/dist/src/shell/main-scaffold.js +116 -0
  246. package/dist/src/shell/main-scaffold.js.map +1 -0
  247. package/dist/src/shell/main-shutdown.js +115 -0
  248. package/dist/src/shell/main-shutdown.js.map +1 -0
  249. package/dist/src/shell/main-startup.js +48 -0
  250. package/dist/src/shell/main-startup.js.map +1 -0
  251. package/dist/src/shell/main-substrates.js +43 -0
  252. package/dist/src/shell/main-substrates.js.map +1 -0
  253. package/dist/src/shell/main.js +385 -0
  254. package/dist/src/shell/main.js.map +1 -0
  255. package/dist/src/shell/orchestrator-feedback.js +69 -0
  256. package/dist/src/shell/orchestrator-feedback.js.map +1 -0
  257. package/dist/src/shell/orchestrator-image.js +167 -0
  258. package/dist/src/shell/orchestrator-image.js.map +1 -0
  259. package/dist/src/shell/orchestrator-loop.js +468 -0
  260. package/dist/src/shell/orchestrator-loop.js.map +1 -0
  261. package/dist/src/shell/orchestrator-reconcile.js +36 -0
  262. package/dist/src/shell/orchestrator-reconcile.js.map +1 -0
  263. package/dist/src/shell/reconciler-loop.js +228 -0
  264. package/dist/src/shell/reconciler-loop.js.map +1 -0
  265. package/dist/src/shell/runner-loop-turn.js +301 -0
  266. package/dist/src/shell/runner-loop-turn.js.map +1 -0
  267. package/dist/src/shell/runner-loop.js +338 -0
  268. package/dist/src/shell/runner-loop.js.map +1 -0
  269. package/dist/src/shell/server/http.js +208 -0
  270. package/dist/src/shell/server/http.js.map +1 -0
  271. package/dist/src/shell/server/mcp-runtime-effects.js +237 -0
  272. package/dist/src/shell/server/mcp-runtime-effects.js.map +1 -0
  273. package/dist/src/shell/server/mcp-runtime.js +99 -0
  274. package/dist/src/shell/server/mcp-runtime.js.map +1 -0
  275. package/dist/src/shell/workspace-key.js +14 -0
  276. package/dist/src/shell/workspace-key.js.map +1 -0
  277. package/dist/src/types/acp.js +8 -0
  278. package/dist/src/types/acp.js.map +1 -0
  279. package/dist/src/types/actions/plan.js +6 -0
  280. package/dist/src/types/actions/plan.js.map +1 -0
  281. package/dist/src/types/actions/predicates.js +6 -0
  282. package/dist/src/types/actions/predicates.js.map +1 -0
  283. package/dist/src/types/actions/run-fold.js +8 -0
  284. package/dist/src/types/actions/run-fold.js.map +1 -0
  285. package/dist/src/types/actions.js +7 -0
  286. package/dist/src/types/actions.js.map +1 -0
  287. package/dist/src/types/adapter/clock-random.js +4 -0
  288. package/dist/src/types/adapter/clock-random.js.map +1 -0
  289. package/dist/src/types/adapter/gondolin-image-converter.js +5 -0
  290. package/dist/src/types/adapter/gondolin-image-converter.js.map +1 -0
  291. package/dist/src/types/adapter/gondolin-image-fetch.js +5 -0
  292. package/dist/src/types/adapter/gondolin-image-fetch.js.map +1 -0
  293. package/dist/src/types/adapter/workflow-loader.js +4 -0
  294. package/dist/src/types/adapter/workflow-loader.js.map +1 -0
  295. package/dist/src/types/cli/args.js +8 -0
  296. package/dist/src/types/cli/args.js.map +1 -0
  297. package/dist/src/types/config.js +8 -0
  298. package/dist/src/types/config.js.map +1 -0
  299. package/dist/src/types/credential-interp.js +6 -0
  300. package/dist/src/types/credential-interp.js.map +1 -0
  301. package/dist/src/types/credentials.js +10 -0
  302. package/dist/src/types/credentials.js.map +1 -0
  303. package/dist/src/types/doctor.js +7 -0
  304. package/dist/src/types/doctor.js.map +1 -0
  305. package/dist/src/types/domain.js +7 -0
  306. package/dist/src/types/domain.js.map +1 -0
  307. package/dist/src/types/effect.js +15 -0
  308. package/dist/src/types/effect.js.map +1 -0
  309. package/dist/src/types/errors.js +39 -0
  310. package/dist/src/types/errors.js.map +1 -0
  311. package/dist/src/types/http/decisions.js +6 -0
  312. package/dist/src/types/http/decisions.js.map +1 -0
  313. package/dist/src/types/http/render.js +10 -0
  314. package/dist/src/types/http/render.js.map +1 -0
  315. package/dist/src/types/http/views.js +6 -0
  316. package/dist/src/types/http/views.js.map +1 -0
  317. package/dist/src/types/http.js +9 -0
  318. package/dist/src/types/http.js.map +1 -0
  319. package/dist/src/types/image/managed-image.js +7 -0
  320. package/dist/src/types/image/managed-image.js.map +1 -0
  321. package/dist/src/types/interp/effect-interpreter.js +8 -0
  322. package/dist/src/types/interp/effect-interpreter.js.map +1 -0
  323. package/dist/src/types/interp/tracker.js +7 -0
  324. package/dist/src/types/interp/tracker.js.map +1 -0
  325. package/dist/src/types/issue/file.js +6 -0
  326. package/dist/src/types/issue/file.js.map +1 -0
  327. package/dist/src/types/issue/parse.js +8 -0
  328. package/dist/src/types/issue/parse.js.map +1 -0
  329. package/dist/src/types/main-acp.js +13 -0
  330. package/dist/src/types/main-acp.js.map +1 -0
  331. package/dist/src/types/main-adapters.js +5 -0
  332. package/dist/src/types/main-adapters.js.map +1 -0
  333. package/dist/src/types/main-credential.js +21 -0
  334. package/dist/src/types/main-credential.js.map +1 -0
  335. package/dist/src/types/main-doctor.js +6 -0
  336. package/dist/src/types/main-doctor.js.map +1 -0
  337. package/dist/src/types/main-http-handler.js +12 -0
  338. package/dist/src/types/main-http-handler.js.map +1 -0
  339. package/dist/src/types/main-http.js +5 -0
  340. package/dist/src/types/main-http.js.map +1 -0
  341. package/dist/src/types/main-loops.js +5 -0
  342. package/dist/src/types/main-loops.js.map +1 -0
  343. package/dist/src/types/main-mcp.js +12 -0
  344. package/dist/src/types/main-mcp.js.map +1 -0
  345. package/dist/src/types/main-orchestrator.js +5 -0
  346. package/dist/src/types/main-orchestrator.js.map +1 -0
  347. package/dist/src/types/main-reconcilers.js +11 -0
  348. package/dist/src/types/main-reconcilers.js.map +1 -0
  349. package/dist/src/types/main-runner.js +13 -0
  350. package/dist/src/types/main-runner.js.map +1 -0
  351. package/dist/src/types/main-startup.js +5 -0
  352. package/dist/src/types/main-startup.js.map +1 -0
  353. package/dist/src/types/main-substrates.js +5 -0
  354. package/dist/src/types/main-substrates.js.map +1 -0
  355. package/dist/src/types/mcp/dispatch.js +4 -0
  356. package/dist/src/types/mcp/dispatch.js.map +1 -0
  357. package/dist/src/types/mcp/post-move.js +7 -0
  358. package/dist/src/types/mcp/post-move.js.map +1 -0
  359. package/dist/src/types/mcp.js +9 -0
  360. package/dist/src/types/mcp.js.map +1 -0
  361. package/dist/src/types/ports.js +12 -0
  362. package/dist/src/types/ports.js.map +1 -0
  363. package/dist/src/types/reconcile/image-decide.js +5 -0
  364. package/dist/src/types/reconcile/image-decide.js.map +1 -0
  365. package/dist/src/types/reconcile/ledger.js +7 -0
  366. package/dist/src/types/reconcile/ledger.js.map +1 -0
  367. package/dist/src/types/reconcile/pr-loop.js +8 -0
  368. package/dist/src/types/reconcile/pr-loop.js.map +1 -0
  369. package/dist/src/types/reconcile/vm-reap.js +8 -0
  370. package/dist/src/types/reconcile/vm-reap.js.map +1 -0
  371. package/dist/src/types/reconcile/workspace-decide.js +7 -0
  372. package/dist/src/types/reconcile/workspace-decide.js.map +1 -0
  373. package/dist/src/types/reconcile.js +9 -0
  374. package/dist/src/types/reconcile.js.map +1 -0
  375. package/dist/src/types/runlog.js +7 -0
  376. package/dist/src/types/runlog.js.map +1 -0
  377. package/dist/src/types/runner/actions-runner.js +12 -0
  378. package/dist/src/types/runner/actions-runner.js.map +1 -0
  379. package/dist/src/types/runner/gondolin-dispatch.js +5 -0
  380. package/dist/src/types/runner/gondolin-dispatch.js.map +1 -0
  381. package/dist/src/types/runner/injection.js +6 -0
  382. package/dist/src/types/runner/injection.js.map +1 -0
  383. package/dist/src/types/runner/runner-loop.js +5 -0
  384. package/dist/src/types/runner/runner-loop.js.map +1 -0
  385. package/dist/src/types/runner/turn.js +4 -0
  386. package/dist/src/types/runner/turn.js.map +1 -0
  387. package/dist/src/types/runner/vm-plan.js +4 -0
  388. package/dist/src/types/runner/vm-plan.js.map +1 -0
  389. package/dist/src/types/runtime.js +9 -0
  390. package/dist/src/types/runtime.js.map +1 -0
  391. package/dist/src/types/schedule/admission.js +7 -0
  392. package/dist/src/types/schedule/admission.js.map +1 -0
  393. package/dist/src/types/schedule/circuit-breaker.js +2 -0
  394. package/dist/src/types/schedule/circuit-breaker.js.map +1 -0
  395. package/dist/src/types/schedule/eligibility.js +9 -0
  396. package/dist/src/types/schedule/eligibility.js.map +1 -0
  397. package/dist/src/types/schedule/orchestrator-loop.js +10 -0
  398. package/dist/src/types/schedule/orchestrator-loop.js.map +1 -0
  399. package/dist/src/types/schedule/sleep-cycle.js +4 -0
  400. package/dist/src/types/schedule/sleep-cycle.js.map +1 -0
  401. package/dist/src/types/schedule/slots.js +8 -0
  402. package/dist/src/types/schedule/slots.js.map +1 -0
  403. package/dist/src/types/schedule/tick.js +9 -0
  404. package/dist/src/types/schedule/tick.js.map +1 -0
  405. package/dist/src/types/server/mcp-runtime.js +8 -0
  406. package/dist/src/types/server/mcp-runtime.js.map +1 -0
  407. package/dist/src/types/workflow/parse.js +4 -0
  408. package/dist/src/types/workflow/parse.js.map +1 -0
  409. package/dist/tests/core/account-id.test.js +35 -0
  410. package/dist/tests/core/account-id.test.js.map +1 -0
  411. package/dist/tests/core/actions-parse.test.js +176 -0
  412. package/dist/tests/core/actions-parse.test.js.map +1 -0
  413. package/dist/tests/core/adapter-config.test.js +133 -0
  414. package/dist/tests/core/adapter-config.test.js.map +1 -0
  415. package/dist/tests/core/admission.test.js +215 -0
  416. package/dist/tests/core/admission.test.js.map +1 -0
  417. package/dist/tests/core/args.test.js +132 -0
  418. package/dist/tests/core/args.test.js.map +1 -0
  419. package/dist/tests/core/availability.test.js +62 -0
  420. package/dist/tests/core/availability.test.js.map +1 -0
  421. package/dist/tests/core/checks.test.js +395 -0
  422. package/dist/tests/core/checks.test.js.map +1 -0
  423. package/dist/tests/core/circuit-breaker.test.js +172 -0
  424. package/dist/tests/core/circuit-breaker.test.js.map +1 -0
  425. package/dist/tests/core/coerce.test.js +87 -0
  426. package/dist/tests/core/coerce.test.js.map +1 -0
  427. package/dist/tests/core/context.test.js +228 -0
  428. package/dist/tests/core/context.test.js.map +1 -0
  429. package/dist/tests/core/decisions.test.js +310 -0
  430. package/dist/tests/core/decisions.test.js.map +1 -0
  431. package/dist/tests/core/derive.test.js +205 -0
  432. package/dist/tests/core/derive.test.js.map +1 -0
  433. package/dist/tests/core/dispatch-config.test.js +164 -0
  434. package/dist/tests/core/dispatch-config.test.js.map +1 -0
  435. package/dist/tests/core/dispatch.test.js +302 -0
  436. package/dist/tests/core/dispatch.test.js.map +1 -0
  437. package/dist/tests/core/eligibility.test.js +163 -0
  438. package/dist/tests/core/eligibility.test.js.map +1 -0
  439. package/dist/tests/core/extract.test.js +139 -0
  440. package/dist/tests/core/extract.test.js.map +1 -0
  441. package/dist/tests/core/fake-creds.test.js +134 -0
  442. package/dist/tests/core/fake-creds.test.js.map +1 -0
  443. package/dist/tests/core/file.test.js +197 -0
  444. package/dist/tests/core/file.test.js.map +1 -0
  445. package/dist/tests/core/git-result.test.js +113 -0
  446. package/dist/tests/core/git-result.test.js.map +1 -0
  447. package/dist/tests/core/identity.test.js +180 -0
  448. package/dist/tests/core/identity.test.js.map +1 -0
  449. package/dist/tests/core/image-decide.test.js +59 -0
  450. package/dist/tests/core/image-decide.test.js.map +1 -0
  451. package/dist/tests/core/injection.test.js +163 -0
  452. package/dist/tests/core/injection.test.js.map +1 -0
  453. package/dist/tests/core/ledger.test.js +218 -0
  454. package/dist/tests/core/ledger.test.js.map +1 -0
  455. package/dist/tests/core/managed-image.test.js +68 -0
  456. package/dist/tests/core/managed-image.test.js.map +1 -0
  457. package/dist/tests/core/mise.test.js +138 -0
  458. package/dist/tests/core/mise.test.js.map +1 -0
  459. package/dist/tests/core/parse.test.js +174 -0
  460. package/dist/tests/core/parse.test.js.map +1 -0
  461. package/dist/tests/core/path.test.js +50 -0
  462. package/dist/tests/core/path.test.js.map +1 -0
  463. package/dist/tests/core/plan.test.js +218 -0
  464. package/dist/tests/core/plan.test.js.map +1 -0
  465. package/dist/tests/core/post-move.test.js +162 -0
  466. package/dist/tests/core/post-move.test.js.map +1 -0
  467. package/dist/tests/core/pr-classify.test.js +117 -0
  468. package/dist/tests/core/pr-classify.test.js.map +1 -0
  469. package/dist/tests/core/pr-decide.test.js +298 -0
  470. package/dist/tests/core/pr-decide.test.js.map +1 -0
  471. package/dist/tests/core/pr-loop.test.js +301 -0
  472. package/dist/tests/core/pr-loop.test.js.map +1 -0
  473. package/dist/tests/core/pr-notes.test.js +165 -0
  474. package/dist/tests/core/pr-notes.test.js.map +1 -0
  475. package/dist/tests/core/predicates.test.js +154 -0
  476. package/dist/tests/core/predicates.test.js.map +1 -0
  477. package/dist/tests/core/prompt.test.js +189 -0
  478. package/dist/tests/core/prompt.test.js.map +1 -0
  479. package/dist/tests/core/protocol.test.js +195 -0
  480. package/dist/tests/core/protocol.test.js.map +1 -0
  481. package/dist/tests/core/reconcile-issue.test.js +116 -0
  482. package/dist/tests/core/reconcile-issue.test.js.map +1 -0
  483. package/dist/tests/core/render.test.js +549 -0
  484. package/dist/tests/core/render.test.js.map +1 -0
  485. package/dist/tests/core/retry.test.js +186 -0
  486. package/dist/tests/core/retry.test.js.map +1 -0
  487. package/dist/tests/core/routes.test.js +247 -0
  488. package/dist/tests/core/routes.test.js.map +1 -0
  489. package/dist/tests/core/run-fold.test.js +299 -0
  490. package/dist/tests/core/run-fold.test.js.map +1 -0
  491. package/dist/tests/core/shape.test.js +185 -0
  492. package/dist/tests/core/shape.test.js.map +1 -0
  493. package/dist/tests/core/sleep-cycle.test.js +150 -0
  494. package/dist/tests/core/sleep-cycle.test.js.map +1 -0
  495. package/dist/tests/core/slots.test.js +201 -0
  496. package/dist/tests/core/slots.test.js.map +1 -0
  497. package/dist/tests/core/state-resolve.test.js +80 -0
  498. package/dist/tests/core/state-resolve.test.js.map +1 -0
  499. package/dist/tests/core/summary.test.js +200 -0
  500. package/dist/tests/core/summary.test.js.map +1 -0
  501. package/dist/tests/core/template.test.js +116 -0
  502. package/dist/tests/core/template.test.js.map +1 -0
  503. package/dist/tests/core/tick.test.js +558 -0
  504. package/dist/tests/core/tick.test.js.map +1 -0
  505. package/dist/tests/core/token-fold.test.js +176 -0
  506. package/dist/tests/core/token-fold.test.js.map +1 -0
  507. package/dist/tests/core/turn.test.js +388 -0
  508. package/dist/tests/core/turn.test.js.map +1 -0
  509. package/dist/tests/core/url.test.js +118 -0
  510. package/dist/tests/core/url.test.js.map +1 -0
  511. package/dist/tests/core/validate.test.js +247 -0
  512. package/dist/tests/core/validate.test.js.map +1 -0
  513. package/dist/tests/core/views.test.js +252 -0
  514. package/dist/tests/core/views.test.js.map +1 -0
  515. package/dist/tests/core/vm-decide.test.js +110 -0
  516. package/dist/tests/core/vm-decide.test.js.map +1 -0
  517. package/dist/tests/core/vm-guards.test.js +153 -0
  518. package/dist/tests/core/vm-guards.test.js.map +1 -0
  519. package/dist/tests/core/vm-plan.test.js +332 -0
  520. package/dist/tests/core/vm-plan.test.js.map +1 -0
  521. package/dist/tests/core/vm-reap.test.js +196 -0
  522. package/dist/tests/core/vm-reap.test.js.map +1 -0
  523. package/dist/tests/core/workflow-parse.test.js +493 -0
  524. package/dist/tests/core/workflow-parse.test.js.map +1 -0
  525. package/dist/tests/core/workspace-decide.test.js +236 -0
  526. package/dist/tests/core/workspace-decide.test.js.map +1 -0
  527. package/dist/tests/helpers/fixtures.js +167 -0
  528. package/dist/tests/helpers/fixtures.js.map +1 -0
  529. package/dist/tests/shell/acp-substrate.test.js +101 -0
  530. package/dist/tests/shell/acp-substrate.test.js.map +1 -0
  531. package/dist/tests/shell/actions-runner-push.test.js +203 -0
  532. package/dist/tests/shell/actions-runner-push.test.js.map +1 -0
  533. package/dist/tests/shell/credential-hooks.test.js +36 -0
  534. package/dist/tests/shell/credential-hooks.test.js.map +1 -0
  535. package/dist/tests/shell/credential-registry.test.js +165 -0
  536. package/dist/tests/shell/credential-registry.test.js.map +1 -0
  537. package/dist/tests/shell/credential-substrate.test.js +179 -0
  538. package/dist/tests/shell/credential-substrate.test.js.map +1 -0
  539. package/dist/tests/shell/dockerfile-mise-pin.test.js +51 -0
  540. package/dist/tests/shell/dockerfile-mise-pin.test.js.map +1 -0
  541. package/dist/tests/shell/doctor.test.js +101 -0
  542. package/dist/tests/shell/doctor.test.js.map +1 -0
  543. package/dist/tests/shell/effect-vm-create.test.js +52 -0
  544. package/dist/tests/shell/effect-vm-create.test.js.map +1 -0
  545. package/dist/tests/shell/gh-port.test.js +63 -0
  546. package/dist/tests/shell/gh-port.test.js.map +1 -0
  547. package/dist/tests/shell/gondolin-dispatch-guard.test.js +144 -0
  548. package/dist/tests/shell/gondolin-dispatch-guard.test.js.map +1 -0
  549. package/dist/tests/shell/gondolin-dispatch-shquote.test.js +168 -0
  550. package/dist/tests/shell/gondolin-dispatch-shquote.test.js.map +1 -0
  551. package/dist/tests/shell/gondolin-image-converter.test.js +208 -0
  552. package/dist/tests/shell/gondolin-image-converter.test.js.map +1 -0
  553. package/dist/tests/shell/gondolin-image-fetch.test.js +93 -0
  554. package/dist/tests/shell/gondolin-image-fetch.test.js.map +1 -0
  555. package/dist/tests/shell/http-handler.test.js +608 -0
  556. package/dist/tests/shell/http-handler.test.js.map +1 -0
  557. package/dist/tests/shell/http-server.test.js +53 -0
  558. package/dist/tests/shell/http-server.test.js.map +1 -0
  559. package/dist/tests/shell/mcp-runtime.test.js +366 -0
  560. package/dist/tests/shell/mcp-runtime.test.js.map +1 -0
  561. package/dist/tests/shell/mise-config-asset.test.js +87 -0
  562. package/dist/tests/shell/mise-config-asset.test.js.map +1 -0
  563. package/dist/tests/shell/orchestrator-loop.test.js +583 -0
  564. package/dist/tests/shell/orchestrator-loop.test.js.map +1 -0
  565. package/dist/tests/shell/reconciler-passes.test.js +314 -0
  566. package/dist/tests/shell/reconciler-passes.test.js.map +1 -0
  567. package/dist/tests/shell/runner-loop-turn.test.js +97 -0
  568. package/dist/tests/shell/runner-loop-turn.test.js.map +1 -0
  569. package/dist/tests/shell/runner-slice.test.js +536 -0
  570. package/dist/tests/shell/runner-slice.test.js.map +1 -0
  571. package/dist/tests/shell/scaffold.test.js +65 -0
  572. package/dist/tests/shell/scaffold.test.js.map +1 -0
  573. package/dist/tests/shell/tick-config.test.js +83 -0
  574. package/dist/tests/shell/tick-config.test.js.map +1 -0
  575. package/dist/tests/shell/tracker-parse-dates.test.js +44 -0
  576. package/dist/tests/shell/tracker-parse-dates.test.js.map +1 -0
  577. package/dist/tests/shell/tracker-write-issue.test.js +154 -0
  578. package/dist/tests/shell/tracker-write-issue.test.js.map +1 -0
  579. package/dist/tests/shell/workflow-prompt-split.test.js +208 -0
  580. package/dist/tests/shell/workflow-prompt-split.test.js.map +1 -0
  581. package/dist/tests/shell/workspace-live-config.test.js +140 -0
  582. package/dist/tests/shell/workspace-live-config.test.js.map +1 -0
  583. package/package.json +31 -11
  584. package/patches/@earendil-works+gondolin+0.12.0.patch +173 -0
  585. package/prompts/Reflect.md +91 -0
  586. package/prompts/Review.md +97 -0
  587. package/prompts/Todo.md +96 -0
  588. package/prompts/_footer.md +41 -0
  589. package/prompts/_preamble.md +42 -0
  590. package/prompts-minimal/Todo.md +26 -0
  591. package/scripts/postinstall.mjs +63 -0
  592. package/scripts/vm-agent.mjs +433 -0
  593. package/WORKFLOW.md +0 -269
  594. package/WORKFLOW.template.md +0 -307
  595. package/dist/agent/acp.js +0 -304
  596. package/dist/agent/acp.js.map +0 -1
  597. package/dist/agent/adapters.js +0 -275
  598. package/dist/agent/adapters.js.map +0 -1
  599. package/dist/agent/codex.js +0 -439
  600. package/dist/agent/codex.js.map +0 -1
  601. package/dist/agent/runner.js +0 -394
  602. package/dist/agent/runner.js.map +0 -1
  603. package/dist/agent/smolvm.js +0 -174
  604. package/dist/agent/smolvm.js.map +0 -1
  605. package/dist/bin/symphony.js +0 -205
  606. package/dist/bin/symphony.js.map +0 -1
  607. package/dist/http.js +0 -1189
  608. package/dist/http.js.map +0 -1
  609. package/dist/logging.js +0 -45
  610. package/dist/logging.js.map +0 -1
  611. package/dist/mcp.js +0 -478
  612. package/dist/mcp.js.map +0 -1
  613. package/dist/orchestrator.js +0 -683
  614. package/dist/orchestrator.js.map +0 -1
  615. package/dist/prompt.js +0 -65
  616. package/dist/prompt.js.map +0 -1
  617. package/dist/trackers/local.js +0 -344
  618. package/dist/trackers/local.js.map +0 -1
  619. package/dist/trackers/types.js +0 -10
  620. package/dist/trackers/types.js.map +0 -1
  621. package/dist/types.js +0 -3
  622. package/dist/types.js.map +0 -1
  623. package/dist/workflow.js +0 -385
  624. package/dist/workflow.js.map +0 -1
  625. package/dist/workspace.js +0 -196
  626. package/dist/workspace.js.map +0 -1
  627. package/scripts/build-vm.sh +0 -67
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ // symphony-vm-agent — in-guest proxy that runs the ACP adapter and bridges its stdio
3
+ // to a WebSocket dialed back to the host orchestrator's unified HTTP server.
4
+ //
5
+ // Why a WebSocket and not stdio or raw TCP: the original proxy piped the in-VM adapter's
6
+ // stdio through the VM-exec stdio channel, which has a stdin-pump bug that wedges the
7
+ // adapter after the SDK's first notification. That was replaced by a raw-TCP bridge on a
8
+ // dedicated host port; this version collapses that bridge onto the SAME host listener
9
+ // that already serves MCP + the dashboard — ACP now rides a `/acp` WebSocket `Upgrade`
10
+ // reached through the SAME `tcp.hosts` tunnel MCP uses (the guest dials
11
+ // `ws://symphony-mcp:7001/acp`). One mapping, one synthetic host, one host port; the
12
+ // VM-exec stdio is reduced to a launcher whose stderr carries only diagnostics.
13
+ //
14
+ // The proxy authenticates with a per-dispatch bearer sent as the FIRST WebSocket
15
+ // message (the WHATWG `WebSocket` client cannot set custom upgrade headers, so a header
16
+ // bearer is not an option for the guest dialer). The auth frame is
17
+ // `{"type":"auth","token":"<token>","proto":1}`; the host validates the bearer
18
+ // constant-time and then replies `{"type":"ready","proto":1}` BEFORE any ACP byte
19
+ // flows. Only after that ready frame does the proxy spawn the adapter and start pumping
20
+ // ACP JSON-RPC bytes as WebSocket frames: inbound frame payloads are written to the
21
+ // adapter's stdin; the adapter's stdout is sent as binary frames. Each chunk is
22
+ // forwarded straight through as it arrives — NEVER buffered — preserving streaming
23
+ // (issue 135). Backpressure pauses the SOURCE stream (child.stdout or the WS receive
24
+ // side), it never accumulates a frame/turn before forwarding.
25
+ //
26
+ // Liveness: the proxy sends a WS PING every ~15 s and tracks the last pong; if no pong
27
+ // arrives for ~3 intervals the half-open socket is torn down so the dispatch fails fast
28
+ // instead of hanging until the host's prompt timeout. Both ends ping independently, so a
29
+ // long silent tool call (no ACP frames) does NOT trip the watchdog.
30
+ //
31
+ // Configuration (all required unless noted):
32
+ // SYMPHONY_ACP_URL — `ws://host:port/acp` (or `wss://…`) to dial. Under the
33
+ // Gondolin tunnel this is `ws://symphony-mcp:7001/acp`.
34
+ // SYMPHONY_ACP_TOKEN — opaque per-dispatch bearer; sent as the first WS message
35
+ // `{"type":"auth","token":"<token>","proto":1}`.
36
+ // SYMPHONY_ADAPTER_BIN — adapter executable to spawn (PATH-resolved).
37
+ // SYMPHONY_ADAPTER_ARGS — JSON array of extra argv. Optional; defaults to `[]`.
38
+ // SYMPHONY_HEARTBEAT_INTERVAL_MS — optional; WS ping interval. Defaults to 15000.
39
+ // SYMPHONY_HEARTBEAT_TIMEOUT_MS — optional; dead-peer threshold. Defaults to 45000.
40
+ // SYMPHONY_VM_AGENT_DEBUG — optional; truthy → log lifecycle to stderr.
41
+ //
42
+ // Exit code: mirrors the adapter's exit when it exits, or 1 if the connection fails
43
+ // before the adapter starts.
44
+
45
+ import { spawn } from 'node:child_process';
46
+ import { constants as osConstants } from 'node:os';
47
+ import { URL } from 'node:url';
48
+ import { Buffer } from 'node:buffer';
49
+
50
+ // Wire protocol version exchanged in the auth/ready handshake; bumped lockstep with the
51
+ // host (src/acp-ws.ts). The launcher and host now move together (PR-A injects this file
52
+ // from the host's own disk at dispatch), so a single integer suffices.
53
+ const PROTO = 1;
54
+
55
+ // Outbound backpressure (child.stdout → ws.send): when the WebSocket's send queue grows
56
+ // past the high-water mark we PAUSE child.stdout (stops the source — never accumulates a
57
+ // frame on our side) and resume once the queue drains below the low-water mark. WHATWG
58
+ // WebSocket exposes no 'drain' event, only `bufferedAmount`, so the drain is detected by a
59
+ // short poll. The marks are generous so a normal streaming turn never pauses.
60
+ const WS_HIGH_WATER_BYTES = 1 << 20; // 1 MiB queued in the WS send buffer → pause source
61
+ const WS_LOW_WATER_BYTES = 256 * 1024; // resume once it drains below this
62
+ const WS_DRAIN_POLL_MS = 10;
63
+
64
+ const requiredEnv = (name) => {
65
+ const v = process.env[name];
66
+ if (!v) {
67
+ process.stderr.write(`symphony-vm-agent: ${name} is not set\n`);
68
+ process.exit(2);
69
+ }
70
+ return v;
71
+ };
72
+
73
+ const intEnv = (name, dflt) => {
74
+ const raw = process.env[name];
75
+ if (!raw) return dflt;
76
+ const n = Number(raw);
77
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : dflt;
78
+ };
79
+
80
+ const acpUrl = requiredEnv('SYMPHONY_ACP_URL');
81
+ const acpToken = requiredEnv('SYMPHONY_ACP_TOKEN');
82
+ const adapterBin = requiredEnv('SYMPHONY_ADAPTER_BIN');
83
+ const heartbeatIntervalMs = intEnv('SYMPHONY_HEARTBEAT_INTERVAL_MS', 15_000);
84
+ const heartbeatTimeoutMs = intEnv('SYMPHONY_HEARTBEAT_TIMEOUT_MS', 45_000);
85
+
86
+ let adapterArgs = [];
87
+ const rawArgs = process.env.SYMPHONY_ADAPTER_ARGS;
88
+ if (rawArgs && rawArgs.length > 0) {
89
+ try {
90
+ const parsed = JSON.parse(rawArgs);
91
+ if (!Array.isArray(parsed) || !parsed.every((a) => typeof a === 'string')) {
92
+ throw new Error('must be a JSON array of strings');
93
+ }
94
+ adapterArgs = parsed;
95
+ } catch (err) {
96
+ process.stderr.write(`symphony-vm-agent: SYMPHONY_ADAPTER_ARGS invalid: ${err.message}\n`);
97
+ process.exit(2);
98
+ }
99
+ }
100
+
101
+ const debug = !!process.env.SYMPHONY_VM_AGENT_DEBUG;
102
+ const log = (msg) => {
103
+ if (debug) process.stderr.write(`symphony-vm-agent: ${msg}\n`);
104
+ };
105
+
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(acpUrl);
109
+ } catch (err) {
110
+ process.stderr.write(`symphony-vm-agent: SYMPHONY_ACP_URL invalid: ${err.message}\n`);
111
+ process.exit(2);
112
+ }
113
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
114
+ process.stderr.write(
115
+ `symphony-vm-agent: SYMPHONY_ACP_URL must use ws:// or wss:// scheme (got ${parsed.protocol})\n`,
116
+ );
117
+ process.exit(2);
118
+ }
119
+
120
+ if (typeof WebSocket !== 'function') {
121
+ process.stderr.write('symphony-vm-agent: global WebSocket is unavailable (need Node >= 21)\n');
122
+ process.exit(2);
123
+ }
124
+
125
+ log(`dialing ${acpUrl}`);
126
+
127
+ let child = null;
128
+ // The adapter's exit code/signal, captured on `child` close so we can flush the WebSocket
129
+ // before exiting. Null until the adapter exits.
130
+ let pendingExit = null;
131
+ let finalized = false;
132
+ let opened = false;
133
+ // The host must send `{type:'ready',proto:1}` after validating the bearer; only then do we
134
+ // spawn the adapter and start the ACP byte pump. Guards against starting the adapter before
135
+ // the host has accepted us (and lets the host gate the spawn until it is ready to relay).
136
+ let ready = false;
137
+ let bridgeStarted = false;
138
+
139
+ // --- Heartbeat (WS ping / pong watchdog) ----------------------------------------------
140
+ // Both ends ping independently. We send an application PING frame every interval and record
141
+ // the last pong arrival; if the gap exceeds the dead-peer threshold we tear the socket down
142
+ // so the dispatch fails fast (host classifies the drop as a transport_error). The watchdog
143
+ // is INDEPENDENT of ACP frames, so a long silent tool call does not trip it. Every timer is
144
+ // cleared on close (no dangling interval after teardown).
145
+ let pingTimer = null;
146
+ let lastPongAt = 0;
147
+ function startHeartbeat() {
148
+ lastPongAt = Date.now();
149
+ pingTimer = setInterval(() => {
150
+ if (ws.readyState !== WebSocket.OPEN) return;
151
+ if (Date.now() - lastPongAt > heartbeatTimeoutMs) {
152
+ log(`no pong within ${heartbeatTimeoutMs}ms; tearing down (dead host)`);
153
+ stopHeartbeat();
154
+ try {
155
+ ws.close(1001, 'heartbeat timeout');
156
+ } catch {
157
+ /* ignore */
158
+ }
159
+ // Belt-and-braces: a half-open socket may never deliver `close`; kill the adapter
160
+ // and finalize so the dispatch unwinds rather than hanging.
161
+ if (child) {
162
+ try {
163
+ child.kill('SIGTERM');
164
+ } catch {
165
+ /* ignore */
166
+ }
167
+ }
168
+ const t = setTimeout(finalize, 2_000);
169
+ if (typeof t.unref === 'function') t.unref();
170
+ return;
171
+ }
172
+ try {
173
+ ws.send(JSON.stringify({ type: 'ping', t: Date.now() }));
174
+ } catch {
175
+ /* socket closing — the close handler clears the timer */
176
+ }
177
+ }, heartbeatIntervalMs);
178
+ if (typeof pingTimer.unref === 'function') pingTimer.unref();
179
+ }
180
+ function stopHeartbeat() {
181
+ if (pingTimer) {
182
+ clearInterval(pingTimer);
183
+ pingTimer = null;
184
+ }
185
+ }
186
+
187
+ const ws = new WebSocket(acpUrl);
188
+ ws.binaryType = 'arraybuffer';
189
+
190
+ function finalize() {
191
+ if (finalized) return;
192
+ finalized = true;
193
+ stopHeartbeat();
194
+ stopOutboundDrainPoll();
195
+ if (pendingExit && pendingExit.signal) {
196
+ const sig = osConstants.signals[pendingExit.signal] ?? 0;
197
+ process.exit(128 + sig);
198
+ }
199
+ process.exit(pendingExit ? (pendingExit.code ?? 0) : 0);
200
+ }
201
+
202
+ ws.addEventListener('open', () => {
203
+ opened = true;
204
+ log('connected; sending auth message');
205
+ // The bearer is the FIRST message; the host validates it before resolving the session,
206
+ // then replies `{type:'ready',proto:1}`. We spawn the adapter only after that ready frame.
207
+ ws.send(JSON.stringify({ type: 'auth', token: acpToken, proto: PROTO }));
208
+ startHeartbeat();
209
+ });
210
+
211
+ ws.addEventListener('error', (ev) => {
212
+ const msg = ev?.message || (ev?.error && ev.error.message) || 'websocket error';
213
+ if (!opened) {
214
+ // Couldn't reach the host at all → exit hard so symphony's connect-timeout error path
215
+ // fires (mirrors the raw bridge's pre-connect error handling).
216
+ process.stderr.write(`symphony-vm-agent: websocket error before open: ${msg}\n`);
217
+ process.exit(1);
218
+ }
219
+ log(`websocket error after open: ${msg}`);
220
+ if (child) {
221
+ try {
222
+ child.kill('SIGTERM');
223
+ } catch {
224
+ /* ignore */
225
+ }
226
+ }
227
+ });
228
+
229
+ ws.addEventListener('close', (ev) => {
230
+ log(`websocket closed code=${ev?.code}`);
231
+ stopHeartbeat();
232
+ stopOutboundDrainPoll();
233
+ // Adapter already exited: the close completes our flush → exit with its code.
234
+ if (pendingExit) return finalize();
235
+ // Host closed first (cancel / attempt teardown): end the adapter's stdin so it unwinds
236
+ // (parity with the raw socket half-close), and kill it as a belt-and-braces.
237
+ if (child) {
238
+ try {
239
+ child.stdin.end();
240
+ } catch {
241
+ /* ignore */
242
+ }
243
+ try {
244
+ child.kill('SIGTERM');
245
+ } catch {
246
+ /* ignore */
247
+ }
248
+ }
249
+ });
250
+
251
+ // The control + handshake frame handler. Installed before `startBridge()` so the ready
252
+ // frame (and pongs) are observed even before the adapter exists. `startBridge()` adds a
253
+ // SECOND `message` listener for ACP bytes once ready; this one only consumes JSON control
254
+ // frames (auth-ready handshake + pong) and ignores everything else, so it never competes
255
+ // with the ACP byte pump for opaque adapter bytes.
256
+ ws.addEventListener('message', (ev) => {
257
+ const ctrl = parseControlFrame(ev.data);
258
+ if (!ctrl) return; // opaque ACP bytes — handled by startBridge's pump (once ready)
259
+ if (ctrl.type === 'ready') {
260
+ if (ready) return;
261
+ ready = true;
262
+ log('host ready; spawning adapter');
263
+ startBridge();
264
+ return;
265
+ }
266
+ if (ctrl.type === 'pong' || ctrl.type === 'ping') {
267
+ // Any control traffic proves the peer is alive; refresh the watchdog. We reply to a
268
+ // host ping with a pong so the host's own watchdog stays satisfied symmetrically.
269
+ lastPongAt = Date.now();
270
+ if (ctrl.type === 'ping') {
271
+ try {
272
+ ws.send(JSON.stringify({ type: 'pong', t: Date.now() }));
273
+ } catch {
274
+ /* socket closing */
275
+ }
276
+ }
277
+ }
278
+ });
279
+
280
+ // Parse a host→guest control frame. Returns `{type}` for our small JSON control vocabulary
281
+ // (`ready`/`ping`/`pong`), or null when the payload is opaque ACP bytes (binary frame, or
282
+ // text that is not one of our control messages). Kept deliberately strict so adapter JSON
283
+ // that happens to be text can never be mistaken for a control frame: ACP bytes are sent by
284
+ // the host as BINARY frames (ArrayBuffer here), so a string payload that parses to a known
285
+ // control `type` is the only thing treated as control.
286
+ function parseControlFrame(data) {
287
+ if (typeof data !== 'string') return null; // binary → ACP bytes
288
+ let msg;
289
+ try {
290
+ msg = JSON.parse(data);
291
+ } catch {
292
+ return null;
293
+ }
294
+ if (msg && (msg.type === 'ready' || msg.type === 'ping' || msg.type === 'pong')) {
295
+ return { type: msg.type };
296
+ }
297
+ return null;
298
+ }
299
+
300
+ // --- Outbound backpressure poll (child.stdout → ws.send) -------------------------------
301
+ let outboundDrainTimer = null;
302
+ function stopOutboundDrainPoll() {
303
+ if (outboundDrainTimer) {
304
+ clearInterval(outboundDrainTimer);
305
+ outboundDrainTimer = null;
306
+ }
307
+ }
308
+
309
+ function startBridge() {
310
+ if (bridgeStarted) return;
311
+ bridgeStarted = true;
312
+ log(`spawn ${adapterBin} ${JSON.stringify(adapterArgs)}`);
313
+ child = spawn(adapterBin, adapterArgs, {
314
+ // Adapter stdio: kernel pipes we fully own. Adapter stderr is inherited so any
315
+ // crashes / warnings show up on this proxy's stderr → VM-exec stderr → host
316
+ // orchestrator stderr capture. ACP frames flow ONLY over the WebSocket.
317
+ stdio: ['pipe', 'pipe', 'inherit'],
318
+ });
319
+
320
+ // Inbound (host → adapter): each WS binary frame's payload is written to the adapter's
321
+ // stdin AS IT ARRIVES — never buffered. We deliberately do NOT track `write()`'s backpressure
322
+ // return here: the WHATWG WebSocket receive side has no pause/pull API, so there is nothing to
323
+ // throttle locally. The real backpressure is end-to-end instead — a slow adapter fills its
324
+ // stdin pipe, then the guest's TCP receive window, which pauses the HOST's socket reads
325
+ // (src/acp-ws.ts) where the source actually lives. We never accumulate a frame here; Node's
326
+ // own bounded stdin buffer absorbs the in-flight bytes during the brief drain window.
327
+ ws.addEventListener('message', (ev) => {
328
+ const data = ev.data;
329
+ if (typeof data === 'string') {
330
+ // A string frame post-ready is one of our control messages (handled by the control
331
+ // listener above), never ACP bytes. Skip it here so control traffic is not written
332
+ // into the adapter's stdin.
333
+ if (parseControlFrame(data)) return;
334
+ }
335
+ const buf = typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data);
336
+ try {
337
+ // A `false` return (kernel pipe momentarily full) is intentionally not acted on — see
338
+ // above; Node buffers the write and the host-side socket pause is what throttles the source.
339
+ child.stdin.write(buf);
340
+ } catch {
341
+ /* adapter stdin closed while we were writing — see exit handler */
342
+ }
343
+ });
344
+
345
+ // Outbound (adapter → host): adapter stdout is sent as binary frames. Each chunk is
346
+ // forwarded as it arrives — never buffered (issue 135). Backpressure GATES on the WS send
347
+ // queue: when `ws.bufferedAmount` exceeds the high-water mark we PAUSE child.stdout (the
348
+ // SOURCE) and resume once the queue drains below the low-water mark. Pausing the source —
349
+ // not accumulating — is what keeps this from re-introducing the issue-135 full-buffering
350
+ // stall while still bounding memory if the host socket stalls.
351
+ const maybeResumeStdout = () => {
352
+ if (outboundDrainTimer && ws.bufferedAmount <= WS_LOW_WATER_BYTES) {
353
+ stopOutboundDrainPoll();
354
+ if (!child.stdout.destroyed) child.stdout.resume();
355
+ }
356
+ };
357
+ child.stdout.on('data', (chunk) => {
358
+ if (ws.readyState !== WebSocket.OPEN) return;
359
+ try {
360
+ // Send a tight ArrayBuffer copy of exactly the chunk's bytes as one binary frame.
361
+ // `chunk` is a Buffer that may be a view into Node's shared allocation pool (non-zero
362
+ // byteOffset); slicing its backing ArrayBuffer yields a fresh buffer of exactly the
363
+ // chunk's data, so no surrounding pool bytes can ever leak into the frame regardless
364
+ // of how the WebSocket impl treats ArrayBufferView offsets.
365
+ ws.send(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
366
+ } catch {
367
+ /* host socket closing while we were writing — see exit handlers */
368
+ }
369
+ // Gate AFTER forwarding this chunk: the chunk is already on the wire, so we never hold
370
+ // it back; we only pause the NEXT read when the send queue is over the high-water mark.
371
+ if (ws.bufferedAmount > WS_HIGH_WATER_BYTES && !outboundDrainTimer) {
372
+ child.stdout.pause();
373
+ // WHATWG WebSocket has no 'drain'; poll `bufferedAmount` until it falls to low-water,
374
+ // then resume. The timer is cleared on resume AND on close/finalize (no dangling poll).
375
+ outboundDrainTimer = setInterval(maybeResumeStdout, WS_DRAIN_POLL_MS);
376
+ if (typeof outboundDrainTimer.unref === 'function') outboundDrainTimer.unref();
377
+ }
378
+ });
379
+ // Half-close: when the adapter closes stdout, close the WebSocket write side so the host
380
+ // sees end-of-stream.
381
+ child.stdout.on('end', () => {
382
+ log('adapter closed stdout; closing websocket');
383
+ try {
384
+ ws.close(1000, 'adapter stdout end');
385
+ } catch {
386
+ /* ignore */
387
+ }
388
+ });
389
+ child.stdin.on('error', (err) => {
390
+ if (err.code !== 'EPIPE') log(`adapter stdin error: ${err.message}`);
391
+ });
392
+ child.on('error', (err) => {
393
+ process.stderr.write(`symphony-vm-agent: failed to spawn adapter: ${err.message}\n`);
394
+ try {
395
+ ws.close(1011, 'spawn failed');
396
+ } catch {
397
+ /* ignore */
398
+ }
399
+ process.exit(127);
400
+ });
401
+ // Use `close` not `exit`: `exit` fires when the adapter process terminates but its stdio
402
+ // pipes may still have buffered bytes our `data` handlers haven't drained yet. `close`
403
+ // fires only after all stdio streams have closed AND drained, so by then every byte the
404
+ // adapter wrote has been forwarded to ws.send() above. We then close the WebSocket and
405
+ // let its `close` event (or the grace timer) finalize the exit so the forwarded frames
406
+ // flush to the host before we terminate.
407
+ child.on('close', (code, signal) => {
408
+ log(`adapter close code=${code} signal=${signal}`);
409
+ pendingExit = { code, signal };
410
+ stopHeartbeat();
411
+ stopOutboundDrainPoll();
412
+ try {
413
+ ws.close(1000, 'adapter exit');
414
+ } catch {
415
+ finalize();
416
+ return;
417
+ }
418
+ // Safety net: if the close handshake stalls, exit anyway rather than hang the dispatch.
419
+ const t = setTimeout(finalize, 2_000);
420
+ if (typeof t.unref === 'function') t.unref();
421
+ });
422
+ // Forward host-side termination signals to the adapter.
423
+ for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
424
+ process.on(sig, () => {
425
+ log(`received ${sig}; forwarding to adapter`);
426
+ try {
427
+ child.kill(sig);
428
+ } catch {
429
+ /* ignore */
430
+ }
431
+ });
432
+ }
433
+ }