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
@@ -0,0 +1,303 @@
1
+ // Functional core — `symphony doctor` preflight + startup banner + HTTP-port resolution.
2
+ //
3
+ // The orchestrator's startup path validates prerequisites FATALLY (`bailStartup`
4
+ // exits on the first failure), so a first-time adopter fixes one cliff, re-runs,
5
+ // hits the next, and so on. The doctor runs every probe NON-FATALLY and prints a
6
+ // PASS/FAIL line per check with a one-line fix on failure, so the whole adoption
7
+ // surface is legible in one pass.
8
+ //
9
+ // These helpers are PURE: each takes a raw probe OBSERVATION (gathered by the
10
+ // shell — fs reads, port binds, image resolution) and returns a typed
11
+ // {@link DoctorCheck} row, or renders the aligned report / startup banner. No IO,
12
+ // no clock, no randomness — the only imports are type-only, from src/types.
13
+ // ─── version helpers ─────────────────────────────────────────────────────────
14
+ /**
15
+ * Extract a `[major, minor, patch]` tuple from a version-ish string. Reads the
16
+ * first up-to-three integer runs, defaulting missing components to 0, so it
17
+ * handles both a running version (`v23.6.0`) and an `engines` floor spec
18
+ * (`>=23.6.0`).
19
+ */
20
+ function parseVersionTuple(s) {
21
+ const nums = (s.match(/\d+/g) ?? []).slice(0, 3).map((n) => parseInt(n, 10));
22
+ return [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0];
23
+ }
24
+ function compareVersionTuples(a, b) {
25
+ for (let i = 0; i < 3; i++) {
26
+ if (a[i] !== b[i])
27
+ return a[i] < b[i] ? -1 : 1;
28
+ }
29
+ return 0;
30
+ }
31
+ /**
32
+ * node >= the package `engines.node` floor. `current` is `process.version`
33
+ * (`v23.6.0`); `floor` is the raw `engines.node` spec (`>=23.6.0`) or null when
34
+ * it can't be read (then the check passes, reporting the running version only —
35
+ * the doctor should never fail on its own inability to read package.json).
36
+ */
37
+ export function checkNodeVersion(current, floor) {
38
+ if (floor === null) {
39
+ return { key: 'node', pass: true, detail: `${current} (no engines floor declared)`, fix: '' };
40
+ }
41
+ const min = parseVersionTuple(floor);
42
+ const floorText = min.join('.');
43
+ const pass = compareVersionTuples(parseVersionTuple(current), min) >= 0;
44
+ return {
45
+ key: 'node',
46
+ pass,
47
+ detail: `${current} (engines floor >=${floorText})`,
48
+ fix: `install Node >=${floorText} (e.g. nvm install ${min[0]}) and re-run`,
49
+ };
50
+ }
51
+ // ─── workflow ────────────────────────────────────────────────────────────────
52
+ /**
53
+ * WORKFLOW.yaml parses and declares the required state roles (active / terminal /
54
+ * holding). A parse failure is a single FAIL; a parsed-but-incomplete state map
55
+ * names exactly which roles are missing.
56
+ */
57
+ export function checkWorkflow(obs) {
58
+ if (obs.parseError !== null) {
59
+ return {
60
+ key: 'workflow',
61
+ pass: false,
62
+ detail: `does not parse: ${obs.parseError}`,
63
+ fix: 'fix the YAML / states block in WORKFLOW.yaml',
64
+ };
65
+ }
66
+ const missing = [];
67
+ if (!obs.hasActive)
68
+ missing.push('active');
69
+ if (!obs.hasTerminal)
70
+ missing.push('terminal');
71
+ if (!obs.hasHolding)
72
+ missing.push('holding');
73
+ if (missing.length > 0) {
74
+ return {
75
+ key: 'workflow',
76
+ pass: false,
77
+ detail: `missing required state role(s): ${missing.join(', ')}`,
78
+ fix: `declare at least one state with role: ${missing.join(', role: ')}`,
79
+ };
80
+ }
81
+ return {
82
+ key: 'workflow',
83
+ pass: true,
84
+ detail: `${obs.stateNames.join(', ')} (active/terminal/holding present)`,
85
+ fix: '',
86
+ };
87
+ }
88
+ // ─── gondolin image ──────────────────────────────────────────────────────────
89
+ /**
90
+ * A digest-shaped (content-addressed) image id printed by `npm run build:image`:
91
+ * a `sha256:`-prefixed or bare long hex run, or a UUID-shaped build id (the form
92
+ * Gondolin actually prints, e.g. `cb875342-03ef-56e0-9306-dde8628aa17d`).
93
+ */
94
+ function looksLikeDigest(image) {
95
+ return (/^(sha256:)?[0-9a-f]{12,}$/i.test(image) ||
96
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(image));
97
+ }
98
+ /** A path-shaped image selector (an asset directory rather than a registry ref). */
99
+ function looksLikePath(image) {
100
+ return image.includes('/') || image.startsWith('.') || image.startsWith('~');
101
+ }
102
+ /**
103
+ * `gondolin.image` is pinned (a digest / build id, an asset path, or an explicit
104
+ * `name:tag` — not a bare floating name Gondolin silently defaults to `:latest`)
105
+ * **and** resolvable. Pinned-ness is a pure property of the string; resolvability
106
+ * is probed against Gondolin's own resolver in the shell, because a pinned ref
107
+ * whose built image is not importable from the local store passes the shape test
108
+ * but still fails at VM bring-up. Both must hold for a PASS.
109
+ */
110
+ export function checkGondolinImage(obs) {
111
+ const key = 'gondolin.image';
112
+ const fix = 'set gondolin.image (a build id, name:tag, or asset dir from `npm run build:image`) or gondolin.oci_image (an OCI ref auto-converted on first reconcile) in WORKFLOW.yaml';
113
+ // Mirror runtime precedence (`resolveImagePath`, src/core/runner/vm-plan.ts): the
114
+ // pre-converted `gondolin.image` escape hatch ALWAYS wins when set, so the doctor
115
+ // validates IT — and fails on an unpinned/unresolvable selector REGARDLESS of
116
+ // oci_image, because dispatch would boot that selector and fail. The soft
117
+ // oci_image auto-convert path applies only when `image` is null/empty.
118
+ if (obs.image && obs.image.length > 0) {
119
+ const image = obs.image;
120
+ // A bare floating name (no tag, not a build id, not a path) is unpinned — flag it
121
+ // before resolution, since Gondolin would silently resolve it to `:latest`.
122
+ if (!looksLikePath(image) && !looksLikeDigest(image) && !image.includes(':')) {
123
+ return {
124
+ key,
125
+ pass: false,
126
+ detail: `unpinned ref "${image}" (no tag or digest — floats)`,
127
+ fix: 'pin a name:tag or digest (`npm run build:image` prints one)',
128
+ };
129
+ }
130
+ // Pinned. Resolvability decides: probe Gondolin's resolver rather than trust the
131
+ // string shape, so a `does-not-exist:tag` that boots-fails is caught here.
132
+ const res = obs.resolution;
133
+ if (res === null || !res.resolved) {
134
+ return {
135
+ key,
136
+ pass: false,
137
+ detail: `pinned but unresolvable: ${res?.detail ?? 'not probed'}`,
138
+ fix: 'run `npm run build:image` (or import the asset dir) so the pinned image resolves locally',
139
+ };
140
+ }
141
+ return { key, pass: true, detail: `${image} (resolved: ${res.source ?? 'ok'})`, fix: '' };
142
+ }
143
+ // No escape-hatch `image` set — auto-convert path (issue 206): `gondolin.oci_image`.
144
+ // A not-yet-converted asset is a SOFT/info state (the reconcile loop builds it on
145
+ // first poll), NOT a hard failure — except when it can never be built (pullPolicy:
146
+ // never AND the OCI ref isn't in the local container store, so there's nothing to
147
+ // convert).
148
+ if (obs.oci) {
149
+ const { ref, converted, locallyResolvable, pullPolicy } = obs.oci;
150
+ if (converted) {
151
+ return { key, pass: true, detail: `oci_image ${ref} (converted asset cached)`, fix: '' };
152
+ }
153
+ if (pullPolicy === 'never' && !locallyResolvable) {
154
+ return {
155
+ key,
156
+ pass: false,
157
+ detail: `oci_image ${ref} not converted and not in the local container store (oci_pull_policy: never)`,
158
+ fix: 'pull the OCI image locally, or set gondolin.oci_pull_policy to if-not-present/always so reconcile can pull it',
159
+ };
160
+ }
161
+ return {
162
+ key,
163
+ pass: true,
164
+ detail: `oci_image ${ref} — not yet converted; will build on first reconcile (one-time)`,
165
+ fix: '',
166
+ };
167
+ }
168
+ // Managed prebuilt-image source (issue 224): neither `image` nor `oci_image` set, but
169
+ // `gondolin.managed_image` is active. PASS when the host arch has a published asset
170
+ // (cached, or fetchable on first reconcile); FAIL on an unsupported arch (no asset).
171
+ if (obs.managed) {
172
+ const { supported, arch, ref, cached } = obs.managed;
173
+ if (!supported) {
174
+ return {
175
+ key,
176
+ pass: false,
177
+ detail: `managed image source has no published asset for this host architecture${arch ? ` (${arch})` : ''}`,
178
+ fix: 'managed assets are published for x86_64 and arm64 only; on another arch set gondolin.image (a pre-built local asset) or gondolin.oci_image',
179
+ };
180
+ }
181
+ if (cached) {
182
+ return { key, pass: true, detail: `managed image ${ref} (fetched asset cached)`, fix: '' };
183
+ }
184
+ return {
185
+ key,
186
+ pass: true,
187
+ detail: `managed image ${ref} — not yet fetched; will download on first reconcile (one-time)`,
188
+ fix: '',
189
+ };
190
+ }
191
+ // Neither `image`, `oci_image`, nor a managed source configured.
192
+ return { key, pass: false, detail: 'not set', fix };
193
+ }
194
+ // ─── tracker root ────────────────────────────────────────────────────────────
195
+ /** `tracker.root` exists, is a directory, and is writable by symphony. */
196
+ export function checkTrackerRoot(obs) {
197
+ const key = 'tracker.root';
198
+ if (!obs.root) {
199
+ return { key, pass: false, detail: 'not set', fix: 'set tracker.root in WORKFLOW.yaml' };
200
+ }
201
+ if (!obs.exists) {
202
+ return { key, pass: false, detail: `not found: ${obs.root}`, fix: `mkdir -p ${obs.root}` };
203
+ }
204
+ if (!obs.isDirectory) {
205
+ return {
206
+ key,
207
+ pass: false,
208
+ detail: `not a directory: ${obs.root}`,
209
+ fix: `remove the file at ${obs.root} and create a directory there`,
210
+ };
211
+ }
212
+ if (!obs.writable) {
213
+ return {
214
+ key,
215
+ pass: false,
216
+ detail: `not writable: ${obs.root}`,
217
+ fix: `grant write permission on ${obs.root} (chmod/chown so symphony can write issue files)`,
218
+ };
219
+ }
220
+ return { key, pass: true, detail: `${obs.root} (writable)`, fix: '' };
221
+ }
222
+ // ─── dashboard port ──────────────────────────────────────────────────────────
223
+ /** The dashboard port is free / bindable. An ephemeral (0) port is always free. */
224
+ export function checkDashboardPort(obs) {
225
+ const key = 'dashboard.port';
226
+ if (obs.port === 0) {
227
+ return { key, pass: true, detail: 'ephemeral (kernel-assigned — always free)', fix: '' };
228
+ }
229
+ if (obs.bindable) {
230
+ return { key, pass: true, detail: `${obs.port} (bindable)`, fix: '' };
231
+ }
232
+ return {
233
+ key,
234
+ pass: false,
235
+ detail: `${obs.port} not bindable${obs.reason ? ` (${obs.reason})` : ''}`,
236
+ fix: 'free the port or set a different server.port / --port',
237
+ };
238
+ }
239
+ // ─── report ──────────────────────────────────────────────────────────────────
240
+ /** True iff every doctor check passed. The doctor exits 0 iff this is true. */
241
+ export function doctorReportPassed(checks) {
242
+ return checks.every((c) => c.pass);
243
+ }
244
+ /**
245
+ * Render the doctor report as aligned, color-free columns matching symphony's
246
+ * quiet-workshop banner voice: a PASS/FAIL status, the padded key, and the
247
+ * detail, with an indented `fix:` line under each failure.
248
+ */
249
+ export function formatDoctorReport(checks) {
250
+ const keyWidth = checks.reduce((w, c) => Math.max(w, c.key.length), 0);
251
+ const lines = ['symphony doctor', ''];
252
+ for (const c of checks) {
253
+ lines.push(`${c.pass ? 'PASS' : 'FAIL'} ${c.key.padEnd(keyWidth)} ${c.detail}`);
254
+ if (!c.pass && c.fix)
255
+ lines.push(` fix: ${c.fix}`);
256
+ }
257
+ const pass = checks.filter((c) => c.pass).length;
258
+ lines.push('', `${checks.length} checks · ${pass} pass · ${checks.length - pass} fail`);
259
+ return lines.join('\n') + '\n';
260
+ }
261
+ // ─── http port + startup banner ──────────────────────────────────────────────
262
+ /**
263
+ * Resolve the HTTP port: CLI `--port` wins, then `server.port`, otherwise 0
264
+ * (ephemeral). Returning 0 when neither is set lets the kernel pick a free port,
265
+ * so symphony "just works" with no config and two concurrent instances never
266
+ * collide; an explicit `--port`/`server.port` still pins a fixed port.
267
+ */
268
+ export function resolveHttpPort(cliPort, configPort) {
269
+ return cliPort ?? configPort ?? 0;
270
+ }
271
+ /**
272
+ * Map a wildcard bind address to a clickable loopback host for the dashboard URL
273
+ * line — `0.0.0.0`/`::` aren't dialable, `localhost` is.
274
+ */
275
+ function displayHost(host) {
276
+ return host === '0.0.0.0' || host === '::' ? 'localhost' : host;
277
+ }
278
+ /**
279
+ * Build the human-facing startup summary. Defaulting `server.port` to an
280
+ * ephemeral port means the operator can't know the bound port from config alone,
281
+ * so the banner reports the *actually-bound* HTTP port (`boundPort`) and the
282
+ * dashboard URL. Kept pure so it is unit-testable.
283
+ *
284
+ * Names where the operator's three orientation points live: the dashboard URL,
285
+ * the resolved log file (the env override path when SYMPHONY_LOG_FILE is set,
286
+ * else <logs.root>/symphony.log; `(disabled)` when file logging is off), and the
287
+ * tracker + workspace roots — so nobody has to guess where state lives once the
288
+ * roots move out of the repo. Kept pure so the content is unit-testable without IO.
289
+ */
290
+ export function formatStartupBanner(opts) {
291
+ const lines = [
292
+ `symphony ${opts.version}`,
293
+ ` workflow ${opts.workflowPath}`,
294
+ ` tracker root ${opts.trackerRoot ?? '<unset>'}`,
295
+ ` workspace ${opts.workspaceRoot ?? '<unset>'}`,
296
+ ` dashboard http://${displayHost(opts.host)}:${opts.boundPort}/`,
297
+ ` log file ${opts.logFile ?? '(disabled)'}`,
298
+ ` adapter ${opts.adapter}`,
299
+ ` image ${opts.image ?? '<unset>'}`,
300
+ ];
301
+ return lines.join('\n') + '\n';
302
+ }
303
+ //# sourceMappingURL=checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.js","sourceRoot":"","sources":["../../../../src/core/doctor/checks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ // FCIS rewrite — pure classifiers for git push results + gh PR-create output.
2
+ //
3
+ // These are the two pure DECISIONS the IO effect-interpreter used to run inline:
4
+ // • classifyPushResult — whether a `git push` exited clean and, on failure,
5
+ // whether the failure is a non-fast-forward rejection (the keystone of the
6
+ // push_branch NFF → force-with-lease recovery; actions-runner reads the flag).
7
+ // • parsePrNumberFromUrl — extract the PR number + canonical URL from the
8
+ // `gh pr create` stdout (the trailing line is the created PR URL).
9
+ //
10
+ // Both were stranded in `shell/effect-interpreter-families.ts` (the regex twin of
11
+ // `runlog/summary.ts:PR_URL_RE`). Pulling them into core means the push-recovery
12
+ // and PR-number decisions are unit-tested once and can't drift from the regexes
13
+ // the rest of core already uses.
14
+ //
15
+ // HARD CONSTRAINTS (gate-enforced): 100% SYNCHRONOUS, no IO/node/clock/random,
16
+ // imports ONLY from src/types/**.
17
+ /**
18
+ * A non-fast-forward push rejection. Matched against the combined stdout+stderr
19
+ * of the `git push`. The alternatives cover git's TRUE non-fast-forward phrasings:
20
+ * • `! [rejected] … (non-fast-forward)` / `(non-fast-forward)`
21
+ * • `! [rejected] … (fetch first)` / `fetch first`
22
+ * • `Updates were rejected because the tip … is behind …`
23
+ *
24
+ * It deliberately does NOT match the bare word `rejected`. git emits
25
+ * `! [remote rejected] … (push declined …)` for SERVER-SIDE declines —
26
+ * branch-protection rules, pre-receive hooks, or a PAT lacking `workflow` scope
27
+ * (the 227 case: GitHub refusing a `.github/workflows/` edit). Those are NOT
28
+ * non-fast-forwards and are NOT recoverable by force-with-lease. The old bare
29
+ * `rejected` alternative false-positived them into the NFF fetch→force recovery,
30
+ * whose fetch then died `couldn't find remote ref` (the push had never landed) and
31
+ * MASKED the real decline message the operator needs (issue 227). A declined push
32
+ * now fails fast with its own stderr instead.
33
+ */
34
+ const NON_FAST_FORWARD_RE = /non-fast-forward|fetch first|Updates were rejected/i;
35
+ /** Canonical `…/pull/<n>` URL → number capture (shared shape with runlog PR_URL_RE). */
36
+ const PR_URL_RE = /\/pull\/(\d+)/;
37
+ /**
38
+ * A SERVER-SIDE push decline caused by the push credential lacking the GitHub
39
+ * `workflow` scope / fine-grained Workflows:write permission. GitHub refuses any
40
+ * push whose diff creates or updates a file under `.github/workflows/` unless the
41
+ * token carries that scope — the shared fine-grained PAT symphony's in-VM agent
42
+ * pushes with (`gh auth setup-git`) is INTENTIONALLY not granted it, so such a
43
+ * push is always declined and can only be landed by a manual SSH push (issue 235).
44
+ *
45
+ * git surfaces this as `! [remote rejected] … (refusing to allow a Personal Access
46
+ * Token to create or update workflow `.github/workflows/…` without `workflow`
47
+ * scope)`. The two alternatives catch the "refusing to allow … workflow" decline
48
+ * and the trailing "… without `workflow` scope" clause. This is DISTINCT from a
49
+ * non-fast-forward (see NON_FAST_FORWARD_RE): a scope decline is never recoverable
50
+ * by force-with-lease.
51
+ */
52
+ const WORKFLOW_SCOPE_DECLINE_RE = /refusing to allow[\s\S]*?\bworkflow\b|\bworkflow\b[^\n]{0,20}\bscope\b/i;
53
+ /** The actionable prefix a workflow-scope decline is rewritten under (also the
54
+ * idempotency guard so {@link describeHandoffFailure} never double-wraps). */
55
+ const MANUAL_PUSH_PREFIX = 'manual SSH push required';
56
+ /**
57
+ * True when a push failure reason is the GitHub workflow-scope server-side
58
+ * decline (a `.github/workflows/` edit pushed by the Workflows:write-less PAT).
59
+ */
60
+ export function isWorkflowScopeDecline(reason) {
61
+ return WORKFLOW_SCOPE_DECLINE_RE.test(reason);
62
+ }
63
+ /**
64
+ * Rewrite a failed-handoff reason into an operator-actionable one. A workflow-scope
65
+ * decline (see {@link isWorkflowScopeDecline}) becomes a "manual SSH push required"
66
+ * message naming the cause + the fix, keeping the original git error appended so
67
+ * the raw stderr stays inspectable; every other reason passes through unchanged.
68
+ * Idempotent — an already-rewritten reason is returned as-is.
69
+ */
70
+ export function describeHandoffFailure(reason) {
71
+ if (reason.startsWith(MANUAL_PUSH_PREFIX))
72
+ return reason;
73
+ if (isWorkflowScopeDecline(reason)) {
74
+ return (`${MANUAL_PUSH_PREFIX} — the shared fine-grained PAT is not granted ` +
75
+ `Workflows:write, so GitHub server-side-declines any push touching ` +
76
+ `.github/workflows/. Land this branch with a manual SSH push (the token ` +
77
+ `never enters the VM). Original git error: ${reason}`);
78
+ }
79
+ return reason;
80
+ }
81
+ /**
82
+ * Classify a `git push` RunResultData: clean iff `exit_code === 0`; on a non-zero
83
+ * exit, flag a non-fast-forward rejection from the combined output. A clean push
84
+ * is never reported as non-fast-forward. Pure mirror of the inline interpreter
85
+ * `pushStatus` decision. (`clean` = exited 0; `nonFastForward` = recoverable via
86
+ * force-with-lease.)
87
+ */
88
+ export function classifyPushResult(result) {
89
+ const clean = result.exit_code === 0;
90
+ return {
91
+ clean,
92
+ nonFastForward: !clean && NON_FAST_FORWARD_RE.test(`${result.stdout}\n${result.stderr}`),
93
+ };
94
+ }
95
+ /**
96
+ * Parse the `gh pr create` stdout into the created PR's number + URL. `gh` prints
97
+ * the PR URL as the last non-empty line; we scan it for the `/pull/<n>` segment.
98
+ * Returns `{ number: 0, url }` when no PR number can be parsed (the original's
99
+ * behavior — a created-but-unparseable URL still surfaces the URL, number 0), and
100
+ * `{ number: 0, url: '' }` when stdout carried no URL at all.
101
+ */
102
+ export function parsePrNumberFromUrl(stdout) {
103
+ const url = stdout.trim().split('\n').pop() ?? '';
104
+ const m = PR_URL_RE.exec(url);
105
+ return { number: m ? Number(m[1]) : 0, url };
106
+ }
107
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../../../../src/core/git/result.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,iFAAiF;AACjF,8EAA8E;AAC9E,+EAA+E;AAC/E,mFAAmF;AACnF,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,kFAAkF;AAClF,iFAAiF;AACjF,gFAAgF;AAChF,iCAAiC;AACjC,EAAE;AACF,+EAA+E;AAC/E,kCAAkC;AAIlC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,mBAAmB,GAAG,qDAAqD,CAAC;AAElF,wFAAwF;AACxF,MAAM,SAAS,GAAG,eAAe,CAAC;AAElC;;;;;;;;;;;;;;GAcG;AACH,MAAM,yBAAyB,GAC7B,yEAAyE,CAAC;AAE5E;+EAC+E;AAC/E,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,OAAO,yBAAyB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,IAAI,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC;QAAE,OAAO,MAAM,CAAC;IACzD,IAAI,sBAAsB,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,OAAO,CACL,GAAG,kBAAkB,gDAAgD;YACrE,oEAAoE;YACpE,yEAAyE;YACzE,6CAA6C,MAAM,EAAE,CACtD,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,KAAK,CAAC,CAAC;IACrC,OAAO;QACL,KAAK;QACL,cAAc,EAAE,CAAC,KAAK,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;KACzF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAClD,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,225 @@
1
+ // FCIS rewrite — PURE HTTP decision layer.
2
+ //
3
+ // Ported faithfully from the original (read-only reference):
4
+ // src/http-handlers.ts:decideCreateIssue (+ decideTriageTransition, now
5
+ // generalized into decideMoveTransition — the operator move; issue 126)
6
+ // src/orchestrator-decisions.ts:buildIssueDetailDto (+ its two private section builders)
7
+ //
8
+ // Each function takes already-parsed inputs and returns a decision (validated
9
+ // command + HTTP status, or a DTO) WITHOUT touching IncomingMessage /
10
+ // ServerResponse / the filesystem / the wall clock. The HTTP shell stays a thin
11
+ // dispatcher: parse → decide (here) → emit.
12
+ //
13
+ // 100% synchronous. No async/await/Promise, no IO, no node: imports, no
14
+ // process/fetch, no wall-clock or randomness. Imports ONLY from src/types.
15
+ //
16
+ // NOTE on dates: buildIssueDetailDto formats `retry.due_at_ms` into ISO-8601 via
17
+ // the local pure `epochMsToIso` helper (NOT the `Date` constructor — that is
18
+ // lint-banned in core since it can't tell a deterministic ms-conversion from a
19
+ // wall-clock read). The conversion is a deterministic function of its argument.
20
+ import { resolveDeclaredName, isInFlowTransition } from '../state-resolve.js';
21
+ // ─── POST /api/v1/issues body validation ────────────────────────────────────
22
+ // `identifier` and `state` are optional: identifier is derived from the title when
23
+ // omitted (by the shell), state defaults to the first declared active state
24
+ // (typically `Todo`). State is restricted to declared non-terminal states (active
25
+ // or holding) so the kanban's `+ new issue` affordance can target Triage and so
26
+ // terminal states stay closed to direct creation. Values containing path
27
+ // separators / `..` are rejected by the set lookup so the request cannot escape
28
+ // the tracker root via path.join.
29
+ export function decideCreateIssue(body, states) {
30
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
31
+ return { ok: false, status: 400, code: 'bad_request', message: 'body must be a JSON object' };
32
+ }
33
+ const b = body;
34
+ const identifier = typeof b.identifier === 'string' ? b.identifier.trim() : '';
35
+ const title = typeof b.title === 'string' ? b.title.trim() : '';
36
+ const stateInput = typeof b.state === 'string' ? b.state.trim() : '';
37
+ if (!title) {
38
+ return { ok: false, status: 400, code: 'bad_request', message: 'title is required' };
39
+ }
40
+ const firstActiveName = states.find((s) => s.role === 'active')?.name ?? '';
41
+ const state = stateInput || firstActiveName;
42
+ if (!state) {
43
+ return {
44
+ ok: false,
45
+ status: 400,
46
+ code: 'bad_request',
47
+ message: 'state is required (no active states declared to default to)',
48
+ };
49
+ }
50
+ const allowedNames = states.filter((s) => s.role !== 'terminal').map((s) => s.name);
51
+ const allowedStates = new Set(allowedNames);
52
+ if (!allowedStates.has(state)) {
53
+ return {
54
+ ok: false,
55
+ status: 400,
56
+ code: 'bad_request',
57
+ message: `state must be one of: ${allowedNames.join(', ') || '<none configured>'}`,
58
+ };
59
+ }
60
+ const description = typeof b.description === 'string' ? b.description : undefined;
61
+ const priority = typeof b.priority === 'number' && Number.isFinite(b.priority) ? b.priority : null;
62
+ const labels = Array.isArray(b.labels)
63
+ ? b.labels.filter((x) => typeof x === 'string')
64
+ : [];
65
+ const blocked_by = Array.isArray(b.blocked_by)
66
+ ? b.blocked_by.filter((x) => typeof x === 'string')
67
+ : [];
68
+ return { ok: true, identifier, title, state, description, priority, labels, blocked_by };
69
+ }
70
+ // ─── Operator move transition (POST /api/v1/issues/<id>/move) ─────────────────
71
+ // Replaces the old fixed approve/discard `decideTriageTransition` (issue 126):
72
+ // the operator (the human-in-the-loop) can move ANY issue to ANY declared state.
73
+ // Approve becomes "move to <first active>", discard "move to <terminal>" — both
74
+ // ordinary in-flow moves with no separate code path.
75
+ //
76
+ // The decider validates the body's target `state` (declared, canonicalized),
77
+ // then:
78
+ // - 400 when the target is blank or not a declared state;
79
+ // - 409 `from_state_mismatch` when the body's claimed `from_state` does not
80
+ // match the file's ACTUAL on-disk directory (`actualFromState`, resolved by
81
+ // the shell) — hardens a hand-crafted POST and kills the move-by-id
82
+ // ambiguity (the file moved out from under the operator);
83
+ // - otherwise ok, carrying the canonical target, the actual from-state (so the
84
+ // shell moves the RIGHT file), and the soft `offFlow` warning flag.
85
+ //
86
+ // `offFlow` is keyed on the SAME graph the agent path enforces: a move is
87
+ // off-flow when the actual from-state's `allowed_transitions` is non-null and
88
+ // does not include the target (the exact condition `decideTransition` rejects on
89
+ // — but here the operator move still applies; the flag is observability only).
90
+ // A null list, or an undeclared from-state, is in-flow (the open default); a
91
+ // self-transition is always in-flow. The shared predicate is
92
+ // `isInFlowTransition` (src/core/state-resolve.ts).
93
+ export function decideMoveTransition(body, states, actualFromState) {
94
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
95
+ return { ok: false, status: 400, code: 'bad_request', message: 'body must be a JSON object' };
96
+ }
97
+ const b = body;
98
+ const target = typeof b.state === 'string' ? b.state.trim() : '';
99
+ const claimedFrom = typeof b.from_state === 'string' ? b.from_state.trim() : '';
100
+ if (!target) {
101
+ return { ok: false, status: 400, code: 'bad_request', message: 'state is required' };
102
+ }
103
+ const order = states.map((s) => s.name);
104
+ const canonicalTarget = resolveDeclaredName(order, target);
105
+ if (canonicalTarget === null) {
106
+ return {
107
+ ok: false,
108
+ status: 400,
109
+ code: 'bad_request',
110
+ message: `state must be one of: ${order.join(', ') || '<none configured>'}`,
111
+ };
112
+ }
113
+ // Claimed from-state must match the file's actual directory (case-insensitive,
114
+ // matching the rest of symphony). A mismatch means the operator's view is stale
115
+ // or the POST was hand-crafted — refuse rather than move the wrong file.
116
+ if (claimedFrom && claimedFrom.toLowerCase() !== actualFromState.toLowerCase()) {
117
+ return {
118
+ ok: false,
119
+ status: 409,
120
+ code: 'from_state_mismatch',
121
+ message: `issue is in "${actualFromState}", not "${claimedFrom}"`,
122
+ };
123
+ }
124
+ // Off-flow flag keyed on the ACTUAL from-state's allowed_transitions. An
125
+ // undeclared from-state (an orphan directory) canonicalizes to null ⇒ open ⇒
126
+ // in-flow, matching the agent path's "skip the gate when from is undeclared".
127
+ const canonicalFrom = resolveDeclaredName(order, actualFromState);
128
+ const allowed = canonicalFrom !== null
129
+ ? states.find((s) => s.name === canonicalFrom)?.allowed_transitions ?? null
130
+ : null;
131
+ const offFlow = !isInFlowTransition(allowed, canonicalFrom ?? actualFromState, canonicalTarget);
132
+ return { ok: true, toState: canonicalTarget, fromState: actualFromState, offFlow };
133
+ }
134
+ // ─── Issue-detail DTO (GET /api/v1/<identifier>) ──────────────────────────────
135
+ // Pure with respect to the views supplied by the shell (the running/retry scans
136
+ // are impure and live in the shell). Returns null when neither a running nor a
137
+ // retrying entry matches the identifier.
138
+ /**
139
+ * Build the issue-detail DTO served by `GET /api/v1/<identifier>`. Pure with
140
+ * respect to the views supplied by the shell. Returns null when neither a
141
+ * running nor a retrying entry matches.
142
+ */
143
+ export function buildIssueDetailDto(identifier, entry, retry) {
144
+ if (!entry && !retry)
145
+ return null;
146
+ // A pending `continuation` is a clean post-transition handoff (e.g.
147
+ // Review → Todo), not a failure — surface it as a calm `resuming` state so an
148
+ // API client can tell it apart from a stuck `failure`-backoff `retrying`. (The
149
+ // dashboard pill collapses a continuation into `running` under the Three-Pill
150
+ // Rule — issue 154 — since a live-updating list would otherwise flash. This
151
+ // point-query DTO never flashes, so it keeps the finer-grained `resuming`
152
+ // distinction.)
153
+ const status = entry
154
+ ? 'running'
155
+ : retry?.kind === 'continuation'
156
+ ? 'resuming'
157
+ : 'retrying';
158
+ return {
159
+ issue_identifier: identifier,
160
+ issue_id: entry?.issue_id ?? retry?.issue_id ?? null,
161
+ status,
162
+ workspace: entry ? { path: entry.workspace_path } : null,
163
+ attempts: { current_retry_attempt: retry?.attempt ?? null },
164
+ running: entry ? buildIssueDetailRunningSection(entry) : null,
165
+ retry: retry ? buildIssueDetailRetrySection(retry) : null,
166
+ recent_events: entry?.recent_events ?? [],
167
+ last_error: entry?.last_error ?? retry?.error ?? null,
168
+ tracked: {},
169
+ };
170
+ }
171
+ function buildIssueDetailRunningSection(entry) {
172
+ return {
173
+ session_id: entry.session_id,
174
+ turn_count: entry.turn_count,
175
+ state: entry.state,
176
+ started_at: entry.started_at,
177
+ last_event: entry.last_event,
178
+ last_message: entry.last_message,
179
+ last_event_at: entry.last_event_at,
180
+ tokens: {
181
+ input_tokens: entry.input_tokens,
182
+ output_tokens: entry.output_tokens,
183
+ total_tokens: entry.total_tokens,
184
+ },
185
+ };
186
+ }
187
+ function buildIssueDetailRetrySection(retry) {
188
+ return {
189
+ attempt: retry.attempt,
190
+ due_at: epochMsToIso(retry.due_at_ms),
191
+ error: retry.error,
192
+ kind: retry.kind,
193
+ };
194
+ }
195
+ // Pure epoch-ms → ISO-8601 UTC string, byte-for-byte identical to
196
+ // `new Date(ms).toISOString()` but WITHOUT the `Date` constructor (which the
197
+ // core-purity lint bans, as it cannot tell a deterministic ms-conversion from a
198
+ // wall-clock read). Civil-date split uses Howard Hinnant's days→y/m/d algorithm.
199
+ function epochMsToIso(ms) {
200
+ const totalMs = Math.trunc(ms);
201
+ const days = Math.floor(totalMs / 86_400_000);
202
+ const ofDayMs = totalMs - days * 86_400_000; // 0..86_399_999
203
+ const msPart = ofDayMs % 1000;
204
+ const secs = (ofDayMs - msPart) / 1000;
205
+ const ss = secs % 60;
206
+ const mins = (secs - ss) / 60;
207
+ const mm = mins % 60;
208
+ const hh = (mins - mm) / 60;
209
+ // days since 1970-01-01 → year/month/day (era-based, Gregorian)
210
+ const z = days + 719_468;
211
+ const era = Math.floor(z / 146_097);
212
+ const doe = z - era * 146_097;
213
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36_524) - Math.floor(doe / 146_096)) / 365);
214
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
215
+ const mp = Math.floor((5 * doy + 2) / 153);
216
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
217
+ const m = mp < 10 ? mp + 3 : mp - 9;
218
+ const year = yoe + era * 400 + (m <= 2 ? 1 : 0);
219
+ const p2 = (n) => String(n).padStart(2, '0');
220
+ const yStr = year < 0 || year > 9999
221
+ ? (year < 0 ? '-' : '+') + String(Math.abs(year)).padStart(6, '0')
222
+ : String(year).padStart(4, '0');
223
+ return `${yStr}-${p2(m)}-${p2(d)}T${p2(hh)}:${p2(mm)}:${p2(ss)}.${String(msPart).padStart(3, '0')}Z`;
224
+ }
225
+ //# sourceMappingURL=decisions.js.map