@ashlr/hub 2.2.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 (477) hide show
  1. package/CHANGELOG.md +1158 -0
  2. package/LICENSE +21 -0
  3. package/README.md +833 -0
  4. package/bin/ashlr +8 -0
  5. package/dist/api/core.d.ts +28 -0
  6. package/dist/api/core.js +42 -0
  7. package/dist/api/core.js.map +1 -0
  8. package/dist/api/index.d.ts +9 -0
  9. package/dist/api/index.js +9 -0
  10. package/dist/api/index.js.map +1 -0
  11. package/dist/api/plugin.d.ts +12 -0
  12. package/dist/api/plugin.js +13 -0
  13. package/dist/api/plugin.js.map +1 -0
  14. package/dist/api/types.d.ts +9 -0
  15. package/dist/api/types.js +9 -0
  16. package/dist/api/types.js.map +1 -0
  17. package/dist/cli/args.d.ts +20 -0
  18. package/dist/cli/args.js +23 -0
  19. package/dist/cli/args.js.map +1 -0
  20. package/dist/cli/ask.d.ts +22 -0
  21. package/dist/cli/ask.js +240 -0
  22. package/dist/cli/ask.js.map +1 -0
  23. package/dist/cli/audit.d.ts +26 -0
  24. package/dist/cli/audit.js +199 -0
  25. package/dist/cli/audit.js.map +1 -0
  26. package/dist/cli/backlog.d.ts +26 -0
  27. package/dist/cli/backlog.js +322 -0
  28. package/dist/cli/backlog.js.map +1 -0
  29. package/dist/cli/completions.d.ts +18 -0
  30. package/dist/cli/completions.js +156 -0
  31. package/dist/cli/completions.js.map +1 -0
  32. package/dist/cli/daemon.d.ts +31 -0
  33. package/dist/cli/daemon.js +334 -0
  34. package/dist/cli/daemon.js.map +1 -0
  35. package/dist/cli/demo-sandbox.d.ts +78 -0
  36. package/dist/cli/demo-sandbox.js +227 -0
  37. package/dist/cli/demo-sandbox.js.map +1 -0
  38. package/dist/cli/demo.d.ts +76 -0
  39. package/dist/cli/demo.js +285 -0
  40. package/dist/cli/demo.js.map +1 -0
  41. package/dist/cli/digest.d.ts +46 -0
  42. package/dist/cli/digest.js +352 -0
  43. package/dist/cli/digest.js.map +1 -0
  44. package/dist/cli/doctor-init.d.ts +28 -0
  45. package/dist/cli/doctor-init.js +335 -0
  46. package/dist/cli/doctor-init.js.map +1 -0
  47. package/dist/cli/genome.d.ts +62 -0
  48. package/dist/cli/genome.js +1061 -0
  49. package/dist/cli/genome.js.map +1 -0
  50. package/dist/cli/gh.d.ts +27 -0
  51. package/dist/cli/gh.js +322 -0
  52. package/dist/cli/gh.js.map +1 -0
  53. package/dist/cli/goals.d.ts +55 -0
  54. package/dist/cli/goals.js +667 -0
  55. package/dist/cli/goals.js.map +1 -0
  56. package/dist/cli/health.d.ts +56 -0
  57. package/dist/cli/health.js +407 -0
  58. package/dist/cli/health.js.map +1 -0
  59. package/dist/cli/help.d.ts +51 -0
  60. package/dist/cli/help.js +436 -0
  61. package/dist/cli/help.js.map +1 -0
  62. package/dist/cli/inbox.d.ts +28 -0
  63. package/dist/cli/inbox.js +589 -0
  64. package/dist/cli/inbox.js.map +1 -0
  65. package/dist/cli/index.d.ts +41 -0
  66. package/dist/cli/index.js +1208 -0
  67. package/dist/cli/index.js.map +1 -0
  68. package/dist/cli/knowledge.d.ts +33 -0
  69. package/dist/cli/knowledge.js +575 -0
  70. package/dist/cli/knowledge.js.map +1 -0
  71. package/dist/cli/mcp.d.ts +26 -0
  72. package/dist/cli/mcp.js +487 -0
  73. package/dist/cli/mcp.js.map +1 -0
  74. package/dist/cli/models.d.ts +26 -0
  75. package/dist/cli/models.js +435 -0
  76. package/dist/cli/models.js.map +1 -0
  77. package/dist/cli/new.d.ts +32 -0
  78. package/dist/cli/new.js +565 -0
  79. package/dist/cli/new.js.map +1 -0
  80. package/dist/cli/notify.d.ts +20 -0
  81. package/dist/cli/notify.js +188 -0
  82. package/dist/cli/notify.js.map +1 -0
  83. package/dist/cli/onboard.d.ts +85 -0
  84. package/dist/cli/onboard.js +305 -0
  85. package/dist/cli/onboard.js.map +1 -0
  86. package/dist/cli/open.d.ts +38 -0
  87. package/dist/cli/open.js +86 -0
  88. package/dist/cli/open.js.map +1 -0
  89. package/dist/cli/orient.d.ts +14 -0
  90. package/dist/cli/orient.js +110 -0
  91. package/dist/cli/orient.js.map +1 -0
  92. package/dist/cli/picker.d.ts +21 -0
  93. package/dist/cli/picker.js +177 -0
  94. package/dist/cli/picker.js.map +1 -0
  95. package/dist/cli/plugins.d.ts +21 -0
  96. package/dist/cli/plugins.js +394 -0
  97. package/dist/cli/plugins.js.map +1 -0
  98. package/dist/cli/preflight.d.ts +29 -0
  99. package/dist/cli/preflight.js +131 -0
  100. package/dist/cli/preflight.js.map +1 -0
  101. package/dist/cli/pulse.d.ts +26 -0
  102. package/dist/cli/pulse.js +482 -0
  103. package/dist/cli/pulse.js.map +1 -0
  104. package/dist/cli/reflect.d.ts +42 -0
  105. package/dist/cli/reflect.js +381 -0
  106. package/dist/cli/reflect.js.map +1 -0
  107. package/dist/cli/run.d.ts +29 -0
  108. package/dist/cli/run.js +687 -0
  109. package/dist/cli/run.js.map +1 -0
  110. package/dist/cli/sandbox.d.ts +20 -0
  111. package/dist/cli/sandbox.js +345 -0
  112. package/dist/cli/sandbox.js.map +1 -0
  113. package/dist/cli/seams.d.ts +41 -0
  114. package/dist/cli/seams.js +167 -0
  115. package/dist/cli/seams.js.map +1 -0
  116. package/dist/cli/serve.d.ts +25 -0
  117. package/dist/cli/serve.js +249 -0
  118. package/dist/cli/serve.js.map +1 -0
  119. package/dist/cli/ship.d.ts +33 -0
  120. package/dist/cli/ship.js +433 -0
  121. package/dist/cli/ship.js.map +1 -0
  122. package/dist/cli/spec.d.ts +19 -0
  123. package/dist/cli/spec.js +478 -0
  124. package/dist/cli/spec.js.map +1 -0
  125. package/dist/cli/swarm.d.ts +42 -0
  126. package/dist/cli/swarm.js +1365 -0
  127. package/dist/cli/swarm.js.map +1 -0
  128. package/dist/cli/telemetry.d.ts +26 -0
  129. package/dist/cli/telemetry.js +364 -0
  130. package/dist/cli/telemetry.js.map +1 -0
  131. package/dist/cli/tui.d.ts +17 -0
  132. package/dist/cli/tui.js +52 -0
  133. package/dist/cli/tui.js.map +1 -0
  134. package/dist/cli/ui.d.ts +53 -0
  135. package/dist/cli/ui.js +66 -0
  136. package/dist/cli/ui.js.map +1 -0
  137. package/dist/cli/update.d.ts +38 -0
  138. package/dist/cli/update.js +682 -0
  139. package/dist/cli/update.js.map +1 -0
  140. package/dist/cli/vercel.d.ts +20 -0
  141. package/dist/cli/vercel.js +193 -0
  142. package/dist/cli/vercel.js.map +1 -0
  143. package/dist/cli/verify-safety.d.ts +96 -0
  144. package/dist/cli/verify-safety.js +509 -0
  145. package/dist/cli/verify-safety.js.map +1 -0
  146. package/dist/cli/wire.d.ts +22 -0
  147. package/dist/cli/wire.js +205 -0
  148. package/dist/cli/wire.js.map +1 -0
  149. package/dist/core/classify.d.ts +51 -0
  150. package/dist/core/classify.js +450 -0
  151. package/dist/core/classify.js.map +1 -0
  152. package/dist/core/config.d.ts +63 -0
  153. package/dist/core/config.js +474 -0
  154. package/dist/core/config.js.map +1 -0
  155. package/dist/core/daemon/loop.d.ts +74 -0
  156. package/dist/core/daemon/loop.js +618 -0
  157. package/dist/core/daemon/loop.js.map +1 -0
  158. package/dist/core/daemon/state.d.ts +66 -0
  159. package/dist/core/daemon/state.js +197 -0
  160. package/dist/core/daemon/state.js.map +1 -0
  161. package/dist/core/dashboard.d.ts +40 -0
  162. package/dist/core/dashboard.js +463 -0
  163. package/dist/core/dashboard.js.map +1 -0
  164. package/dist/core/digest/build.d.ts +51 -0
  165. package/dist/core/digest/build.js +230 -0
  166. package/dist/core/digest/build.js.map +1 -0
  167. package/dist/core/digest/deliver.d.ts +47 -0
  168. package/dist/core/digest/deliver.js +230 -0
  169. package/dist/core/digest/deliver.js.map +1 -0
  170. package/dist/core/digest/store.d.ts +57 -0
  171. package/dist/core/digest/store.js +223 -0
  172. package/dist/core/digest/store.js.map +1 -0
  173. package/dist/core/doctor-fix.d.ts +21 -0
  174. package/dist/core/doctor-fix.js +440 -0
  175. package/dist/core/doctor-fix.js.map +1 -0
  176. package/dist/core/doctor.d.ts +18 -0
  177. package/dist/core/doctor.js +806 -0
  178. package/dist/core/doctor.js.map +1 -0
  179. package/dist/core/env-bridge.d.ts +64 -0
  180. package/dist/core/env-bridge.js +103 -0
  181. package/dist/core/env-bridge.js.map +1 -0
  182. package/dist/core/genome/capture.d.ts +49 -0
  183. package/dist/core/genome/capture.js +352 -0
  184. package/dist/core/genome/capture.js.map +1 -0
  185. package/dist/core/genome/consolidate.d.ts +38 -0
  186. package/dist/core/genome/consolidate.js +426 -0
  187. package/dist/core/genome/consolidate.js.map +1 -0
  188. package/dist/core/genome/export.d.ts +29 -0
  189. package/dist/core/genome/export.js +102 -0
  190. package/dist/core/genome/export.js.map +1 -0
  191. package/dist/core/genome/playbook.d.ts +33 -0
  192. package/dist/core/genome/playbook.js +320 -0
  193. package/dist/core/genome/playbook.js.map +1 -0
  194. package/dist/core/genome/recall.d.ts +45 -0
  195. package/dist/core/genome/recall.js +293 -0
  196. package/dist/core/genome/recall.js.map +1 -0
  197. package/dist/core/genome/store.d.ts +62 -0
  198. package/dist/core/genome/store.js +711 -0
  199. package/dist/core/genome/store.js.map +1 -0
  200. package/dist/core/git.d.ts +34 -0
  201. package/dist/core/git.js +115 -0
  202. package/dist/core/git.js.map +1 -0
  203. package/dist/core/goals/advance.d.ts +82 -0
  204. package/dist/core/goals/advance.js +263 -0
  205. package/dist/core/goals/advance.js.map +1 -0
  206. package/dist/core/goals/planner.d.ts +58 -0
  207. package/dist/core/goals/planner.js +233 -0
  208. package/dist/core/goals/planner.js.map +1 -0
  209. package/dist/core/goals/store.d.ts +128 -0
  210. package/dist/core/goals/store.js +442 -0
  211. package/dist/core/goals/store.js.map +1 -0
  212. package/dist/core/inbox/apply.d.ts +36 -0
  213. package/dist/core/inbox/apply.js +407 -0
  214. package/dist/core/inbox/apply.js.map +1 -0
  215. package/dist/core/inbox/notify-proposal.d.ts +13 -0
  216. package/dist/core/inbox/notify-proposal.js +21 -0
  217. package/dist/core/inbox/notify-proposal.js.map +1 -0
  218. package/dist/core/inbox/store.d.ts +67 -0
  219. package/dist/core/inbox/store.js +258 -0
  220. package/dist/core/inbox/store.js.map +1 -0
  221. package/dist/core/index-engine.d.ts +62 -0
  222. package/dist/core/index-engine.js +486 -0
  223. package/dist/core/index-engine.js.map +1 -0
  224. package/dist/core/integrations/desktop-notify.d.ts +18 -0
  225. package/dist/core/integrations/desktop-notify.js +42 -0
  226. package/dist/core/integrations/desktop-notify.js.map +1 -0
  227. package/dist/core/integrations/editors.d.ts +50 -0
  228. package/dist/core/integrations/editors.js +216 -0
  229. package/dist/core/integrations/editors.js.map +1 -0
  230. package/dist/core/integrations/github.d.ts +66 -0
  231. package/dist/core/integrations/github.js +332 -0
  232. package/dist/core/integrations/github.js.map +1 -0
  233. package/dist/core/integrations/identity.d.ts +29 -0
  234. package/dist/core/integrations/identity.js +359 -0
  235. package/dist/core/integrations/identity.js.map +1 -0
  236. package/dist/core/integrations/notify.d.ts +23 -0
  237. package/dist/core/integrations/notify.js +96 -0
  238. package/dist/core/integrations/notify.js.map +1 -0
  239. package/dist/core/integrations/vercel.d.ts +37 -0
  240. package/dist/core/integrations/vercel.js +192 -0
  241. package/dist/core/integrations/vercel.js.map +1 -0
  242. package/dist/core/knowledge/ask.d.ts +34 -0
  243. package/dist/core/knowledge/ask.js +325 -0
  244. package/dist/core/knowledge/ask.js.map +1 -0
  245. package/dist/core/knowledge/graph.d.ts +51 -0
  246. package/dist/core/knowledge/graph.js +564 -0
  247. package/dist/core/knowledge/graph.js.map +1 -0
  248. package/dist/core/knowledge/index.d.ts +74 -0
  249. package/dist/core/knowledge/index.js +586 -0
  250. package/dist/core/knowledge/index.js.map +1 -0
  251. package/dist/core/learn/playbooks.d.ts +84 -0
  252. package/dist/core/learn/playbooks.js +241 -0
  253. package/dist/core/learn/playbooks.js.map +1 -0
  254. package/dist/core/learn/reflect.d.ts +87 -0
  255. package/dist/core/learn/reflect.js +435 -0
  256. package/dist/core/learn/reflect.js.map +1 -0
  257. package/dist/core/learn/store.d.ts +51 -0
  258. package/dist/core/learn/store.js +165 -0
  259. package/dist/core/learn/store.js.map +1 -0
  260. package/dist/core/learn/tuning.d.ts +48 -0
  261. package/dist/core/learn/tuning.js +201 -0
  262. package/dist/core/learn/tuning.js.map +1 -0
  263. package/dist/core/lifecycle/scaffold.d.ts +43 -0
  264. package/dist/core/lifecycle/scaffold.js +260 -0
  265. package/dist/core/lifecycle/scaffold.js.map +1 -0
  266. package/dist/core/lifecycle/ship.d.ts +48 -0
  267. package/dist/core/lifecycle/ship.js +513 -0
  268. package/dist/core/lifecycle/ship.js.map +1 -0
  269. package/dist/core/lifecycle/templates.d.ts +20 -0
  270. package/dist/core/lifecycle/templates.js +605 -0
  271. package/dist/core/lifecycle/templates.js.map +1 -0
  272. package/dist/core/mcp-gateway.d.ts +59 -0
  273. package/dist/core/mcp-gateway.js +385 -0
  274. package/dist/core/mcp-gateway.js.map +1 -0
  275. package/dist/core/mcp-native.d.ts +53 -0
  276. package/dist/core/mcp-native.js +507 -0
  277. package/dist/core/mcp-native.js.map +1 -0
  278. package/dist/core/mcp-registry.d.ts +36 -0
  279. package/dist/core/mcp-registry.js +180 -0
  280. package/dist/core/mcp-registry.js.map +1 -0
  281. package/dist/core/observability/budget-alert.d.ts +18 -0
  282. package/dist/core/observability/budget-alert.js +90 -0
  283. package/dist/core/observability/budget-alert.js.map +1 -0
  284. package/dist/core/observability/estimate.d.ts +27 -0
  285. package/dist/core/observability/estimate.js +188 -0
  286. package/dist/core/observability/estimate.js.map +1 -0
  287. package/dist/core/observability/forecast.d.ts +19 -0
  288. package/dist/core/observability/forecast.js +101 -0
  289. package/dist/core/observability/forecast.js.map +1 -0
  290. package/dist/core/observability/governance.d.ts +28 -0
  291. package/dist/core/observability/governance.js +101 -0
  292. package/dist/core/observability/governance.js.map +1 -0
  293. package/dist/core/observability/otlp.d.ts +88 -0
  294. package/dist/core/observability/otlp.js +217 -0
  295. package/dist/core/observability/otlp.js.map +1 -0
  296. package/dist/core/observability/rollup.d.ts +35 -0
  297. package/dist/core/observability/rollup.js +311 -0
  298. package/dist/core/observability/rollup.js.map +1 -0
  299. package/dist/core/observability/telemetry-sink.d.ts +63 -0
  300. package/dist/core/observability/telemetry-sink.js +350 -0
  301. package/dist/core/observability/telemetry-sink.js.map +1 -0
  302. package/dist/core/observability/usage-source.d.ts +60 -0
  303. package/dist/core/observability/usage-source.js +347 -0
  304. package/dist/core/observability/usage-source.js.map +1 -0
  305. package/dist/core/onboard.d.ts +27 -0
  306. package/dist/core/onboard.js +288 -0
  307. package/dist/core/onboard.js.map +1 -0
  308. package/dist/core/orient.d.ts +23 -0
  309. package/dist/core/orient.js +135 -0
  310. package/dist/core/orient.js.map +1 -0
  311. package/dist/core/phantom.d.ts +23 -0
  312. package/dist/core/phantom.js +279 -0
  313. package/dist/core/phantom.js.map +1 -0
  314. package/dist/core/plugins/host-api.d.ts +29 -0
  315. package/dist/core/plugins/host-api.js +113 -0
  316. package/dist/core/plugins/host-api.js.map +1 -0
  317. package/dist/core/plugins/integrity.d.ts +36 -0
  318. package/dist/core/plugins/integrity.js +73 -0
  319. package/dist/core/plugins/integrity.js.map +1 -0
  320. package/dist/core/plugins/manifest.d.ts +31 -0
  321. package/dist/core/plugins/manifest.js +316 -0
  322. package/dist/core/plugins/manifest.js.map +1 -0
  323. package/dist/core/plugins/registry.d.ts +87 -0
  324. package/dist/core/plugins/registry.js +415 -0
  325. package/dist/core/plugins/registry.js.map +1 -0
  326. package/dist/core/plugins/types.d.ts +182 -0
  327. package/dist/core/plugins/types.js +40 -0
  328. package/dist/core/plugins/types.js.map +1 -0
  329. package/dist/core/plugins/wrappers.d.ts +40 -0
  330. package/dist/core/plugins/wrappers.js +229 -0
  331. package/dist/core/plugins/wrappers.js.map +1 -0
  332. package/dist/core/portfolio/backlog.d.ts +40 -0
  333. package/dist/core/portfolio/backlog.js +177 -0
  334. package/dist/core/portfolio/backlog.js.map +1 -0
  335. package/dist/core/portfolio/scanners.d.ts +21 -0
  336. package/dist/core/portfolio/scanners.js +600 -0
  337. package/dist/core/portfolio/scanners.js.map +1 -0
  338. package/dist/core/providers.d.ts +34 -0
  339. package/dist/core/providers.js +250 -0
  340. package/dist/core/providers.js.map +1 -0
  341. package/dist/core/quality/conventions.d.ts +35 -0
  342. package/dist/core/quality/conventions.js +267 -0
  343. package/dist/core/quality/conventions.js.map +1 -0
  344. package/dist/core/quality/fixes.d.ts +56 -0
  345. package/dist/core/quality/fixes.js +209 -0
  346. package/dist/core/quality/fixes.js.map +1 -0
  347. package/dist/core/quality/health.d.ts +69 -0
  348. package/dist/core/quality/health.js +350 -0
  349. package/dist/core/quality/health.js.map +1 -0
  350. package/dist/core/quality/store.d.ts +56 -0
  351. package/dist/core/quality/store.js +195 -0
  352. package/dist/core/quality/store.js.map +1 -0
  353. package/dist/core/readiness.d.ts +112 -0
  354. package/dist/core/readiness.js +431 -0
  355. package/dist/core/readiness.js.map +1 -0
  356. package/dist/core/run/agent-loop.d.ts +40 -0
  357. package/dist/core/run/agent-loop.js +291 -0
  358. package/dist/core/run/agent-loop.js.map +1 -0
  359. package/dist/core/run/budget.d.ts +47 -0
  360. package/dist/core/run/budget.js +113 -0
  361. package/dist/core/run/budget.js.map +1 -0
  362. package/dist/core/run/engines.d.ts +75 -0
  363. package/dist/core/run/engines.js +199 -0
  364. package/dist/core/run/engines.js.map +1 -0
  365. package/dist/core/run/model-manager.d.ts +64 -0
  366. package/dist/core/run/model-manager.js +339 -0
  367. package/dist/core/run/model-manager.js.map +1 -0
  368. package/dist/core/run/orchestrator.d.ts +100 -0
  369. package/dist/core/run/orchestrator.js +1515 -0
  370. package/dist/core/run/orchestrator.js.map +1 -0
  371. package/dist/core/run/provider-client.d.ts +46 -0
  372. package/dist/core/run/provider-client.js +796 -0
  373. package/dist/core/run/provider-client.js.map +1 -0
  374. package/dist/core/run/retry.d.ts +19 -0
  375. package/dist/core/run/retry.js +68 -0
  376. package/dist/core/run/retry.js.map +1 -0
  377. package/dist/core/run/router.d.ts +50 -0
  378. package/dist/core/run/router.js +257 -0
  379. package/dist/core/run/router.js.map +1 -0
  380. package/dist/core/run/self-heal.d.ts +52 -0
  381. package/dist/core/run/self-heal.js +181 -0
  382. package/dist/core/run/self-heal.js.map +1 -0
  383. package/dist/core/run/streaming.d.ts +31 -0
  384. package/dist/core/run/streaming.js +122 -0
  385. package/dist/core/run/streaming.js.map +1 -0
  386. package/dist/core/run/verify.d.ts +30 -0
  387. package/dist/core/run/verify.js +204 -0
  388. package/dist/core/run/verify.js.map +1 -0
  389. package/dist/core/sandbox/audit.d.ts +31 -0
  390. package/dist/core/sandbox/audit.js +169 -0
  391. package/dist/core/sandbox/audit.js.map +1 -0
  392. package/dist/core/sandbox/policy.d.ts +61 -0
  393. package/dist/core/sandbox/policy.js +211 -0
  394. package/dist/core/sandbox/policy.js.map +1 -0
  395. package/dist/core/sandbox/worktree.d.ts +175 -0
  396. package/dist/core/sandbox/worktree.js +673 -0
  397. package/dist/core/sandbox/worktree.js.map +1 -0
  398. package/dist/core/seams/backlog.d.ts +47 -0
  399. package/dist/core/seams/backlog.js +47 -0
  400. package/dist/core/seams/backlog.js.map +1 -0
  401. package/dist/core/seams/daemon-coordinator.d.ts +72 -0
  402. package/dist/core/seams/daemon-coordinator.js +76 -0
  403. package/dist/core/seams/daemon-coordinator.js.map +1 -0
  404. package/dist/core/seams/genome.d.ts +45 -0
  405. package/dist/core/seams/genome.js +53 -0
  406. package/dist/core/seams/genome.js.map +1 -0
  407. package/dist/core/seams/identity.d.ts +40 -0
  408. package/dist/core/seams/identity.js +44 -0
  409. package/dist/core/seams/identity.js.map +1 -0
  410. package/dist/core/seams/inbox.d.ts +60 -0
  411. package/dist/core/seams/inbox.js +66 -0
  412. package/dist/core/seams/inbox.js.map +1 -0
  413. package/dist/core/seams/index.d.ts +20 -0
  414. package/dist/core/seams/index.js +21 -0
  415. package/dist/core/seams/index.js.map +1 -0
  416. package/dist/core/seams/portfolio.d.ts +50 -0
  417. package/dist/core/seams/portfolio.js +61 -0
  418. package/dist/core/seams/portfolio.js.map +1 -0
  419. package/dist/core/seams/registry.d.ts +42 -0
  420. package/dist/core/seams/registry.js +128 -0
  421. package/dist/core/seams/registry.js.map +1 -0
  422. package/dist/core/seams/run-swarm.d.ts +66 -0
  423. package/dist/core/seams/run-swarm.js +74 -0
  424. package/dist/core/seams/run-swarm.js.map +1 -0
  425. package/dist/core/seams/types.d.ts +123 -0
  426. package/dist/core/seams/types.js +35 -0
  427. package/dist/core/seams/types.js.map +1 -0
  428. package/dist/core/spec/spec-store.d.ts +62 -0
  429. package/dist/core/spec/spec-store.js +359 -0
  430. package/dist/core/spec/spec-store.js.map +1 -0
  431. package/dist/core/swarm/gate.d.ts +45 -0
  432. package/dist/core/swarm/gate.js +114 -0
  433. package/dist/core/swarm/gate.js.map +1 -0
  434. package/dist/core/swarm/planner.d.ts +32 -0
  435. package/dist/core/swarm/planner.js +293 -0
  436. package/dist/core/swarm/planner.js.map +1 -0
  437. package/dist/core/swarm/rollback.d.ts +56 -0
  438. package/dist/core/swarm/rollback.js +266 -0
  439. package/dist/core/swarm/rollback.js.map +1 -0
  440. package/dist/core/swarm/runner.d.ts +62 -0
  441. package/dist/core/swarm/runner.js +1263 -0
  442. package/dist/core/swarm/runner.js.map +1 -0
  443. package/dist/core/swarm/sign.d.ts +71 -0
  444. package/dist/core/swarm/sign.js +362 -0
  445. package/dist/core/swarm/sign.js.map +1 -0
  446. package/dist/core/swarm/store.d.ts +52 -0
  447. package/dist/core/swarm/store.js +195 -0
  448. package/dist/core/swarm/store.js.map +1 -0
  449. package/dist/core/tidy.d.ts +32 -0
  450. package/dist/core/tidy.js +354 -0
  451. package/dist/core/tidy.js.map +1 -0
  452. package/dist/core/tools-registry.d.ts +16 -0
  453. package/dist/core/tools-registry.js +308 -0
  454. package/dist/core/tools-registry.js.map +1 -0
  455. package/dist/core/types.d.ts +2545 -0
  456. package/dist/core/types.js +9 -0
  457. package/dist/core/types.js.map +1 -0
  458. package/dist/core/web/api.d.ts +53 -0
  459. package/dist/core/web/api.js +698 -0
  460. package/dist/core/web/api.js.map +1 -0
  461. package/dist/core/web/public/app.js +1906 -0
  462. package/dist/core/web/public/index.html +721 -0
  463. package/dist/core/web/public/styles.css +2007 -0
  464. package/dist/core/web/server.d.ts +18 -0
  465. package/dist/core/web/server.js +122 -0
  466. package/dist/core/web/server.js.map +1 -0
  467. package/dist/core/web/static.d.ts +18 -0
  468. package/dist/core/web/static.js +122 -0
  469. package/dist/core/web/static.js.map +1 -0
  470. package/dist/tui/app.d.ts +33 -0
  471. package/dist/tui/app.js +350 -0
  472. package/dist/tui/app.js.map +1 -0
  473. package/dist/tui/render.d.ts +20 -0
  474. package/dist/tui/render.js +558 -0
  475. package/dist/tui/render.js.map +1 -0
  476. package/package.json +80 -0
  477. package/schema/config.schema.json +223 -0
@@ -0,0 +1,1263 @@
1
+ /**
2
+ * core/swarm/runner.ts — M12 swarm runner, M17 verified + unattended-safe.
3
+ *
4
+ * Executes a contracts-first swarm: scaffold → build (parallel) → integrate →
5
+ * verify → review. Each task delegates to orchestrator.runGoal (local-first).
6
+ *
7
+ * Guardrails:
8
+ * - RECURSION GUARD: refuses if ASHLR_IN_SWARM is set in the environment.
9
+ * - HARD TOTAL BUDGET across all tasks; aborts cleanly with partial state.
10
+ * - Bounded concurrency (opts.parallel, default 3, max 8).
11
+ * - LOCAL-FIRST: cloud only when opts.allowCloud + key present.
12
+ * - NO OUTWARD/DESTRUCTIVE ACTION by default (code/build/test only).
13
+ * - Resumable via opts.resumeId; persists after every step.
14
+ * - --background: re-execs self as a detached worker process.
15
+ * - ASHLR_IN_SWARM=1 is set ONCE on this runner's process.env (see runSwarm)
16
+ * and inherited by every spawned engine subprocess. That single assignment
17
+ * is the load-bearing recursion / fork-bomb guard — do not remove it.
18
+ * - Never throws out of runSwarm; all errors surface in SwarmRun.status.
19
+ *
20
+ * M16 addition: auto-capture.
21
+ * - On completion (any terminal status), calls captureFromSwarm (fire-and-
22
+ * forget) from genome/capture.ts. Disabled via opts.noCapture or
23
+ * cfg.genome?.autoCapture === false. Never throws, never blocks.
24
+ *
25
+ * M17 additions: verified + unattended-safe swarms.
26
+ * - snapshotProject: on swarm start, snapshot the project's git state.
27
+ * - signOutput: after each task completes (done), sign its result.
28
+ * - verifyOutput: before a task consumes a dep's result, verify the dep's sig.
29
+ * On mismatch → EscalationEvent{kind:'tamper'} + 'needs-approval' + STOP.
30
+ * - shouldEscalate: after each task, risk-scan goal+result; on a hit →
31
+ * EscalationEvent + 'needs-approval' + STOP. Over-budget is handled by
32
+ * the existing M12 hard-abort path (status='aborted'), not this gate.
33
+ * - opts.approved: when resuming a 'needs-approval' swarm, clear the pause.
34
+ * - All M17 helpers are best-effort: import failures degrade gracefully.
35
+ */
36
+ import * as path from 'node:path';
37
+ import * as child_process from 'node:child_process';
38
+ import { fileURLToPath } from 'node:url';
39
+ import { newUsage, addUsage, overBudget } from '../run/budget.js';
40
+ import { planSwarm } from './planner.js';
41
+ import { saveSwarm, loadSwarm } from './store.js';
42
+ import { runGoal } from '../run/orchestrator.js';
43
+ let _signOutput = null;
44
+ let _verifyOutput = null;
45
+ let _riskScan = null;
46
+ let _shouldEscalate = null;
47
+ let _snapshotProject = null;
48
+ let _m17Loaded = false;
49
+ async function loadM17() {
50
+ if (_m17Loaded)
51
+ return;
52
+ _m17Loaded = true;
53
+ try {
54
+ const sign = await import('./sign.js');
55
+ _signOutput = sign.signOutput;
56
+ _verifyOutput = sign.verifyOutput;
57
+ }
58
+ catch {
59
+ // sign.ts not built yet — signing/verification degrades to no-op
60
+ }
61
+ try {
62
+ const gate = await import('./gate.js');
63
+ _riskScan = gate.riskScan;
64
+ _shouldEscalate = gate.shouldEscalate;
65
+ }
66
+ catch {
67
+ // gate.ts not built yet — escalation degrades to no-op
68
+ }
69
+ try {
70
+ const rb = await import('./rollback.js');
71
+ _snapshotProject = rb.snapshotProject;
72
+ }
73
+ catch {
74
+ // rollback.ts not built yet — snapshot degrades to no-op
75
+ }
76
+ }
77
+ let _createSandbox = null;
78
+ let _sandboxDiff = null;
79
+ let _removeSandbox = null;
80
+ let _audit = null;
81
+ let _createProposal = null;
82
+ let _m21Loaded = false;
83
+ async function loadM21() {
84
+ if (_m21Loaded)
85
+ return;
86
+ _m21Loaded = true;
87
+ try {
88
+ // Store the specifier in a variable so TypeScript does not attempt static
89
+ // module resolution on a module that may not exist yet (M21 worktree.ts is
90
+ // built by a sibling agent). The try/catch degrades gracefully if absent.
91
+ const wtSpec = '../sandbox/worktree.js';
92
+ const wt = await import(/* @vite-ignore */ wtSpec);
93
+ _createSandbox = wt.createSandbox;
94
+ _sandboxDiff = wt.sandboxDiff;
95
+ _removeSandbox = wt.removeSandbox;
96
+ }
97
+ catch {
98
+ // worktree.ts not built yet — sandbox mode degrades to no-op (default behavior)
99
+ }
100
+ try {
101
+ const auSpec = '../sandbox/audit.js';
102
+ const au = await import(/* @vite-ignore */ auSpec);
103
+ _audit = au.audit;
104
+ }
105
+ catch {
106
+ // audit.ts not built yet — audit degrades to no-op
107
+ }
108
+ try {
109
+ // M24: the inbox proposal sink — used ONLY when opts.propose is set.
110
+ const ibSpec = '../inbox/store.js';
111
+ const ib = await import(/* @vite-ignore */ ibSpec);
112
+ _createProposal = ib.createProposal;
113
+ }
114
+ catch {
115
+ // inbox/store.ts not built yet — propose degrades to no-op
116
+ }
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // Constants
120
+ // ---------------------------------------------------------------------------
121
+ const DEFAULT_PARALLEL = 3;
122
+ const MAX_PARALLEL = 8;
123
+ const DEFAULT_MAX_TOKENS = 200_000;
124
+ const DEFAULT_MAX_STEPS = 200;
125
+ const PHASE_ORDER = [
126
+ 'scaffold',
127
+ 'build',
128
+ 'integrate',
129
+ 'verify',
130
+ 'review',
131
+ ];
132
+ // ---------------------------------------------------------------------------
133
+ // ID generation
134
+ // ---------------------------------------------------------------------------
135
+ // Monotonic, process-local sequence counter. Date.now() has only millisecond
136
+ // resolution, so two swarms minted in the same ms would otherwise rely solely on
137
+ // the ~24-bit random suffix for uniqueness (birthday-bound collision risk under
138
+ // bursty creation). Mirrors inbox/store.ts generateId(): the zero-padded counter
139
+ // comes BEFORE the random segment so lexicographic id comparison orders by
140
+ // (timestamp, monotonic counter) — a stable most-recent-first tiebreak. Purely
141
+ // additive id-uniqueness hardening: no behavior/signature change, no new dep, no
142
+ // guard touched. Exported as a thin test seam for the H3 stress suite.
143
+ //
144
+ // SINGLE-PROCESS BOUNDARY: `_seq` is module-level and resets to 0 per process, so
145
+ // it guarantees uniqueness only WITHIN one process. Two concurrent daemon
146
+ // processes minting in the same ms rely on the `<ts>`+`<rand>` segments alone
147
+ // (astronomically unlikely, not a hard cross-process guarantee). Cross-process id
148
+ // allocation — like the multi-daemon budget race — is the GATED M30
149
+ // DaemonCoordinator seam, out of H3 scope. See docs/contracts/CONTRACT-H3.md MULTI-PROCESS
150
+ // LIMITATION.
151
+ let _seq = 0;
152
+ export function makeId() {
153
+ const ts = Date.now().toString(36);
154
+ const seq = (_seq++).toString(36).padStart(6, '0');
155
+ const rand = Math.random().toString(36).slice(2, 8);
156
+ return `swarm-${ts}-${seq}-${rand}`;
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Budget helpers
160
+ // ---------------------------------------------------------------------------
161
+ function buildBudget(opts) {
162
+ return {
163
+ maxTokens: opts.budget?.maxTokens ?? DEFAULT_MAX_TOKENS,
164
+ maxSteps: opts.budget?.maxSteps ?? DEFAULT_MAX_STEPS,
165
+ allowCloud: opts.budget?.allowCloud ?? opts.allowCloud ?? false,
166
+ };
167
+ }
168
+ /**
169
+ * Return a per-task budget slice that NEVER lets the sum of concurrently
170
+ * authorized tasks exceed the hard total budget.
171
+ *
172
+ * HARD-BUDGET CONTRACT: the caller passes `reserved` — the tokens/steps already
173
+ * authorized to in-flight siblings in the current batch (not yet reflected in
174
+ * `used`, because run.usage is only updated AFTER a task completes). The slice
175
+ * is computed off the pool that remains AFTER subtracting both already-spent
176
+ * (`used`) and in-flight-authorized (`reserved`) budget, then divided across the
177
+ * tasks still to be launched in this batch (`remainingInBatch`, >= 1). This
178
+ * guarantees sum(authorized task budgets in a batch) <= total - used, so a
179
+ * single concurrent batch can never overshoot the swarm ceiling (the prior
180
+ * 8 x 25% = ~200% overshoot is impossible).
181
+ *
182
+ * Returns maxTokens/maxSteps === 0 when the pool is exhausted, signalling the
183
+ * caller to skip the task rather than authorize budget that doesn't exist.
184
+ */
185
+ function sliceBudget(total, used, allowCloud, reserved = { tokens: 0, steps: 0 }, remainingInBatch = 1) {
186
+ const slots = Math.max(1, remainingInBatch);
187
+ const poolTokens = Math.max(0, total.maxTokens - (used.tokensIn + used.tokensOut) - reserved.tokens);
188
+ const poolSteps = Math.max(0, total.maxSteps - used.steps - reserved.steps);
189
+ // Per-task ceiling: an equal share of the UNRESERVED remaining pool, capped at
190
+ // 25% of the total so one task can't hog the whole swarm even when the batch
191
+ // is small. Never less than 1 (avoids a zero-budget task that aborts instantly)
192
+ // UNLESS the pool itself is exhausted — then 0 tells the caller to skip.
193
+ const fairShareTokens = Math.floor(poolTokens / slots);
194
+ const fairShareSteps = Math.floor(poolSteps / slots);
195
+ const taskMaxTokens = poolTokens <= 0
196
+ ? 0
197
+ : Math.max(1, Math.min(fairShareTokens, Math.ceil(total.maxTokens / 4)));
198
+ const taskMaxSteps = poolSteps <= 0
199
+ ? 0
200
+ : Math.max(1, Math.min(fairShareSteps, Math.ceil(total.maxSteps / 4)));
201
+ return {
202
+ maxTokens: taskMaxTokens,
203
+ maxSteps: taskMaxSteps,
204
+ allowCloud,
205
+ };
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // Sink helpers
209
+ // ---------------------------------------------------------------------------
210
+ function emit(sink, kind, text, taskId, data) {
211
+ try {
212
+ sink({ kind, taskId, text, data, ts: new Date().toISOString() });
213
+ }
214
+ catch {
215
+ // sink must never propagate
216
+ }
217
+ }
218
+ function emitLog(sink, text, data) {
219
+ emit(sink, 'log', text, undefined, data);
220
+ }
221
+ // ---------------------------------------------------------------------------
222
+ // M17 helpers — escalation
223
+ // ---------------------------------------------------------------------------
224
+ /**
225
+ * Push an EscalationEvent onto the SwarmRun, set status='needs-approval',
226
+ * persist, and emit a log line. The CALLER must stop the swarm after this.
227
+ * Never throws.
228
+ */
229
+ function escalate(run, kind, taskId, detail, sink) {
230
+ const event = {
231
+ taskId,
232
+ kind,
233
+ detail,
234
+ ts: new Date().toISOString(),
235
+ };
236
+ if (!run.escalations)
237
+ run.escalations = [];
238
+ run.escalations.push(event);
239
+ run.status = 'needs-approval';
240
+ persist(run);
241
+ emitLog(sink, `[M17] Swarm PAUSED (needs-approval): ${kind} — ${detail}`, { taskId, kind });
242
+ }
243
+ // ---------------------------------------------------------------------------
244
+ // Swarm initialisation / state helpers
245
+ // ---------------------------------------------------------------------------
246
+ function initSwarmRun(id, goal, specId, plan, budget, parallel, project) {
247
+ const now = new Date().toISOString();
248
+ const tasks = plan.tasks.map((t) => ({
249
+ id: t.id,
250
+ phase: t.phase,
251
+ status: 'pending',
252
+ }));
253
+ return {
254
+ id,
255
+ goal,
256
+ specId,
257
+ project,
258
+ createdAt: now,
259
+ updatedAt: now,
260
+ budget,
261
+ usage: newUsage(),
262
+ parallel,
263
+ status: 'planning',
264
+ plan,
265
+ tasks,
266
+ };
267
+ }
268
+ function touch(run) {
269
+ run.updatedAt = new Date().toISOString();
270
+ }
271
+ /** Persist + update timestamp. */
272
+ function persist(run) {
273
+ touch(run);
274
+ try {
275
+ saveSwarm(run);
276
+ }
277
+ catch {
278
+ // best-effort persistence; never crash the run
279
+ }
280
+ }
281
+ // ---------------------------------------------------------------------------
282
+ // Single-task execution
283
+ // ---------------------------------------------------------------------------
284
+ /**
285
+ * Returns 'continue' when the task completed normally (or was skipped/failed
286
+ * without tripping a gate), 'escalate' when an M17 gate fired and the swarm
287
+ * must STOP (status already set to 'needs-approval' by escalate()).
288
+ */
289
+ async function executeTask(taskId, goal, phase, run, cfg, opts, sink,
290
+ /** Budget already authorized to in-flight sibling tasks in this batch. */
291
+ reserved = { tokens: 0, steps: 0 },
292
+ /** Number of tasks (including this one) still being launched in this batch. */
293
+ remainingInBatch = 1,
294
+ /**
295
+ * M21 SANDBOX SEAM: when set, tasks run inside the worktree path instead of
296
+ * run.project. Null (default) = exactly today's behavior (use run.project).
297
+ */
298
+ sandboxCwd = null) {
299
+ // Find the SwarmTaskRun slot.
300
+ const taskRun = run.tasks.find((t) => t.id === taskId);
301
+ if (!taskRun)
302
+ return 'continue';
303
+ // Skip if already done (resume path).
304
+ if (taskRun.status === 'done')
305
+ return 'continue';
306
+ // Check hard budget before starting.
307
+ if (overBudget(run.usage, run.budget)) {
308
+ taskRun.status = 'skipped';
309
+ taskRun.error = 'Skipped: swarm budget exceeded before task started';
310
+ persist(run);
311
+ return 'continue';
312
+ }
313
+ // Build per-task budget slice. This accounts for BOTH already-spent usage AND
314
+ // the budget reserved by concurrent siblings, so the sum of all in-flight task
315
+ // budgets in a batch can never exceed the swarm's remaining hard total.
316
+ const taskBudget = sliceBudget(run.budget, run.usage, opts.allowCloud ?? false, reserved, remainingInBatch);
317
+ // If the unreserved pool is exhausted, skip rather than authorize phantom
318
+ // budget. This is what keeps a concurrent batch under the hard ceiling.
319
+ if (taskBudget.maxTokens <= 0 || taskBudget.maxSteps <= 0) {
320
+ taskRun.status = 'skipped';
321
+ taskRun.error =
322
+ 'Skipped: swarm budget exhausted (no remaining tokens/steps for this task)';
323
+ persist(run);
324
+ return 'continue';
325
+ }
326
+ // -------------------------------------------------------------------------
327
+ // M17: verify dependency signatures BEFORE consuming their outputs.
328
+ // On any tamper/mismatch, escalate and stop — do NOT proceed.
329
+ // -------------------------------------------------------------------------
330
+ const taskSpec = run.plan.tasks.find((t) => t.id === taskId);
331
+ if (taskSpec !== undefined && taskSpec.deps.length > 0 && _verifyOutput !== null) {
332
+ for (const depId of taskSpec.deps) {
333
+ const depRun = run.tasks.find((r) => r.id === depId);
334
+ if (depRun !== undefined &&
335
+ depRun.status === 'done' &&
336
+ depRun.result !== undefined) {
337
+ // UNSIGNED-DEPENDENCY GATE: if signing is enabled (_signOutput present)
338
+ // but this done dep carries NO signature, do NOT silently consume it.
339
+ // An attacker who edits a persisted dep result AND strips its signature
340
+ // would otherwise defeat tamper detection ("only when a signature happens
341
+ // to exist"). Treat a missing signature on a signed-swarm dep as tamper.
342
+ // Skip the gate when this task was explicitly approved by a human.
343
+ if (depRun.signature === undefined &&
344
+ _signOutput !== null &&
345
+ taskRun.approved !== true) {
346
+ escalate(run, 'tamper', taskId, `Dependency "${depId}" completed without a signature (signing is enabled) — ` +
347
+ `cannot verify its output; possible tamper or stripped signature.`, sink);
348
+ return 'escalate';
349
+ }
350
+ if (depRun.signature !== undefined) {
351
+ // verifyOutput never throws (contract); extra guard just in case.
352
+ let ok = false;
353
+ try {
354
+ ok = _verifyOutput(depRun.result, depRun.signature, cfg);
355
+ }
356
+ catch {
357
+ ok = false;
358
+ }
359
+ if (!ok) {
360
+ escalate(run, 'tamper', taskId, `Dependency "${depId}" output failed signature verification — possible tamper.`, sink);
361
+ return 'escalate';
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ // -------------------------------------------------------------------------
368
+ // M17: risk scan on the task GOAL before execution.
369
+ //
370
+ // SKIP when this task was explicitly approved by a human (taskRun.approved):
371
+ // the pre-execution goal-risk gate trips BEFORE the task runs, so without this
372
+ // skip an approved goal-risk escalation would re-scan the identical static
373
+ // goal text on resume and re-escalate forever (infinite needs-approval loop).
374
+ // approved is only set by `ashlr swarm approve <id>` for the escalated task.
375
+ // -------------------------------------------------------------------------
376
+ if (_riskScan !== null && _shouldEscalate !== null && taskRun.approved !== true) {
377
+ let goalRisk = { risky: false, reason: '' };
378
+ try {
379
+ goalRisk = _riskScan(goal);
380
+ }
381
+ catch { /* never throws by contract */ }
382
+ if (goalRisk.risky) {
383
+ let kind = null;
384
+ try {
385
+ kind = _shouldEscalate({ risk: true });
386
+ }
387
+ catch { /* pure, never throws */ }
388
+ if (kind !== null) {
389
+ escalate(run, kind, taskId, `Risk gate on task goal: ${goalRisk.reason}`, sink);
390
+ return 'escalate';
391
+ }
392
+ }
393
+ }
394
+ taskRun.status = 'running';
395
+ persist(run);
396
+ emit(sink, 'task-start', `[${phase}] ${goal}`, taskId);
397
+ // Build a safe goal that cannot trigger outward actions.
398
+ // We prefix the goal with a guardrail instruction. This is DEFENSE-IN-DEPTH
399
+ // only — the structural no-outward guarantee comes from forcing engine:'builtin'
400
+ // below (the builtin agent loop has no outward tool executors).
401
+ const safeGoal = [
402
+ 'IMPORTANT: This task is part of an automated swarm. Perform ONLY code/build/test work.',
403
+ 'Do NOT push to git, deploy, create repositories, or run any outward/destructive actions.',
404
+ `Task: ${goal}`,
405
+ ].join('\n');
406
+ try {
407
+ const taskResult = await runGoal(safeGoal, cfg, {
408
+ budget: taskBudget,
409
+ parallel: 1,
410
+ tools: true,
411
+ allowCloud: opts.allowCloud ?? false,
412
+ // HARD NO-OUTWARD GUARD: force the builtin engine so a cfg/env default
413
+ // engine can never silently delegate a swarm task to a real autonomous
414
+ // CLI (claude/aw/ac) that could push/deploy/ship. The builtin agent loop
415
+ // has no outward tool executors, so the no-outward property holds even if
416
+ // the model ignores the prompt prefix above.
417
+ engine: 'builtin',
418
+ // M21: operate in the sandbox worktree when set; otherwise fall back to
419
+ // run.project (unchanged behavior when opts.sandbox is falsy).
420
+ cwd: sandboxCwd ?? run.project ?? undefined,
421
+ // RECURSION/FORK-BOMB GUARD: ASHLR_IN_SWARM=1 is set once on this runner
422
+ // process's env (see runSwarm) and inherited by every spawned engine
423
+ // subprocess. That single assignment is the load-bearing guard; this task
424
+ // sets nothing per-call.
425
+ noMemory: false,
426
+ });
427
+ // Accumulate usage into swarm totals.
428
+ run.usage = addUsage(run.usage, taskResult.usage);
429
+ taskRun.status = taskResult.status === 'done' ? 'done' : 'failed';
430
+ taskRun.result = taskResult.result;
431
+ taskRun.usage = taskResult.usage;
432
+ if (taskResult.status !== 'done') {
433
+ taskRun.error = `Task ended with status: ${taskResult.status}`;
434
+ }
435
+ }
436
+ catch (err) {
437
+ taskRun.status = 'failed';
438
+ taskRun.error = err instanceof Error ? err.message : String(err);
439
+ }
440
+ emit(sink, 'task-done', `[${phase}] ${taskId} ${taskRun.status}`, taskId, { status: taskRun.status, usage: taskRun.usage });
441
+ // -------------------------------------------------------------------------
442
+ // M17: sign the task output when done (tamper-evidence for downstream tasks).
443
+ // Best-effort: a signing failure never crashes or blocks the swarm.
444
+ // -------------------------------------------------------------------------
445
+ if (taskRun.status === 'done' && taskRun.result !== undefined && _signOutput !== null) {
446
+ try {
447
+ taskRun.signature = _signOutput(taskRun.result, cfg);
448
+ }
449
+ catch {
450
+ // best-effort; absent signature means downstream verification skips this dep
451
+ }
452
+ }
453
+ // -------------------------------------------------------------------------
454
+ // M17: post-task escalation gates — risk scan on task result.
455
+ //
456
+ // NOTE: over-budget is intentionally NOT checked here. The existing M12
457
+ // hard-budget abort logic (phase-loop pre-phase check + per-task pre-start
458
+ // check) owns over-budget and surfaces it as status='aborted'. Passing
459
+ // overBudget:true to shouldEscalate here would race against those checks
460
+ // and produce status='needs-approval' instead of 'aborted', breaking the
461
+ // M12 contract and the 1619 tests. The 'over-budget' escalation kind
462
+ // remains available for callers that opt into it explicitly (e.g. the CLI
463
+ // approve path), but the runner uses the M12 hard-abort for this condition.
464
+ // -------------------------------------------------------------------------
465
+ // SKIP the result risk scan for an explicitly-approved task too: once a human
466
+ // has approved a task past its gate, re-escalating on the same task's output
467
+ // would defeat the approval and (on the next approve) loop. approved tasks are
468
+ // a deliberate, human-acknowledged exception.
469
+ if (_riskScan !== null && _shouldEscalate !== null && taskRun.approved !== true) {
470
+ // Risk scan on the result text (could contain outward-op output).
471
+ let resultRisk = { risky: false, reason: '' };
472
+ try {
473
+ resultRisk = _riskScan(taskRun.result ?? '');
474
+ }
475
+ catch { /* never throws */ }
476
+ if (resultRisk.risky) {
477
+ let kind = null;
478
+ try {
479
+ kind = _shouldEscalate({ risk: true });
480
+ }
481
+ catch {
482
+ kind = null;
483
+ }
484
+ if (kind !== null) {
485
+ escalate(run, kind, taskId, `Risk gate on task result: ${resultRisk.reason}`, sink);
486
+ return 'escalate';
487
+ }
488
+ }
489
+ }
490
+ persist(run);
491
+ return 'continue';
492
+ }
493
+ // ---------------------------------------------------------------------------
494
+ // Phase execution
495
+ // ---------------------------------------------------------------------------
496
+ /**
497
+ * Returns:
498
+ * true — phase completed normally
499
+ * false — budget exceeded mid-phase
500
+ * 'escalate' — an M17 gate fired; swarm must STOP
501
+ */
502
+ async function executePhase(phase, run, cfg, opts, sink, parallelCap,
503
+ /** M21 SANDBOX SEAM: worktree path to run tasks in. Null = today's behavior. */
504
+ sandboxCwd = null) {
505
+ const phaseTasks = run.plan.tasks.filter((t) => t.phase === phase);
506
+ if (phaseTasks.length === 0)
507
+ return true; // nothing to do
508
+ emitLog(sink, `phase start: ${phase} (${phaseTasks.length} task(s))`);
509
+ if (phase === 'build') {
510
+ // BUILD phase: run tasks in parallel batches, capped at parallelCap.
511
+ // Dep-respecting within build: tasks with deps wait for their deps.
512
+ // For simplicity (planner guarantees build tasks are independent), we
513
+ // execute them in batches of parallelCap.
514
+ const pending = phaseTasks.filter((t) => run.tasks.find((r) => r.id === t.id)?.status === 'pending');
515
+ for (let i = 0; i < pending.length; i += parallelCap) {
516
+ // Abort entire phase if budget is already blown.
517
+ if (overBudget(run.usage, run.budget)) {
518
+ emitLog(sink, `phase ${phase}: aborting — swarm budget exceeded`);
519
+ return false;
520
+ }
521
+ const batch = pending.slice(i, i + parallelCap);
522
+ // Reserve budget for each concurrent task BEFORE launching it, so no two
523
+ // siblings in the same batch can both be sized against the same stale
524
+ // run.usage. The sum of reservations + run.usage never exceeds the hard
525
+ // total, so the batch cannot overshoot the swarm ceiling.
526
+ const reserved = { tokens: 0, steps: 0 };
527
+ const launches = [];
528
+ for (let b = 0; b < batch.length; b++) {
529
+ const t = batch[b];
530
+ const remainingInBatch = batch.length - b;
531
+ // Snapshot the per-task slice this task will be authorized so we can
532
+ // reserve it for the siblings launched after it in this loop.
533
+ const slice = sliceBudget(run.budget, run.usage, opts.allowCloud ?? false, reserved, remainingInBatch);
534
+ const snapshot = { tokens: reserved.tokens, steps: reserved.steps };
535
+ reserved.tokens += Math.max(0, slice.maxTokens);
536
+ reserved.steps += Math.max(0, slice.maxSteps);
537
+ launches.push(executeTask(t.id, t.goal, phase, run, cfg, opts, sink, snapshot, remainingInBatch, sandboxCwd));
538
+ }
539
+ const results = await Promise.all(launches);
540
+ // If any task in the batch triggered an escalation gate, stop the phase.
541
+ if (results.includes('escalate')) {
542
+ return 'escalate';
543
+ }
544
+ // Persist burndown after each batch.
545
+ const done = run.tasks.filter((r) => phaseTasks.some((pt) => pt.id === r.id) && r.status === 'done').length;
546
+ emitLog(sink, `phase ${phase}: ${done}/${phaseTasks.length} done`, { done, total: phaseTasks.length });
547
+ }
548
+ }
549
+ else {
550
+ // All other phases: sequential execution respecting deps.
551
+ for (const taskSpec of phaseTasks) {
552
+ if (overBudget(run.usage, run.budget)) {
553
+ emitLog(sink, `phase ${phase}: aborting — swarm budget exceeded`);
554
+ return false;
555
+ }
556
+ // Wait for deps (from earlier phases) to have completed.
557
+ const depsOk = taskSpec.deps.every((depId) => {
558
+ const depRun = run.tasks.find((r) => r.id === depId);
559
+ return depRun?.status === 'done';
560
+ });
561
+ if (!depsOk) {
562
+ const taskRun = run.tasks.find((r) => r.id === taskSpec.id);
563
+ if (taskRun) {
564
+ taskRun.status = 'skipped';
565
+ taskRun.error = 'Skipped: one or more dependencies did not complete';
566
+ persist(run);
567
+ }
568
+ emitLog(sink, `task ${taskSpec.id} skipped: deps not met`);
569
+ continue;
570
+ }
571
+ const result = await executeTask(taskSpec.id, taskSpec.goal, phase, run, cfg, opts, sink, { tokens: 0, steps: 0 }, 1, sandboxCwd);
572
+ if (result === 'escalate') {
573
+ return 'escalate';
574
+ }
575
+ }
576
+ }
577
+ emitLog(sink, `phase done: ${phase}`);
578
+ return true;
579
+ }
580
+ // ---------------------------------------------------------------------------
581
+ // Background worker re-exec
582
+ // ---------------------------------------------------------------------------
583
+ /**
584
+ * Launch a detached background worker that will run the swarm and write
585
+ * progress to the swarm record. Returns the swarm id immediately.
586
+ *
587
+ * The worker is re-exec'd as:
588
+ * node <bin/ashlr> swarm --resume <id> --_worker
589
+ *
590
+ * The --_worker flag suppresses --background recursion and signals the
591
+ * CLI to run the swarm synchronously (no further detach).
592
+ */
593
+ function spawnBackgroundWorker(swarmId) {
594
+ // Locate the bin entry point relative to this file.
595
+ // __dirname equivalent for ESM:
596
+ const thisFile = fileURLToPath(import.meta.url);
597
+ // src/core/swarm/runner.ts → go up 3 dirs to project root → bin/ashlr
598
+ const projectRoot = path.resolve(path.dirname(thisFile), '..', '..', '..');
599
+ const binPath = path.join(projectRoot, 'bin', 'ashlr');
600
+ const child = child_process.spawn(process.execPath, [binPath, 'swarm', '--resume', swarmId, '--_worker'], {
601
+ detached: true,
602
+ stdio: 'ignore',
603
+ env: {
604
+ ...process.env,
605
+ // Worker must NOT set ASHLR_IN_SWARM itself — it IS the swarm runner.
606
+ ASHLR_IN_SWARM: undefined,
607
+ },
608
+ });
609
+ child.unref();
610
+ }
611
+ // ---------------------------------------------------------------------------
612
+ // M19: Telemetry emit + governance (best-effort, fire-and-forget, opt-in)
613
+ // ---------------------------------------------------------------------------
614
+ /**
615
+ * Fire-and-forget OTLP/local telemetry emit for a completed swarm.
616
+ *
617
+ * Dynamically imports observability/telemetry-sink.ts and observability/otlp.ts
618
+ * so this file compiles even before those modules exist. Only emits when both
619
+ * modules are available. All failures are logged to stderr; never thrown to
620
+ * the caller. Never blocks. METADATA ONLY — spans carry model/token/cost/ids/
621
+ * status; never prompts, completions, tool args, file contents, or secrets.
622
+ */
623
+ async function fireEmitSwarm(run, cfg) {
624
+ await (async () => {
625
+ try {
626
+ // Lazy-import the telemetry seam so the swarm core has no hard dependency
627
+ // on it at module-load time (keeps the emit fully best-effort). Both
628
+ // modules are real and fully typed.
629
+ const [sinkMod, otlpMod] = await Promise.all([
630
+ import('../observability/telemetry-sink.js'),
631
+ import('../observability/otlp.js'),
632
+ ]);
633
+ if (typeof sinkMod.getSink !== 'function' ||
634
+ typeof otlpMod.spansFromSwarm !== 'function') {
635
+ return;
636
+ }
637
+ // allowPhantomProbe:false — never run a blocking spawnSync phantom probe
638
+ // on the swarm completion path; OtlpHttpSink resolves the PAT async/bounded.
639
+ const telSink = sinkMod.getSink(cfg, false);
640
+ const spans = otlpMod.spansFromSwarm(run);
641
+ const result = await telSink.emit(spans);
642
+ if (!result.ok) {
643
+ process.stderr.write(`[ashlr swarm] telemetry: emit failed — ${result.detail ?? 'unknown'}\n`);
644
+ }
645
+ }
646
+ catch (err) {
647
+ const msg = err instanceof Error ? err.message : String(err);
648
+ process.stderr.write(`[ashlr swarm] telemetry: best-effort emit failed — ${msg}\n`);
649
+ }
650
+ })();
651
+ }
652
+ /**
653
+ * Evaluate spend governance for a swarm run.
654
+ * Returns a blocking reason string when govAction==='block' AND level==='over'
655
+ * AND --over-budget was not passed. Prints a prominent advisory for warn/over.
656
+ * Never throws. Returns null to proceed normally.
657
+ */
658
+ async function checkGovernanceSwarm(cfg, overBudgetFlag) {
659
+ try {
660
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
661
+ const govMod = await import('../observability/governance.js');
662
+ if (typeof govMod.evalGovernance !== 'function')
663
+ return null;
664
+ const verdict = govMod.evalGovernance(cfg);
665
+ if (verdict.level === 'over') {
666
+ process.stderr.write(`\n[ashlr swarm] SPEND GOVERNANCE OVER-CAP: ${verdict.message}\n\n`);
667
+ if (cfg.telemetry?.govAction === 'block' && !overBudgetFlag) {
668
+ return (`Swarm blocked by spend governance: ${verdict.message} ` +
669
+ `Pass --over-budget to proceed.`);
670
+ }
671
+ }
672
+ else if (verdict.level === 'warn') {
673
+ process.stderr.write(`\n[ashlr swarm] SPEND GOVERNANCE WARNING: ${verdict.message}\n\n`);
674
+ }
675
+ return null;
676
+ }
677
+ catch {
678
+ // Governance must never block a swarm on error.
679
+ return null;
680
+ }
681
+ }
682
+ // ---------------------------------------------------------------------------
683
+ // M16: Auto-capture helper (fire-and-forget, never throws)
684
+ // ---------------------------------------------------------------------------
685
+ /**
686
+ * Fire-and-forget genome capture for a completed swarm.
687
+ * Dynamically imports genome/capture.js so the module can be absent
688
+ * during pre-M16 builds without breaking anything.
689
+ * Never throws, never blocks the caller.
690
+ */
691
+ function fireCaptureFromSwarm(run, cfg) {
692
+ void (async () => {
693
+ try {
694
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
695
+ const capMod = await import('../genome/capture.js');
696
+ if (typeof capMod.captureFromSwarm === 'function') {
697
+ capMod.captureFromSwarm(run, cfg);
698
+ }
699
+ }
700
+ catch {
701
+ // Never surface capture errors to the caller.
702
+ }
703
+ })();
704
+ }
705
+ // ---------------------------------------------------------------------------
706
+ // M21: sandbox diff-capture + cleanup helper
707
+ // ---------------------------------------------------------------------------
708
+ /**
709
+ * Capture the diff from an active sandbox worktree and remove it.
710
+ * Appends a `[M21 sandbox proposal]` block to run.result.
711
+ * Always cleans up (removes worktree + scratch branch) regardless of errors.
712
+ * Never throws — all errors are logged to sink and audited.
713
+ */
714
+ function captureSandboxAndCleanup(sb, run, sink, propose = false) {
715
+ // Capture diff (read-only; never mutates source tree).
716
+ let diff = null;
717
+ if (_sandboxDiff !== null) {
718
+ try {
719
+ diff = _sandboxDiff(sb);
720
+ }
721
+ catch (err) {
722
+ const msg = err instanceof Error ? err.message : String(err);
723
+ emitLog(sink, `[M21] Sandbox diff capture failed: ${msg}`);
724
+ try {
725
+ _audit?.({
726
+ action: 'sandbox:diff',
727
+ repo: sb.sourceRepo,
728
+ sandboxId: sb.id,
729
+ summary: `Diff capture failed: ${msg.slice(0, 120)}`,
730
+ result: 'error',
731
+ });
732
+ }
733
+ catch { /* audit best-effort */ }
734
+ }
735
+ }
736
+ if (diff !== null) {
737
+ // Append proposed diff summary to run.result without overwriting it.
738
+ const proposal = [
739
+ '',
740
+ `[M21 sandbox proposal] id=${sb.id}`,
741
+ `files=${diff.files} insertions=${diff.insertions} deletions=${diff.deletions}`,
742
+ diff.patch ? diff.patch.slice(0, 4000) : '(empty diff)',
743
+ ].join('\n');
744
+ run.result = (run.result ?? '') + proposal;
745
+ emitLog(sink, `[M21] Sandbox diff captured: ${diff.files} file(s) changed`);
746
+ try {
747
+ _audit?.({
748
+ action: 'sandbox:diff',
749
+ repo: sb.sourceRepo,
750
+ sandboxId: sb.id,
751
+ summary: `files=${diff.files} +${diff.insertions} -${diff.deletions}`,
752
+ result: 'ok',
753
+ });
754
+ }
755
+ catch { /* audit best-effort */ }
756
+ // M24: when the caller (e.g. the daemon) sets opts.propose, record the
757
+ // captured diff as a PENDING inbox proposal. This is the ONLY way the
758
+ // daemon's work surfaces. A PENDING proposal is applied LATER only by an
759
+ // explicit human inbox approve — never automatically, never here.
760
+ if (propose && _createProposal !== null) {
761
+ try {
762
+ const created = _createProposal({
763
+ repo: sb.sourceRepo,
764
+ origin: 'swarm',
765
+ kind: 'patch',
766
+ title: (run.goal || `swarm ${run.id}`).slice(0, 120),
767
+ summary: [
768
+ `Autonomous swarm proposal (swarm=${run.id}, status=${run.status})`,
769
+ `repo: ${sb.sourceRepo}`,
770
+ `diff: ${diff.files} file(s), +${diff.insertions} -${diff.deletions}`,
771
+ ].join('\n'),
772
+ diff: diff.patch || undefined,
773
+ sandboxId: sb.id,
774
+ });
775
+ emitLog(sink, `[M24] PENDING proposal recorded for swarm ${run.id}`);
776
+ // M32: unattended path (daemon-dispatched swarm) — fire opt-in desktop/
777
+ // webhook notification. Fire-and-forget; metadata only; never blocks.
778
+ void (async () => {
779
+ try {
780
+ const { loadConfig } = await import('../config.js');
781
+ const { notifyNewProposal } = await import('../inbox/notify-proposal.js');
782
+ await notifyNewProposal(created, loadConfig());
783
+ }
784
+ catch { /* notification is best-effort */ }
785
+ })();
786
+ try {
787
+ _audit?.({
788
+ action: 'inbox:proposal-created',
789
+ repo: sb.sourceRepo,
790
+ sandboxId: sb.id,
791
+ summary: `daemon swarm ${run.id} -> PENDING proposal (${diff.files} file(s))`,
792
+ result: 'ok',
793
+ });
794
+ }
795
+ catch { /* audit best-effort */ }
796
+ }
797
+ catch (err) {
798
+ const msg = err instanceof Error ? err.message : String(err);
799
+ emitLog(sink, `[M24] Proposal creation failed: ${msg.slice(0, 120)}`);
800
+ }
801
+ }
802
+ }
803
+ // Always remove sandbox (worktree + scratch branch). Never touches source tree.
804
+ if (_removeSandbox !== null) {
805
+ try {
806
+ _removeSandbox(sb);
807
+ emitLog(sink, `[M21] Sandbox ${sb.id} removed`);
808
+ try {
809
+ _audit?.({
810
+ action: 'sandbox:remove',
811
+ repo: sb.sourceRepo,
812
+ sandboxId: sb.id,
813
+ summary: `Swarm ${run.id} sandbox removed`,
814
+ result: 'ok',
815
+ });
816
+ }
817
+ catch { /* audit best-effort */ }
818
+ }
819
+ catch (err) {
820
+ const msg = err instanceof Error ? err.message : String(err);
821
+ emitLog(sink, `[M21] Sandbox removal failed (manual cleanup may be needed): ${msg}`);
822
+ try {
823
+ _audit?.({
824
+ action: 'sandbox:remove',
825
+ repo: sb.sourceRepo,
826
+ sandboxId: sb.id,
827
+ summary: `Removal failed: ${msg.slice(0, 120)}`,
828
+ result: 'error',
829
+ });
830
+ }
831
+ catch { /* audit best-effort */ }
832
+ }
833
+ }
834
+ }
835
+ // ---------------------------------------------------------------------------
836
+ // Public API
837
+ // ---------------------------------------------------------------------------
838
+ /**
839
+ * Run a swarm: plan → execute phases → synthesize result.
840
+ *
841
+ * Contract:
842
+ * - Never throws; all errors surface in SwarmRun.status ('failed'/'aborted').
843
+ * - RECURSION GUARD: refuses if ASHLR_IN_SWARM is set.
844
+ * - HARD TOTAL BUDGET: aborts cleanly when exceeded.
845
+ * - LOCAL-FIRST: cloud only when opts.allowCloud.
846
+ * - NO OUTWARD ACTION by default.
847
+ * - Resumable via opts.resumeId.
848
+ * - --background: re-execs self detached, returns id immediately.
849
+ * - M16: calls captureFromSwarm on completion unless opts.noCapture is set.
850
+ * - M17: snapshots project git state, signs task outputs, verifies dep
851
+ * signatures before consumption, escalates on gate trips.
852
+ * opts.approved: when set alongside opts.resumeId, resumes a swarm that
853
+ * is in 'needs-approval' status (set by ashlr swarm approve <id>).
854
+ */
855
+ export async function runSwarm(input, cfg, opts, sink) {
856
+ // -------------------------------------------------------------------------
857
+ // RECURSION GUARD — must be the very first check.
858
+ // -------------------------------------------------------------------------
859
+ if (process.env['ASHLR_IN_SWARM']) {
860
+ // Build a minimal failed run record (no persistence needed — we never
861
+ // started, and there's no id to resume).
862
+ const now = new Date().toISOString();
863
+ const failedRun = {
864
+ id: makeId(),
865
+ goal: input.goal,
866
+ specId: input.specId ?? null,
867
+ project: opts.project ?? null,
868
+ createdAt: now,
869
+ updatedAt: now,
870
+ budget: buildBudget(opts),
871
+ usage: newUsage(),
872
+ parallel: Math.min(Math.max(1, opts.parallel ?? DEFAULT_PARALLEL), MAX_PARALLEL),
873
+ status: 'failed',
874
+ plan: { specId: input.specId ?? null, goal: input.goal, tasks: [] },
875
+ tasks: [],
876
+ result: 'Refused: nested swarm detected (ASHLR_IN_SWARM is set). ' +
877
+ 'A swarm task must not spawn another swarm.',
878
+ };
879
+ emitLog(sink, failedRun.result ?? '');
880
+ return failedRun;
881
+ }
882
+ // -------------------------------------------------------------------------
883
+ // Set ASHLR_IN_SWARM=1 on THIS process so all child engine spawns inherit it.
884
+ // This prevents any task (via runGoal → spawnEngine) from recursively
885
+ // invoking `ashlr swarm`.
886
+ // -------------------------------------------------------------------------
887
+ process.env['ASHLR_IN_SWARM'] = '1';
888
+ // -------------------------------------------------------------------------
889
+ // M17: load sign / gate / rollback helpers (best-effort, never throws).
890
+ // -------------------------------------------------------------------------
891
+ await loadM17();
892
+ // -------------------------------------------------------------------------
893
+ // M21: load sandbox primitives (best-effort, never throws).
894
+ // -------------------------------------------------------------------------
895
+ await loadM21();
896
+ // Clamp parallel concurrency.
897
+ const parallel = Math.min(Math.max(1, opts.parallel ?? DEFAULT_PARALLEL), MAX_PARALLEL);
898
+ const budget = buildBudget(opts);
899
+ const project = opts.project ?? null;
900
+ // -------------------------------------------------------------------------
901
+ // M21 SANDBOX SEAM: when opts.sandbox is true AND project is set AND the
902
+ // worktree module loaded, create an isolated git-worktree sandbox so all
903
+ // task execution operates on the worktree path — NEVER the source working
904
+ // tree. Default (opts.sandbox falsy or modules absent) = exactly today's
905
+ // behavior; no worktree is created and sandboxCwd stays null.
906
+ // -------------------------------------------------------------------------
907
+ let activeSandbox = null;
908
+ // The cwd that tasks should run in. null = use run.project (unchanged behavior).
909
+ let sandboxCwd = null;
910
+ // M24 STRICT SANDBOX: when the caller demands a mandatory sandbox
911
+ // (opts.requireSandbox, ALWAYS set by the autonomous daemon), the sandbox is
912
+ // non-optional. If it cannot be created — worktree module absent, source not a
913
+ // git repo, HEAD unresolvable, `git worktree add` fails, or a kill-switch race
914
+ // inside createSandbox — the swarm MUST abort and execute ZERO tasks rather
915
+ // than silently falling back to run.project (the user's real working tree).
916
+ const requireSandbox = opts.sandbox === true && opts.requireSandbox === true;
917
+ const abortNoSandbox = (reason) => {
918
+ const ts = new Date().toISOString();
919
+ const aborted = {
920
+ id: makeId(),
921
+ goal: input.goal,
922
+ specId: input.specId ?? null,
923
+ project,
924
+ createdAt: ts,
925
+ updatedAt: ts,
926
+ budget,
927
+ usage: newUsage(),
928
+ parallel,
929
+ status: 'failed',
930
+ plan: { specId: input.specId ?? null, goal: input.goal, tasks: [] },
931
+ tasks: [],
932
+ result: `Refused: a mandatory sandbox could not be created (${reason}). ` +
933
+ 'No tasks were executed; the working tree was NOT touched.',
934
+ };
935
+ emitLog(sink, aborted.result ?? '');
936
+ try {
937
+ _audit?.({
938
+ action: 'sandbox:create',
939
+ repo: project,
940
+ sandboxId: null,
941
+ summary: `Mandatory sandbox unavailable — swarm aborted (zero tasks): ${reason.slice(0, 120)}`,
942
+ result: 'error',
943
+ });
944
+ }
945
+ catch { /* audit is best-effort */ }
946
+ return aborted;
947
+ };
948
+ if (opts.sandbox === true && project !== null && _createSandbox !== null) {
949
+ try {
950
+ activeSandbox = _createSandbox(project);
951
+ sandboxCwd = activeSandbox.worktreePath;
952
+ emitLog(sink, `[M21] Sandbox created: ${activeSandbox.id} at ${sandboxCwd}`);
953
+ try {
954
+ _audit?.({
955
+ action: 'sandbox:create',
956
+ repo: project,
957
+ sandboxId: activeSandbox.id,
958
+ summary: `Sandbox created for goal: ${input.goal.slice(0, 120)}`,
959
+ result: 'ok',
960
+ });
961
+ }
962
+ catch { /* audit is best-effort */ }
963
+ }
964
+ catch (err) {
965
+ const msg = err instanceof Error ? err.message : String(err);
966
+ // M24: when the sandbox is MANDATORY, a creation failure aborts the run —
967
+ // NEVER fall back to the user's working tree.
968
+ if (requireSandbox) {
969
+ return abortNoSandbox(`createSandbox failed: ${msg}`);
970
+ }
971
+ // Legacy (non-strict) behavior: fall back to non-sandbox mode rather than
972
+ // crashing the swarm. Log the failure and proceed with source tree.
973
+ emitLog(sink, `[M21] Sandbox creation failed (running without sandbox): ${msg}`);
974
+ try {
975
+ _audit?.({
976
+ action: 'sandbox:create',
977
+ repo: project,
978
+ sandboxId: null,
979
+ summary: `Sandbox creation failed: ${msg.slice(0, 120)}`,
980
+ result: 'error',
981
+ });
982
+ }
983
+ catch { /* audit is best-effort */ }
984
+ activeSandbox = null;
985
+ sandboxCwd = null;
986
+ }
987
+ }
988
+ else if (requireSandbox) {
989
+ // Sandbox demanded but a precondition is missing: no project to sandbox, or
990
+ // the worktree module is absent. Abort — do NOT run against run.project.
991
+ if (project === null) {
992
+ return abortNoSandbox('no project specified for a mandatory sandbox');
993
+ }
994
+ return abortNoSandbox('sandbox worktree module unavailable');
995
+ }
996
+ // -------------------------------------------------------------------------
997
+ // M19: Spend governance check (advisory; block only when govAction==='block').
998
+ // Read --over-budget as an extended property (same pattern as noCapture).
999
+ // -------------------------------------------------------------------------
1000
+ {
1001
+ const overBudgetFlag = opts.overBudget === true;
1002
+ const govBlock = await checkGovernanceSwarm(cfg, overBudgetFlag);
1003
+ if (govBlock !== null) {
1004
+ const now = new Date().toISOString();
1005
+ const blockedRun = {
1006
+ id: makeId(),
1007
+ goal: input.goal,
1008
+ specId: input.specId ?? null,
1009
+ project,
1010
+ createdAt: now,
1011
+ updatedAt: now,
1012
+ budget,
1013
+ usage: newUsage(),
1014
+ parallel,
1015
+ status: 'failed',
1016
+ plan: { specId: input.specId ?? null, goal: input.goal, tasks: [] },
1017
+ tasks: [],
1018
+ result: govBlock,
1019
+ };
1020
+ process.stderr.write(`[ashlr swarm] ${govBlock}\n`);
1021
+ emitLog(sink, govBlock);
1022
+ return blockedRun;
1023
+ }
1024
+ }
1025
+ // -------------------------------------------------------------------------
1026
+ // RESUME: load existing SwarmRun if resumeId provided.
1027
+ // -------------------------------------------------------------------------
1028
+ let run;
1029
+ if (opts.resumeId) {
1030
+ const existing = loadSwarm(opts.resumeId);
1031
+ if (!existing) {
1032
+ const now = new Date().toISOString();
1033
+ const notFound = {
1034
+ id: opts.resumeId,
1035
+ goal: input.goal,
1036
+ specId: input.specId ?? null,
1037
+ project,
1038
+ createdAt: now,
1039
+ updatedAt: now,
1040
+ budget,
1041
+ usage: newUsage(),
1042
+ parallel,
1043
+ status: 'failed',
1044
+ plan: { specId: input.specId ?? null, goal: input.goal, tasks: [] },
1045
+ tasks: [],
1046
+ result: `Resume failed: swarm "${opts.resumeId}" not found.`,
1047
+ };
1048
+ emitLog(sink, notFound.result ?? '');
1049
+ return notFound;
1050
+ }
1051
+ // Already complete — return as-is.
1052
+ if (existing.status === 'done') {
1053
+ emitLog(sink, `Swarm ${existing.id} is already complete — nothing to resume.`);
1054
+ return existing;
1055
+ }
1056
+ // -----------------------------------------------------------------------
1057
+ // M17: 'needs-approval' — only proceed when explicitly approved.
1058
+ // No auto-approval path: if approved flag is absent, return as-is.
1059
+ // -----------------------------------------------------------------------
1060
+ if (existing.status === 'needs-approval') {
1061
+ if (!opts.approved) {
1062
+ emitLog(sink, `Swarm ${existing.id} is paused (needs-approval). ` +
1063
+ `Run \`ashlr swarm approve ${existing.id}\` to resume.`);
1064
+ return existing;
1065
+ }
1066
+ // Approved: clear the gate, set running, persist, then continue.
1067
+ existing.status = 'running';
1068
+ persist(existing);
1069
+ emitLog(sink, `Swarm ${existing.id} approved — resuming execution.`);
1070
+ }
1071
+ run = existing;
1072
+ emitLog(sink, `Resuming swarm ${run.id} (status: ${run.status})`);
1073
+ }
1074
+ else {
1075
+ // Fresh swarm — generate id.
1076
+ run = initSwarmRun(makeId(), input.goal, input.specId ?? null,
1077
+ // Plan is populated below; use empty placeholder until then.
1078
+ { specId: input.specId ?? null, goal: input.goal, tasks: [] }, budget, parallel, project);
1079
+ }
1080
+ // -------------------------------------------------------------------------
1081
+ // BACKGROUND: re-exec as detached worker.
1082
+ // -------------------------------------------------------------------------
1083
+ if (opts.background && !opts.resumeId) {
1084
+ // Persist the skeleton record so the worker can find it.
1085
+ persist(run);
1086
+ spawnBackgroundWorker(run.id);
1087
+ emitLog(sink, `Swarm ${run.id} launched in background.`);
1088
+ return run;
1089
+ }
1090
+ // A dry-run is a plan-only PREVIEW: it must NOT pollute `ashlr swarms` with a
1091
+ // misleading record (it executes nothing). Suppress all persistence on the
1092
+ // dry-run path so no swarm file is written to ~/.ashlr/swarms.
1093
+ const maybePersist = (r) => {
1094
+ if (opts.dryRun) {
1095
+ touch(r); // keep updatedAt fresh in-memory, but do not write to disk
1096
+ return;
1097
+ }
1098
+ persist(r);
1099
+ };
1100
+ // -------------------------------------------------------------------------
1101
+ // PLAN (skip if resuming — plan is already in the persisted record).
1102
+ // -------------------------------------------------------------------------
1103
+ if (!opts.resumeId || run.plan.tasks.length === 0) {
1104
+ run.status = 'planning';
1105
+ maybePersist(run);
1106
+ emitLog(sink, `Planning swarm for goal: ${input.goal}`);
1107
+ try {
1108
+ // specBody is intentionally omitted here; spec-store (a sibling module)
1109
+ // is loaded by the CLI layer (cli/swarm.ts) and passed in via the input
1110
+ // if needed. The planner accepts undefined specBody and works goal-only.
1111
+ const specBody = undefined;
1112
+ const plan = await planSwarm({ goal: input.goal, specBody }, cfg);
1113
+ run.plan = plan;
1114
+ // Initialise task run records from the plan (preserve any from resume).
1115
+ const existingIds = new Set(run.tasks.map((t) => t.id));
1116
+ for (const t of plan.tasks) {
1117
+ if (!existingIds.has(t.id)) {
1118
+ run.tasks.push({ id: t.id, phase: t.phase, status: 'pending' });
1119
+ }
1120
+ }
1121
+ maybePersist(run);
1122
+ emitLog(sink, `Plan ready: ${plan.tasks.length} task(s) across phases`);
1123
+ }
1124
+ catch (err) {
1125
+ run.status = 'failed';
1126
+ run.result = `Planning failed: ${err instanceof Error ? err.message : String(err)}`;
1127
+ maybePersist(run);
1128
+ emitLog(sink, run.result);
1129
+ // M21: clean up sandbox even on planning failure (no diff to capture yet).
1130
+ if (activeSandbox !== null)
1131
+ captureSandboxAndCleanup(activeSandbox, run, sink, opts.propose === true);
1132
+ await fireEmitSwarm(run, cfg);
1133
+ if (!opts.noCapture)
1134
+ fireCaptureFromSwarm(run, cfg);
1135
+ return run;
1136
+ }
1137
+ }
1138
+ // -------------------------------------------------------------------------
1139
+ // DRY-RUN: return plan without executing. Status stays 'planning' (NOT 'done')
1140
+ // and nothing is persisted — the plan is returned to the caller for preview.
1141
+ // -------------------------------------------------------------------------
1142
+ if (opts.dryRun) {
1143
+ run.status = 'planning';
1144
+ run.result = `Dry run complete (preview only). Plan has ${run.plan.tasks.length} task(s).`;
1145
+ touch(run);
1146
+ emitLog(sink, run.result);
1147
+ return run;
1148
+ }
1149
+ // -------------------------------------------------------------------------
1150
+ // M17: snapshot the project git state at swarm start (read-only, never throws).
1151
+ // Only snapshot once — skip if a rollback snapshot already exists (resume path).
1152
+ // -------------------------------------------------------------------------
1153
+ if (_snapshotProject !== null && run.rollback === undefined) {
1154
+ try {
1155
+ run.rollback = _snapshotProject(project);
1156
+ }
1157
+ catch {
1158
+ // best-effort: snapshot failure never blocks the swarm
1159
+ }
1160
+ }
1161
+ // -------------------------------------------------------------------------
1162
+ // EXECUTE phases in order.
1163
+ // -------------------------------------------------------------------------
1164
+ run.status = 'running';
1165
+ persist(run);
1166
+ try {
1167
+ for (const phase of PHASE_ORDER) {
1168
+ // Check hard budget before each phase.
1169
+ if (overBudget(run.usage, run.budget)) {
1170
+ run.status = 'aborted';
1171
+ run.result =
1172
+ 'Swarm aborted: hard total budget exceeded before phase ' + phase;
1173
+ persist(run);
1174
+ emitLog(sink, run.result);
1175
+ // M21: capture diff of work done so far, then remove sandbox.
1176
+ if (activeSandbox !== null)
1177
+ captureSandboxAndCleanup(activeSandbox, run, sink, opts.propose === true);
1178
+ await fireEmitSwarm(run, cfg);
1179
+ if (!opts.noCapture)
1180
+ fireCaptureFromSwarm(run, cfg);
1181
+ return run;
1182
+ }
1183
+ const phaseResult = await executePhase(phase, run, cfg, opts, sink, parallel, sandboxCwd);
1184
+ if (phaseResult === 'escalate') {
1185
+ // Swarm already set to 'needs-approval' and persisted by escalate().
1186
+ // Stop cleanly — do NOT proceed to the next phase.
1187
+ emitLog(sink, `Swarm ${run.id} paused at phase "${phase}" — awaiting human approval.`);
1188
+ // M21: capture diff of partial work, then remove sandbox.
1189
+ if (activeSandbox !== null)
1190
+ captureSandboxAndCleanup(activeSandbox, run, sink, opts.propose === true);
1191
+ await fireEmitSwarm(run, cfg);
1192
+ if (!opts.noCapture)
1193
+ fireCaptureFromSwarm(run, cfg);
1194
+ return run;
1195
+ }
1196
+ if (!phaseResult) {
1197
+ // Phase returned false → budget exceeded mid-phase.
1198
+ run.status = 'aborted';
1199
+ run.result = `Swarm aborted: hard total budget exceeded during phase ${phase}`;
1200
+ persist(run);
1201
+ emitLog(sink, run.result);
1202
+ // M21: capture diff of partial work, then remove sandbox.
1203
+ if (activeSandbox !== null)
1204
+ captureSandboxAndCleanup(activeSandbox, run, sink, opts.propose === true);
1205
+ await fireEmitSwarm(run, cfg);
1206
+ if (!opts.noCapture)
1207
+ fireCaptureFromSwarm(run, cfg);
1208
+ return run;
1209
+ }
1210
+ }
1211
+ }
1212
+ catch (err) {
1213
+ // Unexpected error in the phase loop — surface cleanly.
1214
+ run.status = 'failed';
1215
+ run.result = `Swarm failed: ${err instanceof Error ? err.message : String(err)}`;
1216
+ persist(run);
1217
+ emitLog(sink, run.result);
1218
+ // M21: capture diff of any partial work, then remove sandbox.
1219
+ if (activeSandbox !== null)
1220
+ captureSandboxAndCleanup(activeSandbox, run, sink, opts.propose === true);
1221
+ await fireEmitSwarm(run, cfg);
1222
+ if (!opts.noCapture)
1223
+ fireCaptureFromSwarm(run, cfg);
1224
+ return run;
1225
+ }
1226
+ // -------------------------------------------------------------------------
1227
+ // SYNTHESIZE result.
1228
+ // -------------------------------------------------------------------------
1229
+ const doneTasks = run.tasks.filter((t) => t.status === 'done');
1230
+ const failedTasks = run.tasks.filter((t) => t.status === 'failed');
1231
+ const skippedTasks = run.tasks.filter((t) => t.status === 'skipped');
1232
+ const summaryLines = [
1233
+ `Swarm complete. Goal: ${run.goal}`,
1234
+ `Tasks: ${doneTasks.length} done, ${failedTasks.length} failed, ${skippedTasks.length} skipped.`,
1235
+ `Total usage: ${run.usage.tokensIn + run.usage.tokensOut} tokens, ${run.usage.steps} steps.`,
1236
+ ];
1237
+ if (failedTasks.length > 0) {
1238
+ summaryLines.push('Failed tasks: ' +
1239
+ failedTasks.map((t) => `${t.id} (${t.error ?? 'unknown'})`).join(', '));
1240
+ }
1241
+ if (doneTasks.length > 0) {
1242
+ summaryLines.push('\nTask results:');
1243
+ for (const t of doneTasks) {
1244
+ if (t.result) {
1245
+ summaryLines.push(`[${t.id}] ${t.result.slice(0, 200)}`);
1246
+ }
1247
+ }
1248
+ }
1249
+ run.result = summaryLines.join('\n');
1250
+ run.status = failedTasks.length === run.tasks.length ? 'failed' : 'done';
1251
+ persist(run);
1252
+ // M21: capture full sandbox diff proposal, then remove sandbox.
1253
+ if (activeSandbox !== null)
1254
+ captureSandboxAndCleanup(activeSandbox, run, sink, opts.propose === true);
1255
+ emitLog(sink, `Swarm ${run.id} finished with status: ${run.status}`);
1256
+ // M19: Emit telemetry (best-effort, opt-in, fire-and-forget).
1257
+ await fireEmitSwarm(run, cfg);
1258
+ // M16: Auto-capture on completion (fire-and-forget, never throws).
1259
+ if (!opts.noCapture)
1260
+ fireCaptureFromSwarm(run, cfg);
1261
+ return run;
1262
+ }
1263
+ //# sourceMappingURL=runner.js.map