@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,673 @@
1
+ /**
2
+ * worktree.ts — isolated git-worktree sandbox (M21 SAFETY FOUNDATION).
3
+ *
4
+ * Creates an ISOLATED git worktree of a source repo on a NEW scratch branch
5
+ * under ~/.ashlr/sandboxes/<id>/, so autonomous edits NEVER touch the user's
6
+ * working tree, index, HEAD, or their checked-out branch. The worktree is
7
+ * created, used, then discarded (git worktree remove + scratch-branch delete).
8
+ *
9
+ * ISOLATION + DESTRUCTIVE-SAFETY (the whole point):
10
+ * - Sandbox worktrees live ONLY under ~/.ashlr/sandboxes/.
11
+ * - Creating/removing them MUST NOT modify the source repo's working tree,
12
+ * index, HEAD, or the user's branches.
13
+ * - `git worktree add -b <branch> <path> <baseHead>` off the current HEAD;
14
+ * `git worktree remove --force` + `git branch -D` on cleanup.
15
+ * - NEVER `git reset --hard` / `git checkout` in the source repo. NEVER push.
16
+ * NEVER delete user branches. NEVER touch a repo that is not ENROLLED
17
+ * (except via the explicit allowAnyRepo test hatch on a tmp repo).
18
+ * - Every mutating op (and every refused/errored attempt) is audited.
19
+ * - All git invoked via node:child_process arg ARRAYS (execFile, no shell).
20
+ * - No new runtime deps; node builtins only.
21
+ */
22
+ import { execFileSync } from 'node:child_process';
23
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, } from 'node:fs';
24
+ import { homedir } from 'node:os';
25
+ import { join, resolve, sep } from 'node:path';
26
+ import { randomBytes } from 'node:crypto';
27
+ import { isRepo } from '../git.js';
28
+ import { assertMayMutate } from './policy.js';
29
+ import { audit } from './audit.js';
30
+ // ---------------------------------------------------------------------------
31
+ // Constants
32
+ // ---------------------------------------------------------------------------
33
+ const GIT_TIMEOUT = 30_000; // ms — generous; worktree add can touch many files
34
+ const BRANCH_PREFIX = 'ashlr/sandbox/';
35
+ const META_FILE = 'sandbox.json';
36
+ // ---------------------------------------------------------------------------
37
+ // H5 — bounded sandbox lifecycle (LOCAL-ONLY resource guards)
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * H5 CHANGE 4 — hard cap on concurrent on-disk sandboxes. Conservative default:
41
+ * the daemon caps swarm concurrency at 8 (loop.ts) + runner MAX_PARALLEL=8, so
42
+ * 16 leaves ~2x headroom for concurrent daemon swarms + a manual `ashlr swarm`
43
+ * + transient overlap, while still bounding unbounded accumulation. createSandbox
44
+ * sweeps STALE orphans first, then REFUSES (clean audited error) if still over.
45
+ *
46
+ * CONFIGURABLE: an operator (or a deterministic test) may override the default
47
+ * via ASHLR_MAX_SANDBOXES (a positive integer). Resolved at CALL TIME — like the
48
+ * HOME-sensitive path helpers — so a test can set a small cap without restarting
49
+ * the process. An absent/blank/invalid/non-positive value falls back to the
50
+ * conservative default; the override can only ever SET a finite positive bound
51
+ * (it can never disable the cap), keeping this a strictly LOCAL resource guard.
52
+ */
53
+ const MAX_SANDBOXES_DEFAULT = 16;
54
+ /** Resolve the effective sandbox cap (env override, else conservative default). */
55
+ function maxSandboxes() {
56
+ const raw = process.env.ASHLR_MAX_SANDBOXES;
57
+ if (raw !== undefined && raw.trim() !== '') {
58
+ const n = Number.parseInt(raw, 10);
59
+ // Only a finite, positive integer override takes effect; anything else
60
+ // (NaN, <=0, non-numeric) falls back to the safe default. The override can
61
+ // never remove the bound — it can only set a different finite positive one.
62
+ if (Number.isInteger(n) && n > 0)
63
+ return n;
64
+ }
65
+ return MAX_SANDBOXES_DEFAULT;
66
+ }
67
+ /**
68
+ * H5 CHANGE 1 — orphan-sweep staleness threshold (ms). This is the FALLBACK
69
+ * liveness proxy used ONLY for a sandbox that carries no usable `ownerPid`
70
+ * marker (older metadata, or a crash fixture that models a GONE owner). The
71
+ * PRIMARY liveness signal is the positive `ownerPid` marker (see Sandbox.ownerPid
72
+ * + ownerAlive() below): a sandbox whose owner pid is still alive is skipped by
73
+ * the sweep / disk-cap pre-sweep REGARDLESS of age, so a live in-flight worktree
74
+ * is never reclaimed out from under a running swarm — even a cross-process one.
75
+ *
76
+ * Because there is NO hard wall-clock cap on a swarm (the runner bounds a swarm
77
+ * by step count <= 200 and the token budget only — there is no elapsed-time
78
+ * deadline), createdAt-age is NOT by itself a sound liveness proxy. So this
79
+ * threshold is set FAR above any plausible 200-step run (6 hours) as a
80
+ * belt-and-suspenders bound for the only residual gap: the rare pid-reuse case
81
+ * where a crashed swarm's recorded pid was recycled by an unrelated live process
82
+ * (ownerAlive() would then falsely read 'alive', so age must also have elapsed
83
+ * before we'd ever reclaim). The cost of a too-LARGE value (a stale orphan
84
+ * lingering a few hours longer) is far cheaper than the cost of a too-SMALL one
85
+ * (force-removing a live worktree), so we err large. Shared by the daemon-start
86
+ * sweep wiring (loop.ts) and the disk-cap pre-sweep below.
87
+ *
88
+ * CROSS-PROCESS LIVENESS LIMITATION (documented): the only way a live worktree
89
+ * could still be reclaimed is if BOTH (a) its owner process crashed AND (b) the
90
+ * OS recycled its exact pid for a new live process AND (c) >6h elapsed — a
91
+ * vanishingly small window that, even if hit, only reclaims a worktree whose
92
+ * original owner is provably dead.
93
+ */
94
+ export const ORPHAN_STALE_MS = 6 * 60 * 60_000;
95
+ /**
96
+ * Positive liveness check for a sandbox's recorded owner process. Returns true
97
+ * ONLY when `ownerPid` is a positive integer AND `process.kill(ownerPid, 0)`
98
+ * succeeds (the process exists and is signalable by us) — i.e. a swarm is
99
+ * provably still running and holding this worktree. Any throw (ESRCH = gone,
100
+ * EPERM = exists-but-not-ours, EINVAL, etc.) or an absent/invalid pid returns
101
+ * false, falling back to the conservative createdAt-age staleMs guard. NEVER
102
+ * throws and NEVER sends a real signal (signal 0 is error-check-only).
103
+ */
104
+ function ownerAlive(sb) {
105
+ const pid = sb.ownerPid;
106
+ if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) {
107
+ return false;
108
+ }
109
+ try {
110
+ process.kill(pid, 0);
111
+ return true;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Path helpers (re-resolved at call time so tests can relocate HOME)
119
+ // ---------------------------------------------------------------------------
120
+ /** Absolute path to the sandboxes root: ~/.ashlr/sandboxes. Created lazily. */
121
+ export function sandboxesDir() {
122
+ return join(homedir(), '.ashlr', 'sandboxes');
123
+ }
124
+ /** Directory holding one sandbox's worktree + metadata. */
125
+ function sandboxHome(id) {
126
+ return join(sandboxesDir(), id);
127
+ }
128
+ /** Path to a sandbox's persisted metadata file. */
129
+ function metaPath(id) {
130
+ return join(sandboxHome(id), META_FILE);
131
+ }
132
+ /** Path to the isolated worktree checkout for a sandbox. */
133
+ function worktreePathFor(id) {
134
+ return join(sandboxHome(id), 'worktree');
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // git helper — execFile arg arrays, no shell
138
+ // ---------------------------------------------------------------------------
139
+ /**
140
+ * Run a git command inside `cwd`. Throws on failure (callers decide whether
141
+ * to tolerate). Always uses an arg array — never a shell string.
142
+ */
143
+ function gitRun(cwd, args) {
144
+ return execFileSync('git', args, {
145
+ cwd,
146
+ timeout: GIT_TIMEOUT,
147
+ stdio: 'pipe',
148
+ encoding: 'utf8',
149
+ }).trim();
150
+ }
151
+ /** Run a git command, returning null on any error (never throws). */
152
+ function gitTry(cwd, args) {
153
+ try {
154
+ return gitRun(cwd, args);
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // Metadata persistence
162
+ // ---------------------------------------------------------------------------
163
+ function writeMeta(sb) {
164
+ const home = sandboxHome(sb.id);
165
+ if (!existsSync(home)) {
166
+ mkdirSync(home, { recursive: true });
167
+ }
168
+ writeFileSync(metaPath(sb.id), JSON.stringify(sb, null, 2) + '\n', 'utf8');
169
+ }
170
+ /** Read + validate one sandbox's metadata. Returns null on missing/malformed. */
171
+ function readMeta(id) {
172
+ const p = metaPath(id);
173
+ if (!existsSync(p))
174
+ return null;
175
+ try {
176
+ const parsed = JSON.parse(readFileSync(p, 'utf8'));
177
+ if (parsed !== null &&
178
+ typeof parsed === 'object' &&
179
+ !Array.isArray(parsed)) {
180
+ const o = parsed;
181
+ if (typeof o['id'] === 'string' &&
182
+ typeof o['sourceRepo'] === 'string' &&
183
+ typeof o['worktreePath'] === 'string' &&
184
+ typeof o['branch'] === 'string' &&
185
+ typeof o['baseHead'] === 'string' &&
186
+ typeof o['createdAt'] === 'string') {
187
+ // ownerPid is OPTIONAL (back-compat): only carried through when it is a
188
+ // positive integer; anything else (absent/malformed/<=0) is dropped so
189
+ // ownerAlive() falls back to the createdAt-age guard for that sandbox.
190
+ const rawPid = o['ownerPid'];
191
+ const ownerPid = typeof rawPid === 'number' && Number.isInteger(rawPid) && rawPid > 0
192
+ ? rawPid
193
+ : undefined;
194
+ return {
195
+ id: o['id'],
196
+ sourceRepo: o['sourceRepo'],
197
+ worktreePath: o['worktreePath'],
198
+ branch: o['branch'],
199
+ baseHead: o['baseHead'],
200
+ createdAt: o['createdAt'],
201
+ ...(ownerPid !== undefined ? { ownerPid } : {}),
202
+ };
203
+ }
204
+ }
205
+ }
206
+ catch {
207
+ // malformed — treat as absent
208
+ }
209
+ return null;
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // createSandbox
213
+ // ---------------------------------------------------------------------------
214
+ /**
215
+ * Create an isolated git-worktree sandbox of `sourceRepo` on a NEW scratch
216
+ * branch under ~/.ashlr/sandboxes/<id>/.
217
+ *
218
+ * FIRST calls assertMayMutate(sourceRepo, opts) — refuses (throws + audits
219
+ * result:'refused') if the kill switch is on OR the repo is not enrolled and
220
+ * opts.allowAnyRepo is not set. Verifies sourceRepo is a git repo. Reads the
221
+ * source HEAD WITHOUT mutating it. Adds the worktree via
222
+ * `git worktree add -b <branch> <path> <baseHead>` run in sourceRepo — this
223
+ * MUST NOT modify the source working tree, index, HEAD, or any user branch.
224
+ */
225
+ export function createSandbox(sourceRepo, opts) {
226
+ // Gate FIRST — kill switch / enrollment. Audit refusals.
227
+ //
228
+ // H5 CHANGE 3 (env-gate allowAnyRepo) EDIT SITE — integration applies the real
229
+ // edit. The env-gate lives in assertMayMutate (policy.ts) as the single source
230
+ // of truth: `opts.allowAnyRepo` is honored ONLY when ASHLR_TEST_ALLOW_ANY_REPO
231
+ // ==='1' (mirrors advance.ts:156). createSandbox passes opts straight through,
232
+ // so it inherits the gate transitively — NO separate env check is needed here.
233
+ // The kill switch ALWAYS wins (assertMayMutate checks it first, unconditional).
234
+ try {
235
+ assertMayMutate(sourceRepo, opts);
236
+ }
237
+ catch (err) {
238
+ audit({
239
+ action: 'sandbox:create',
240
+ repo: sourceRepo,
241
+ sandboxId: null,
242
+ summary: 'refused by policy gate',
243
+ result: 'refused',
244
+ });
245
+ throw err;
246
+ }
247
+ // Must be a real git repo.
248
+ if (!isRepo(sourceRepo)) {
249
+ audit({
250
+ action: 'sandbox:create',
251
+ repo: sourceRepo,
252
+ sandboxId: null,
253
+ summary: 'sourceRepo is not a git repository',
254
+ result: 'error',
255
+ });
256
+ throw new Error(`not a git repository: ${sourceRepo}`);
257
+ }
258
+ // Read the source HEAD commit WITHOUT mutating it.
259
+ const baseHead = gitTry(sourceRepo, ['rev-parse', 'HEAD']);
260
+ if (!baseHead) {
261
+ audit({
262
+ action: 'sandbox:create',
263
+ repo: sourceRepo,
264
+ sandboxId: null,
265
+ summary: 'could not resolve source HEAD',
266
+ result: 'error',
267
+ });
268
+ throw new Error(`could not resolve HEAD in repo: ${sourceRepo}`);
269
+ }
270
+ // -------------------------------------------------------------------------
271
+ // H5 CHANGE 4 — DISK/COUNT CAP (LOCAL-ONLY resource guard). When the live
272
+ // sandbox count is at/over the cap, FIRST sweep crash-leftover orphans (NEVER a
273
+ // live one — sweepOrphanSandboxes skips any sandbox whose ownerPid is still
274
+ // alive regardless of age, and falls back to the ORPHAN_STALE_MS age guard only
275
+ // for ones with no live owner; so a same-process in-flight worktree held by a
276
+ // concurrent daemon swarm is protected here), then re-count; if STILL at/over
277
+ // the cap, REFUSE (audit result:'refused' + throw a
278
+ // clean error) rather than accumulate unboundedly. This removes NOTHING in-use
279
+ // and opens no outward capability — it only bounds on-disk worktree growth so a
280
+ // pathological crash/restart loop can never fill the disk. The cap is resolved
281
+ // at call time (env-overridable; conservative default) — see maxSandboxes().
282
+ const cap = maxSandboxes();
283
+ if (listSandboxes().length >= cap) {
284
+ // Reclaim genuine crash leftovers first (stale-guarded — a live sandbox is
285
+ // skipped). A healthy install with a transient burst self-heals here.
286
+ sweepOrphanSandboxes({ staleMs: ORPHAN_STALE_MS });
287
+ if (listSandboxes().length >= cap) {
288
+ audit({
289
+ action: 'sandbox:create',
290
+ repo: sourceRepo,
291
+ sandboxId: null,
292
+ summary: `sandbox cap reached (MAX_SANDBOXES=${cap})`,
293
+ result: 'refused',
294
+ });
295
+ throw new Error(`sandbox cap reached (MAX_SANDBOXES=${cap})`);
296
+ }
297
+ }
298
+ // Generate a unique id; the scratch branch contains it.
299
+ const id = randomBytes(6).toString('hex');
300
+ const branch = `${BRANCH_PREFIX}${id}`;
301
+ const worktreePath = worktreePathFor(id);
302
+ // Ensure the per-sandbox home exists (parent of the worktree). The worktree
303
+ // dir itself must NOT pre-exist — git worktree add creates it.
304
+ const home = sandboxHome(id);
305
+ if (!existsSync(home)) {
306
+ mkdirSync(home, { recursive: true });
307
+ }
308
+ // Add the isolated worktree on a NEW scratch branch off baseHead, run IN the
309
+ // source repo. This does NOT touch the source working tree, index, HEAD, or
310
+ // any user branch — it only creates a new ref + a separate checkout.
311
+ try {
312
+ gitRun(sourceRepo, [
313
+ 'worktree',
314
+ 'add',
315
+ '-b',
316
+ branch,
317
+ worktreePath,
318
+ baseHead,
319
+ ]);
320
+ }
321
+ catch (err) {
322
+ // Best-effort cleanup of the partially-created sandbox home, then audit.
323
+ // A partial `worktree add` (ref created, then checkout failed) can leave an
324
+ // orphan scratch branch and/or a dangling worktree registration in the
325
+ // SOURCE repo. Both are namespaced/harmless, but the sandbox invariant is
326
+ // "created, used, then discarded — bounded", so we prune them defensively.
327
+ // Guarded by the BRANCH_PREFIX assertion so we can NEVER `branch -D` a user
328
+ // branch even if `branch` were somehow corrupted.
329
+ try {
330
+ rmSync(home, { recursive: true, force: true });
331
+ }
332
+ catch {
333
+ /* ignore */
334
+ }
335
+ gitTry(sourceRepo, ['worktree', 'prune']);
336
+ if (branch.startsWith(BRANCH_PREFIX)) {
337
+ gitTry(sourceRepo, ['branch', '-D', branch]);
338
+ }
339
+ audit({
340
+ action: 'sandbox:create',
341
+ repo: sourceRepo,
342
+ sandboxId: id,
343
+ summary: 'git worktree add failed',
344
+ result: 'error',
345
+ });
346
+ throw err instanceof Error ? err : new Error(String(err));
347
+ }
348
+ const sb = {
349
+ id,
350
+ sourceRepo,
351
+ worktreePath,
352
+ branch,
353
+ baseHead,
354
+ createdAt: new Date().toISOString(),
355
+ // H5 — stamp the OWNING process pid as a positive liveness marker so the
356
+ // orphan sweep / disk-cap pre-sweep never force-remove this worktree while
357
+ // this process (or any process holding the same pid) is still alive.
358
+ ownerPid: process.pid,
359
+ };
360
+ writeMeta(sb);
361
+ audit({
362
+ action: 'sandbox:create',
363
+ repo: sourceRepo,
364
+ sandboxId: id,
365
+ summary: `worktree on ${branch} @ ${baseHead.slice(0, 8)}`,
366
+ result: 'ok',
367
+ });
368
+ return sb;
369
+ }
370
+ // ---------------------------------------------------------------------------
371
+ // sandboxDiff
372
+ // ---------------------------------------------------------------------------
373
+ /**
374
+ * Capture the git diff of the sandbox worktree vs its base HEAD. Read-only —
375
+ * never mutates the worktree or the source repo. Counts come from --numstat;
376
+ * the unified patch from a plain `git diff <baseHead>` inside the worktree.
377
+ */
378
+ export function sandboxDiff(sb) {
379
+ const cwd = sb.worktreePath;
380
+ // Stage everything (including UNTRACKED new files) against the worktree's OWN
381
+ // index so plain `git diff` captures new files too. `git diff` ignores
382
+ // untracked files by design, and the autonomous write path writes files
383
+ // without `git add`/`commit` — so without this, brand-new files an autonomous
384
+ // run creates would be silently omitted from the captured proposal diff and
385
+ // then DESTROYED by removeSandbox's `worktree remove --force`. Staging in the
386
+ // SANDBOX worktree is safe: it touches only the worktree's index, never the
387
+ // source repo's index/working-tree/HEAD (the worktree has its own index).
388
+ // `add -A` records adds/mods/deletes; we then diff the index vs baseHead.
389
+ gitTry(cwd, ['add', '-A']);
390
+ // numstat: one line per file "<ins>\t<del>\t<path>" (binary => "-\t-\t..").
391
+ // `--staged` diffs the index (now including new files) against baseHead.
392
+ const numstat = gitTry(cwd, ['diff', '--staged', '--numstat', sb.baseHead]) ?? '';
393
+ let files = 0;
394
+ let insertions = 0;
395
+ let deletions = 0;
396
+ for (const line of numstat.split('\n')) {
397
+ const trimmed = line.trim();
398
+ if (!trimmed)
399
+ continue;
400
+ const parts = trimmed.split('\t');
401
+ if (parts.length < 3)
402
+ continue;
403
+ files += 1;
404
+ const ins = parseInt(parts[0] ?? '0', 10);
405
+ const del = parseInt(parts[1] ?? '0', 10);
406
+ if (!Number.isNaN(ins))
407
+ insertions += ins;
408
+ if (!Number.isNaN(del))
409
+ deletions += del;
410
+ }
411
+ // Full unified patch — staged vs baseHead so new files are included.
412
+ const patch = gitTry(cwd, ['diff', '--staged', sb.baseHead]) ?? '';
413
+ return {
414
+ sandboxId: sb.id,
415
+ files,
416
+ insertions,
417
+ deletions,
418
+ patch,
419
+ };
420
+ }
421
+ // ---------------------------------------------------------------------------
422
+ // removeSandbox
423
+ // ---------------------------------------------------------------------------
424
+ /**
425
+ * Remove a sandbox: `git worktree remove --force` then delete the scratch
426
+ * branch (`git branch -D`), both run against the source repo; then clean up
427
+ * persisted metadata. MUST NOT touch the source working tree/index/HEAD/user
428
+ * branches. Idempotent — tolerates an already-removed worktree, never throws
429
+ * on a missing dir.
430
+ */
431
+ export function removeSandbox(sb) {
432
+ const home = sandboxHome(sb.id);
433
+ // ---- Defense-in-depth: NEVER trust on-disk metadata to drive a destructive
434
+ // git op. Metadata can be tampered/corrupted/future-format, so before ANY
435
+ // mutating git call we (a) re-derive the safe branch + worktree path from the
436
+ // sandbox id, (b) require the stored values to match those safe values, and
437
+ // (c) require the branch to be inside our namespace and the worktree path to
438
+ // be contained under sandboxesDir(). A trip on any guard refuses the git ops
439
+ // (audited result:'refused') and falls through to local-dir cleanup only —
440
+ // we still rmSync the sandbox home, but we NEVER run `branch -D` / `worktree
441
+ // remove` against an arbitrary branch/path in an arbitrary repo.
442
+ const safeBranch = `${BRANCH_PREFIX}${sb.id}`;
443
+ const safeWorktree = worktreePathFor(sb.id);
444
+ const sandboxesRoot = sandboxesDir() + sep;
445
+ const branchInNamespace = sb.branch.startsWith(BRANCH_PREFIX);
446
+ const branchMatches = sb.branch === safeBranch;
447
+ const worktreeContained = resolve(sb.worktreePath).startsWith(sandboxesRoot) &&
448
+ resolve(sb.worktreePath) === resolve(safeWorktree);
449
+ const guardsPass = branchInNamespace && branchMatches && worktreeContained;
450
+ if (!guardsPass) {
451
+ audit({
452
+ action: 'sandbox:remove',
453
+ repo: sb.sourceRepo,
454
+ sandboxId: sb.id,
455
+ summary: 'refused git cleanup: metadata failed branch-prefix/containment guard',
456
+ result: 'refused',
457
+ });
458
+ }
459
+ // 1. Remove the worktree registration from the source repo (best-effort).
460
+ // --force handles a dirty / committed-in worktree. Only attempted when the
461
+ // source repo still exists as a git repo AND the guards passed. We target
462
+ // the RE-DERIVED safe values, not the raw metadata.
463
+ if (guardsPass && isRepo(sb.sourceRepo)) {
464
+ // `git worktree remove` also deletes the worktree directory. If the dir is
465
+ // already gone, prune first so git's bookkeeping is consistent, then ignore.
466
+ gitTry(sb.sourceRepo, ['worktree', 'remove', '--force', safeWorktree]);
467
+ gitTry(sb.sourceRepo, ['worktree', 'prune']);
468
+ // 2. Delete the scratch branch (a sandbox-only ref — never a user branch).
469
+ // safeBranch is guaranteed to start with BRANCH_PREFIX.
470
+ gitTry(sb.sourceRepo, ['branch', '-D', safeBranch]);
471
+ }
472
+ // 3. Clean up the per-sandbox home (worktree leftovers + metadata).
473
+ // rmSync with force never throws on a missing path.
474
+ try {
475
+ rmSync(home, { recursive: true, force: true });
476
+ }
477
+ catch {
478
+ // already gone / unremovable — idempotent cleanup, never throw
479
+ }
480
+ audit({
481
+ action: 'sandbox:remove',
482
+ repo: sb.sourceRepo,
483
+ sandboxId: sb.id,
484
+ summary: guardsPass
485
+ ? `removed worktree + branch ${safeBranch}`
486
+ : `local cleanup only (git ops refused) for ${sb.id}`,
487
+ result: 'ok',
488
+ });
489
+ }
490
+ // ---------------------------------------------------------------------------
491
+ // listSandboxes
492
+ // ---------------------------------------------------------------------------
493
+ /**
494
+ * Enumerate persisted sandbox metadata under sandboxesDir(). Returns [] when
495
+ * none. Never throws on a malformed/partial entry (skips it).
496
+ */
497
+ export function listSandboxes() {
498
+ const dir = sandboxesDir();
499
+ if (!existsSync(dir))
500
+ return [];
501
+ let ids;
502
+ try {
503
+ ids = readdirSync(dir, { withFileTypes: true })
504
+ .filter((e) => e.isDirectory())
505
+ .map((e) => e.name);
506
+ }
507
+ catch {
508
+ return [];
509
+ }
510
+ const out = [];
511
+ for (const id of ids) {
512
+ const sb = readMeta(id);
513
+ if (sb)
514
+ out.push(sb);
515
+ }
516
+ return out;
517
+ }
518
+ // ---------------------------------------------------------------------------
519
+ // sweepOrphanSandboxes (H2 — crash-recovery, LOCAL-ONLY)
520
+ // ---------------------------------------------------------------------------
521
+ /**
522
+ * Sweep ORPHANED sandboxes on restart (H2 crash-recovery). A swarm creates a
523
+ * worktree, uses it, then `removeSandbox`es it — all within a single in-memory
524
+ * `runSwarm` call. The sandbox lifetime is NOT persisted on the SwarmRun (there
525
+ * is no `sandboxId` field on a SwarmRun; the only `sandboxId` references are
526
+ * audit-log entries), so a process killed AFTER `git worktree add` but BEFORE
527
+ * `removeSandbox` leaves a worktree on disk under ~/.ashlr/sandboxes/<id>/ with
528
+ * no record linking it back to any swarm. Such a leftover would otherwise
529
+ * accumulate forever.
530
+ *
531
+ * LIVENESS (important — read before wiring a caller): a persisted sandbox is NOT
532
+ * necessarily an orphan. A `runSwarm` keeps its sandbox metadata on disk for the
533
+ * WHOLE run (createSandbox writes it at start, removeSandbox deletes it at the
534
+ * end), and the daemon runs several sandboxed swarms CONCURRENTLY, each with a
535
+ * live on-disk sandbox; a separately-launched `ashlr swarm` in another process is
536
+ * also possible (the re-entrancy guards are per-process). To NEVER force-remove a
537
+ * live worktree, the sweep uses TWO guards, in order:
538
+ *
539
+ * 1. POSITIVE liveness (primary, age-independent): a sandbox whose recorded
540
+ * `ownerPid` is still alive (ownerAlive()) is SKIPPED regardless of age — so
541
+ * a long-running swarm (same- OR cross-process) older than any staleMs is
542
+ * never reclaimed out from under its owner. This closes the central gap that
543
+ * createdAt-age alone could not (there is no wall-clock cap on a swarm).
544
+ * 2. AGE fallback (secondary): for a sandbox with NO usable ownerPid (older
545
+ * metadata, or a crash fixture that models a GONE owner), the OPTIONAL
546
+ * `staleMs` guard applies — a sandbox whose `createdAt` is younger than
547
+ * `staleMs` is SKIPPED (conservatively assumed possibly-live). With NO
548
+ * `staleMs` (the default, used by recovery PROOF tests where every sandbox is
549
+ * a deliberately-dropped orphan with a dead/absent owner) it sweeps every
550
+ * listed sandbox not protected by guard 1.
551
+ *
552
+ * This sweep composes the two EXISTING safe primitives — `listSandboxes()` to
553
+ * enumerate persisted sandbox metadata, then `removeSandbox()` for each — so it
554
+ * inherits all of removeSandbox's containment guards verbatim: it only ever runs
555
+ * `git worktree remove` / `git branch -D` against a path RE-DERIVED to live under
556
+ * `sandboxesDir()` and a branch RE-DERIVED into the `ashlr/sandbox/<id>` namespace
557
+ * (a metadata mismatch falls through to LOCAL dir cleanup only — never a git op on
558
+ * an arbitrary branch/path). It therefore can NEVER touch a user's working tree,
559
+ * index, HEAD, or any user branch.
560
+ *
561
+ * LOCAL-ONLY by construction: it adds NO outward capability (cleanup is purely
562
+ * inward — it removes only ashlr/sandbox/* worktrees + scratch refs, pushes
563
+ * nothing, opens no PR, applies no proposal), weakens NO guard (every removal
564
+ * goes through removeSandbox's full guard set), and changes NO happy-path
565
+ * behavior (a clean run already removes its own sandbox, so a healthy install
566
+ * has nothing to sweep). H5 wires this into daemon start (loop.ts, staleMs=
567
+ * ORPHAN_STALE_MS), the disk-cap pre-sweep above, and `ashlr sandbox gc` — every
568
+ * caller passes a staleMs and is additionally protected by the age-independent
569
+ * ownerAlive() guard, so no LIVE worktree is ever force-removed. Idempotent
570
+ * and never throws — a removal failure on one id is swallowed so the sweep always
571
+ * makes maximal progress. Returns the ids it swept so a caller / test can assert
572
+ * what was reclaimed.
573
+ *
574
+ * @param opts.staleMs Optional age guard (ms). Skip any sandbox whose `createdAt`
575
+ * is younger than this — a conservative liveness proxy so a concurrently-live
576
+ * sandbox is never force-removed. Omit to sweep all listed sandboxes.
577
+ */
578
+ export function sweepOrphanSandboxes(opts) {
579
+ const staleMs = opts?.staleMs;
580
+ const now = Date.now();
581
+ const swept = [];
582
+ for (const sb of listSandboxes()) {
583
+ // GUARD 1 — POSITIVE liveness (age-independent): never reclaim a worktree
584
+ // whose owning process is still alive, even if it is older than staleMs.
585
+ // This is what makes the sweep safe despite the absence of a swarm
586
+ // wall-clock cap — a live in-flight worktree (any process, any age) is
587
+ // protected by its still-alive owner pid.
588
+ if (ownerAlive(sb)) {
589
+ continue;
590
+ }
591
+ // GUARD 2 — AGE fallback for a sandbox with no usable ownerPid (owner gone
592
+ // or older metadata).
593
+ if (staleMs !== undefined) {
594
+ const createdMs = Date.parse(sb.createdAt);
595
+ // Skip not-yet-stale sandboxes (possibly a live owner whose pid we could
596
+ // not read). An unparseable createdAt is treated as stale=0
597
+ // (Number.isNaN) so a corrupt timestamp is still reclaimable rather than
598
+ // stranded forever.
599
+ if (!Number.isNaN(createdMs) && now - createdMs < staleMs) {
600
+ continue;
601
+ }
602
+ }
603
+ try {
604
+ removeSandbox(sb);
605
+ swept.push(sb.id);
606
+ }
607
+ catch {
608
+ // removeSandbox is already best-effort/idempotent and should not throw;
609
+ // guard anyway so one bad entry never aborts the whole restart sweep.
610
+ }
611
+ }
612
+ return swept;
613
+ }
614
+ // ---------------------------------------------------------------------------
615
+ // sweepRepoSandboxes (H7 — scoped one-command rollback cleanup, LOCAL-ONLY)
616
+ // ---------------------------------------------------------------------------
617
+ /**
618
+ * Reclaim the on-disk sandboxes belonging to ONE specific source repo — the
619
+ * scoped cleanup behind `ashlr onboard --rollback <repo>` (H7 BUILD ITEM 4).
620
+ *
621
+ * WHY a distinct sweep (not sweepOrphanSandboxes): the generic orphan sweep is a
622
+ * BACKGROUND restart heuristic — it protects a possibly-live owner whose pid is
623
+ * unreadable by ALSO requiring `createdAt` to be older than `staleMs` (6h). But a
624
+ * crash-leftover worktree from the VERY activation the user is now explicitly
625
+ * undoing is, by definition, fresh (< 6h old), so the generic sweep would SKIP it
626
+ * and the rollback would leave it on disk — contradicting "one-command undo of a
627
+ * first activation". For an explicit, user-requested cleanup of a NAMED repo that
628
+ * the user just unenrolled, the age heuristic is unwarranted.
629
+ *
630
+ * SAFETY (the real boundary is preserved): this drops ONLY the age guard, never
631
+ * the LIVE-OWNER guard. It still SKIPS any sandbox whose recorded `ownerPid` is
632
+ * still alive (ownerAlive()), so a running in-flight swarm's worktree is NEVER
633
+ * force-removed — even a fresh one. It is SCOPED: only sandboxes whose
634
+ * `sourceRepo` resolves to `resolve(repo)` are considered, so it never touches a
635
+ * different repo's sandboxes. Each removal goes through removeSandbox(), so it
636
+ * inherits the full branch-prefix / path-containment guards verbatim and can
637
+ * NEVER touch a user's working tree, index, HEAD, or any user branch.
638
+ *
639
+ * LOCAL-ONLY by construction: adds NO outward capability (inward cleanup only —
640
+ * removes only ashlr/sandbox/* worktrees + scratch refs), weakens NO guard.
641
+ * Idempotent; never throws — a failure on one id is swallowed so the sweep makes
642
+ * maximal progress. Returns the ids it reclaimed.
643
+ *
644
+ * @param repo The source repo whose sandboxes to reclaim (resolved internally).
645
+ */
646
+ export function sweepRepoSandboxes(repo) {
647
+ const target = resolve(repo);
648
+ const swept = [];
649
+ for (const sb of listSandboxes()) {
650
+ // SCOPE: only this repo's sandboxes.
651
+ if (resolve(sb.sourceRepo) !== target) {
652
+ continue;
653
+ }
654
+ // GUARD — POSITIVE liveness (the one guard we KEEP): never reclaim a
655
+ // worktree whose owning process is still alive, regardless of age. A live
656
+ // in-flight swarm (any age) is protected; the age guard is intentionally
657
+ // dropped because this is an explicit user-requested cleanup of a just-
658
+ // unenrolled repo, not a background restart heuristic.
659
+ if (ownerAlive(sb)) {
660
+ continue;
661
+ }
662
+ try {
663
+ removeSandbox(sb);
664
+ swept.push(sb.id);
665
+ }
666
+ catch {
667
+ // removeSandbox is best-effort/idempotent; guard so one bad entry never
668
+ // aborts the whole scoped sweep.
669
+ }
670
+ }
671
+ return swept;
672
+ }
673
+ //# sourceMappingURL=worktree.js.map