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,38 +1,65 @@
1
1
  #!/usr/bin/env node
2
2
  // symphony-vm-agent — in-guest proxy that runs the ACP adapter and bridges its stdio
3
- // to a TCP socket dialed back to the host orchestrator.
3
+ // to a WebSocket dialed back to the host orchestrator's unified HTTP server.
4
4
  //
5
- // Why TCP and not stdio: the previous version of this proxy bridged the in-VM adapter's
6
- // stdio to the VM-exec stdio channel directly. That channel has a stdin-pump bug
7
- // that wedges the adapter after the SDK's first `available_commands_update` notification
8
- // (the in-guest reader never wakes for subsequent kernel events unless host stdin keeps
9
- // writing). We worked around it for a while with a 1.5 s `\n` keepalive on the host
10
- // side, but the proper fix is to stop piping ACP through the VM-exec stdio at all. This proxy
11
- // dials back to a TCP listener on the host (`SYMPHONY_ACP_URL`), authenticates with a
12
- // per-dispatch bearer token (`SYMPHONY_ACP_TOKEN`), and from then on talks ACP over a
13
- // plain socket the host owns. the VM exec is reduced to a launcher: its stdio just
14
- // carries this proxy's own diagnostic stderr and the adapter's stderr (`inherit`'d).
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.
15
13
  //
16
- // The same proxy works under any sandbox tech that can launch a process with env vars
17
- // and reach the host loopback that's the portability win.
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.
18
30
  //
19
31
  // Configuration (all required unless noted):
20
- // SYMPHONY_ACP_URL — `tcp://host:port` to dial. `127.0.0.1:8788` under the Gondolin bridge mapping
21
- // since the guest loopback is rewritten to the host
22
- // loopback.
23
- // SYMPHONY_ACP_TOKEN — opaque per-dispatch bearer; sent as `Bearer <token>\n`
24
- // first line.
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}`.
25
36
  // SYMPHONY_ADAPTER_BIN — adapter executable to spawn (PATH-resolved).
26
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.
27
40
  // SYMPHONY_VM_AGENT_DEBUG — optional; truthy → log lifecycle to stderr.
28
41
  //
29
42
  // Exit code: mirrors the adapter's exit when it exits, or 1 if the connection fails
30
43
  // before the adapter starts.
31
44
 
32
- import { connect as netConnect } from 'node:net';
33
45
  import { spawn } from 'node:child_process';
34
46
  import { constants as osConstants } from 'node:os';
35
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;
36
63
 
37
64
  const requiredEnv = (name) => {
38
65
  const v = process.env[name];
@@ -43,9 +70,18 @@ const requiredEnv = (name) => {
43
70
  return v;
44
71
  };
45
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
+
46
80
  const acpUrl = requiredEnv('SYMPHONY_ACP_URL');
47
81
  const acpToken = requiredEnv('SYMPHONY_ACP_TOKEN');
48
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);
49
85
 
50
86
  let adapterArgs = [];
51
87
  const rawArgs = process.env.SYMPHONY_ADAPTER_ARGS;
@@ -74,83 +110,278 @@ try {
74
110
  process.stderr.write(`symphony-vm-agent: SYMPHONY_ACP_URL invalid: ${err.message}\n`);
75
111
  process.exit(2);
76
112
  }
77
- if (parsed.protocol !== 'tcp:') {
113
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
78
114
  process.stderr.write(
79
- `symphony-vm-agent: SYMPHONY_ACP_URL must use tcp:// scheme (got ${parsed.protocol})\n`,
115
+ `symphony-vm-agent: SYMPHONY_ACP_URL must use ws:// or wss:// scheme (got ${parsed.protocol})\n`,
80
116
  );
81
117
  process.exit(2);
82
118
  }
83
- const acpHost = parsed.hostname;
84
- const acpPort = parseInt(parsed.port, 10);
85
- if (!Number.isFinite(acpPort) || acpPort <= 0) {
86
- process.stderr.write(`symphony-vm-agent: SYMPHONY_ACP_URL has no port\n`);
119
+
120
+ if (typeof WebSocket !== 'function') {
121
+ process.stderr.write('symphony-vm-agent: global WebSocket is unavailable (need Node >= 21)\n');
87
122
  process.exit(2);
88
123
  }
89
124
 
90
- log(`dialing ${acpHost}:${acpPort}`);
125
+ log(`dialing ${acpUrl}`);
91
126
 
92
- const socket = netConnect({ host: acpHost, port: acpPort, allowHalfOpen: false });
93
- socket.setNoDelay(true);
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;
94
138
 
95
- // Pre-connect error handler: a TCP-level failure here means we couldn't reach the host
96
- // bridge at all. Exit hard so symphony's host-side bridge-connect-timeout fires its own
97
- // error path. We REMOVE this listener once `connect` fires; otherwise it would also kick
98
- // in for post-auth errors and bypass startBridge()'s graceful adapter teardown.
99
- const onPreConnectError = (err) => {
100
- process.stderr.write(`symphony-vm-agent: socket error: ${err.message}\n`);
101
- process.exit(1);
102
- };
103
- socket.once('error', onPreConnectError);
104
-
105
- socket.once('connect', () => {
106
- socket.off('error', onPreConnectError);
107
- log('connected; sending bearer line');
108
- // The bearer line MUST end with a bare \n; the host expects exactly that delimiter.
109
- socket.write(`Bearer ${acpToken}\n`);
110
- startBridge();
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
+ }
111
278
  });
112
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
+
113
309
  function startBridge() {
310
+ if (bridgeStarted) return;
311
+ bridgeStarted = true;
114
312
  log(`spawn ${adapterBin} ${JSON.stringify(adapterArgs)}`);
115
- const child = spawn(adapterBin, adapterArgs, {
313
+ child = spawn(adapterBin, adapterArgs, {
116
314
  // Adapter stdio: kernel pipes we fully own. Adapter stderr is inherited so any
117
315
  // crashes / warnings show up on this proxy's stderr → VM-exec stderr → host
118
- // orchestrator stderr capture. ACP frames flow ONLY over the TCP socket.
316
+ // orchestrator stderr capture. ACP frames flow ONLY over the WebSocket.
119
317
  stdio: ['pipe', 'pipe', 'inherit'],
120
318
  });
121
- // Bridge socket ↔ adapter stdio with explicit `data` handlers + `write()` (NOT
122
- // stream.pipe()): we observed during the bisect that pipe() can interact badly with
123
- // certain transport quirks, so we forward bytes synchronously and let the kernel pipe
124
- // buffer absorb any short-term mismatch. With a clean TCP socket this is moot but
125
- // costs nothing and keeps the pattern uniform.
126
- socket.on('data', (chunk) => {
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);
127
336
  try {
128
- child.stdin.write(chunk);
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);
129
340
  } catch {
130
341
  /* adapter stdin closed while we were writing — see exit handler */
131
342
  }
132
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
+ };
133
357
  child.stdout.on('data', (chunk) => {
358
+ if (ws.readyState !== WebSocket.OPEN) return;
134
359
  try {
135
- socket.write(chunk);
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));
136
366
  } catch {
137
- /* host socket closed while we were writing — see exit handlers */
367
+ /* host socket closing while we were writing — see exit handlers */
138
368
  }
139
- });
140
- // Half-closes: when the host closes its write side, the adapter should see stdin EOF;
141
- // when the adapter closes stdout, the host should see socket-half-close.
142
- socket.on('end', () => {
143
- log('host closed socket; ending adapter stdin');
144
- try {
145
- child.stdin.end();
146
- } catch {
147
- /* ignore */
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();
148
377
  }
149
378
  });
379
+ // Half-close: when the adapter closes stdout, close the WebSocket write side so the host
380
+ // sees end-of-stream.
150
381
  child.stdout.on('end', () => {
151
- log('adapter closed stdout; ending socket write side');
382
+ log('adapter closed stdout; closing websocket');
152
383
  try {
153
- socket.end();
384
+ ws.close(1000, 'adapter stdout end');
154
385
  } catch {
155
386
  /* ignore */
156
387
  }
@@ -158,44 +389,35 @@ function startBridge() {
158
389
  child.stdin.on('error', (err) => {
159
390
  if (err.code !== 'EPIPE') log(`adapter stdin error: ${err.message}`);
160
391
  });
161
- socket.on('error', (err) => {
162
- log(`socket error after auth: ${err.message}`);
163
- try {
164
- child.kill('SIGTERM');
165
- } catch {
166
- /* ignore */
167
- }
168
- });
169
392
  child.on('error', (err) => {
170
393
  process.stderr.write(`symphony-vm-agent: failed to spawn adapter: ${err.message}\n`);
171
394
  try {
172
- socket.end();
395
+ ws.close(1011, 'spawn failed');
173
396
  } catch {
174
397
  /* ignore */
175
398
  }
176
399
  process.exit(127);
177
400
  });
178
- // Use `close` not `exit`: `exit` fires when the adapter process terminates but its
179
- // stdio pipes may still have buffered bytes our `data` handlers haven't drained yet.
180
- // `close` fires only after all stdio streams have been closed AND drained, so by then
181
- // every byte the adapter wrote has been forwarded to our socket.write() above. We
182
- // still need to wait for the socket itself to flush those forwarded bytes to the
183
- // kernel before exiting, hence the `socket.end(callback)` pattern: end() signals
184
- // shutdown and the callback fires after the write buffer has drained.
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.
185
407
  child.on('close', (code, signal) => {
186
408
  log(`adapter close code=${code} signal=${signal}`);
187
- const finalize = () => {
188
- if (signal) {
189
- const sig = osConstants.signals[signal] ?? 0;
190
- process.exit(128 + sig);
191
- }
192
- process.exit(code ?? 0);
193
- };
409
+ pendingExit = { code, signal };
410
+ stopHeartbeat();
411
+ stopOutboundDrainPoll();
194
412
  try {
195
- socket.end(finalize);
413
+ ws.close(1000, 'adapter exit');
196
414
  } catch {
197
415
  finalize();
416
+ return;
198
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();
199
421
  });
200
422
  // Forward host-side termination signals to the adapter.
201
423
  for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP']) {