@loompad/cli 0.1.0 → 1.1.1

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 (687) hide show
  1. package/ARCHITECTURE.md +96 -11
  2. package/CHANGELOG.md +1042 -0
  3. package/README.md +1473 -44
  4. package/THIRD_PARTY_NOTICES.md +275 -0
  5. package/dist/adapters/antigravity-cli.d.ts +62 -0
  6. package/dist/adapters/antigravity-cli.js +258 -0
  7. package/dist/adapters/antigravity-cli.js.map +1 -0
  8. package/dist/adapters/base.d.ts +61 -9
  9. package/dist/adapters/base.js +266 -15
  10. package/dist/adapters/base.js.map +1 -1
  11. package/dist/adapters/bridges/antigravity.d.ts +9 -12
  12. package/dist/adapters/bridges/antigravity.js +9 -12
  13. package/dist/adapters/bridges/antigravity.js.map +1 -1
  14. package/dist/adapters/echo.d.ts +1 -0
  15. package/dist/adapters/echo.js +59 -3
  16. package/dist/adapters/echo.js.map +1 -1
  17. package/dist/adapters/grok.d.ts +7 -0
  18. package/dist/adapters/grok.js +20 -10
  19. package/dist/adapters/grok.js.map +1 -1
  20. package/dist/adapters/index.d.ts +5 -1
  21. package/dist/adapters/index.js +53 -7
  22. package/dist/adapters/index.js.map +1 -1
  23. package/dist/adapters/model.d.ts +137 -0
  24. package/dist/adapters/model.js +527 -0
  25. package/dist/adapters/model.js.map +1 -0
  26. package/dist/adapters/opencode.d.ts +101 -1
  27. package/dist/adapters/opencode.js +874 -98
  28. package/dist/adapters/opencode.js.map +1 -1
  29. package/dist/cli/doctor.d.ts +32 -0
  30. package/dist/cli/doctor.js +89 -4
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/index.js +2651 -87
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/tui-model.js +3 -1
  35. package/dist/cli/tui-model.js.map +1 -1
  36. package/dist/cli/tui.js +44 -1
  37. package/dist/cli/tui.js.map +1 -1
  38. package/dist/cli/ui.d.ts +2 -0
  39. package/dist/cli/ui.js +13 -0
  40. package/dist/cli/ui.js.map +1 -1
  41. package/dist/core/ades.d.ts +32 -16
  42. package/dist/core/ades.js +57 -20
  43. package/dist/core/ades.js.map +1 -1
  44. package/dist/core/agent-state.d.ts +14 -0
  45. package/dist/core/agent-state.js +32 -0
  46. package/dist/core/agent-state.js.map +1 -0
  47. package/dist/core/approvals.d.ts +33 -0
  48. package/dist/core/approvals.js +29 -0
  49. package/dist/core/approvals.js.map +1 -0
  50. package/dist/core/backup.d.ts +19 -0
  51. package/dist/core/backup.js +86 -0
  52. package/dist/core/backup.js.map +1 -0
  53. package/dist/core/baton.d.ts +2 -2
  54. package/dist/core/baton.js.map +1 -1
  55. package/dist/core/brain-extract.d.ts +17 -0
  56. package/dist/core/brain-extract.js +28 -2
  57. package/dist/core/brain-extract.js.map +1 -1
  58. package/dist/core/brain-index.d.ts +71 -0
  59. package/dist/core/brain-index.js +204 -5
  60. package/dist/core/brain-index.js.map +1 -1
  61. package/dist/core/brain.d.ts +58 -2
  62. package/dist/core/brain.js +79 -1
  63. package/dist/core/brain.js.map +1 -1
  64. package/dist/core/chat-import.d.ts +71 -0
  65. package/dist/core/chat-import.js +419 -0
  66. package/dist/core/chat-import.js.map +1 -0
  67. package/dist/core/checkpoint.d.ts +107 -0
  68. package/dist/core/checkpoint.js +259 -0
  69. package/dist/core/checkpoint.js.map +1 -0
  70. package/dist/core/claude-cli.js +1 -1
  71. package/dist/core/claude-cli.js.map +1 -1
  72. package/dist/core/continuity/artifacts.d.ts +14 -0
  73. package/dist/core/continuity/artifacts.js +118 -0
  74. package/dist/core/continuity/artifacts.js.map +1 -0
  75. package/dist/core/continuity/capabilities.d.ts +65 -0
  76. package/dist/core/continuity/capabilities.js +129 -0
  77. package/dist/core/continuity/capabilities.js.map +1 -0
  78. package/dist/core/continuity/contracts.d.ts +352 -0
  79. package/dist/core/continuity/contracts.js +117 -0
  80. package/dist/core/continuity/contracts.js.map +1 -0
  81. package/dist/core/continuity/engine.d.ts +55 -0
  82. package/dist/core/continuity/engine.js +504 -0
  83. package/dist/core/continuity/engine.js.map +1 -0
  84. package/dist/core/continuity/store.d.ts +73 -0
  85. package/dist/core/continuity/store.js +438 -0
  86. package/dist/core/continuity/store.js.map +1 -0
  87. package/dist/core/conversations.d.ts +19 -0
  88. package/dist/core/conversations.js +87 -0
  89. package/dist/core/conversations.js.map +1 -0
  90. package/dist/core/crew-coordination.d.ts +6 -0
  91. package/dist/core/crew-coordination.js +24 -0
  92. package/dist/core/crew-coordination.js.map +1 -0
  93. package/dist/core/crew-policy.d.ts +18 -0
  94. package/dist/core/crew-policy.js +40 -0
  95. package/dist/core/crew-policy.js.map +1 -0
  96. package/dist/core/crew-protocol.d.ts +64 -0
  97. package/dist/core/crew-protocol.js +146 -0
  98. package/dist/core/crew-protocol.js.map +1 -0
  99. package/dist/core/crew-requests.d.ts +14 -0
  100. package/dist/core/crew-requests.js +22 -0
  101. package/dist/core/crew-requests.js.map +1 -0
  102. package/dist/core/crew.d.ts +395 -0
  103. package/dist/core/crew.js +1857 -0
  104. package/dist/core/crew.js.map +1 -0
  105. package/dist/core/digest.d.ts +40 -0
  106. package/dist/core/digest.js +94 -0
  107. package/dist/core/digest.js.map +1 -0
  108. package/dist/core/distill.js +3 -5
  109. package/dist/core/distill.js.map +1 -1
  110. package/dist/core/eventlog.d.ts +27 -25
  111. package/dist/core/eventlog.js +97 -150
  112. package/dist/core/eventlog.js.map +1 -1
  113. package/dist/core/events/contracts.d.ts +51 -0
  114. package/dist/core/events/contracts.js +2 -0
  115. package/dist/core/events/contracts.js.map +1 -0
  116. package/dist/core/events/jsonl-store.d.ts +23 -0
  117. package/dist/core/events/jsonl-store.js +90 -0
  118. package/dist/core/events/jsonl-store.js.map +1 -0
  119. package/dist/core/events/sqlite-store.d.ts +37 -0
  120. package/dist/core/events/sqlite-store.js +215 -0
  121. package/dist/core/events/sqlite-store.js.map +1 -0
  122. package/dist/core/forecast.d.ts +44 -0
  123. package/dist/core/forecast.js +46 -0
  124. package/dist/core/forecast.js.map +1 -0
  125. package/dist/core/free-pool.d.ts +65 -0
  126. package/dist/core/free-pool.js +171 -0
  127. package/dist/core/free-pool.js.map +1 -0
  128. package/dist/core/git.d.ts +30 -0
  129. package/dist/core/git.js +50 -2
  130. package/dist/core/git.js.map +1 -1
  131. package/dist/core/github-events.d.ts +53 -0
  132. package/dist/core/github-events.js +227 -0
  133. package/dist/core/github-events.js.map +1 -0
  134. package/dist/core/goal-lanes.d.ts +53 -0
  135. package/dist/core/goal-lanes.js +99 -0
  136. package/dist/core/goal-lanes.js.map +1 -0
  137. package/dist/core/hosted.d.ts +27 -0
  138. package/dist/core/hosted.js +50 -0
  139. package/dist/core/hosted.js.map +1 -0
  140. package/dist/core/http-server.d.ts +5 -0
  141. package/dist/core/http-server.js +8 -0
  142. package/dist/core/http-server.js.map +1 -0
  143. package/dist/core/invite-link.d.ts +31 -0
  144. package/dist/core/invite-link.js +45 -0
  145. package/dist/core/invite-link.js.map +1 -0
  146. package/dist/core/loops.d.ts +37 -0
  147. package/dist/core/loops.js +81 -0
  148. package/dist/core/loops.js.map +1 -0
  149. package/dist/core/mcp-catalog.d.ts +182 -0
  150. package/dist/core/mcp-catalog.js +431 -0
  151. package/dist/core/mcp-catalog.js.map +1 -0
  152. package/dist/core/mcp.d.ts +87 -0
  153. package/dist/core/mcp.js +181 -0
  154. package/dist/core/mcp.js.map +1 -0
  155. package/dist/core/memory.d.ts +1 -0
  156. package/dist/core/memory.js +5 -2
  157. package/dist/core/memory.js.map +1 -1
  158. package/dist/core/model-tools.d.ts +97 -0
  159. package/dist/core/model-tools.js +388 -0
  160. package/dist/core/model-tools.js.map +1 -0
  161. package/dist/core/notify.js +19 -1
  162. package/dist/core/notify.js.map +1 -1
  163. package/dist/core/orchestra.d.ts +635 -0
  164. package/dist/core/orchestra.js +2120 -0
  165. package/dist/core/orchestra.js.map +1 -0
  166. package/dist/core/permissions.d.ts +56 -0
  167. package/dist/core/permissions.js +122 -0
  168. package/dist/core/permissions.js.map +1 -0
  169. package/dist/core/preview-proxy.d.ts +70 -0
  170. package/dist/core/preview-proxy.js +563 -0
  171. package/dist/core/preview-proxy.js.map +1 -0
  172. package/dist/core/preview-shot.d.ts +46 -0
  173. package/dist/core/preview-shot.js +109 -0
  174. package/dist/core/preview-shot.js.map +1 -0
  175. package/dist/core/process.d.ts +36 -0
  176. package/dist/core/process.js +168 -0
  177. package/dist/core/process.js.map +1 -0
  178. package/dist/core/prompt-queue.d.ts +136 -0
  179. package/dist/core/prompt-queue.js +269 -0
  180. package/dist/core/prompt-queue.js.map +1 -0
  181. package/dist/core/prompts.d.ts +53 -0
  182. package/dist/core/prompts.js +127 -0
  183. package/dist/core/prompts.js.map +1 -0
  184. package/dist/core/proof.d.ts +82 -0
  185. package/dist/core/proof.js +216 -0
  186. package/dist/core/proof.js.map +1 -0
  187. package/dist/core/provider-text.d.ts +23 -0
  188. package/dist/core/provider-text.js +66 -0
  189. package/dist/core/provider-text.js.map +1 -0
  190. package/dist/core/providers.d.ts +152 -0
  191. package/dist/core/providers.js +343 -0
  192. package/dist/core/providers.js.map +1 -0
  193. package/dist/core/quota.d.ts +33 -0
  194. package/dist/core/quota.js +74 -0
  195. package/dist/core/quota.js.map +1 -0
  196. package/dist/core/recipes.d.ts +50 -0
  197. package/dist/core/recipes.js +99 -0
  198. package/dist/core/recipes.js.map +1 -0
  199. package/dist/core/registry.d.ts +54 -0
  200. package/dist/core/registry.js +62 -5
  201. package/dist/core/registry.js.map +1 -1
  202. package/dist/core/relay-client.d.ts +67 -0
  203. package/dist/core/relay-client.js +184 -0
  204. package/dist/core/relay-client.js.map +1 -0
  205. package/dist/core/relay-protocol.d.ts +140 -0
  206. package/dist/core/relay-protocol.js +189 -0
  207. package/dist/core/relay-protocol.js.map +1 -0
  208. package/dist/core/replay.d.ts +18 -0
  209. package/dist/core/replay.js +191 -0
  210. package/dist/core/replay.js.map +1 -0
  211. package/dist/core/review-triage.d.ts +41 -0
  212. package/dist/core/review-triage.js +124 -0
  213. package/dist/core/review-triage.js.map +1 -0
  214. package/dist/core/router.js +2 -2
  215. package/dist/core/router.js.map +1 -1
  216. package/dist/core/routes.d.ts +39 -3
  217. package/dist/core/routes.js +137 -4
  218. package/dist/core/routes.js.map +1 -1
  219. package/dist/core/run-command.d.ts +14 -0
  220. package/dist/core/run-command.js +45 -0
  221. package/dist/core/run-command.js.map +1 -0
  222. package/dist/core/runner-setup.d.ts +36 -0
  223. package/dist/core/runner-setup.js +114 -0
  224. package/dist/core/runner-setup.js.map +1 -0
  225. package/dist/core/search.js +0 -0
  226. package/dist/core/search.js.map +1 -1
  227. package/dist/core/semantic.d.ts +92 -0
  228. package/dist/core/semantic.js +227 -0
  229. package/dist/core/semantic.js.map +1 -0
  230. package/dist/core/servers.d.ts +115 -0
  231. package/dist/core/servers.js +305 -0
  232. package/dist/core/servers.js.map +1 -0
  233. package/dist/core/setup.js +64 -6
  234. package/dist/core/setup.js.map +1 -1
  235. package/dist/core/shield.d.ts +17 -0
  236. package/dist/core/shield.js +81 -0
  237. package/dist/core/shield.js.map +1 -0
  238. package/dist/core/shift.d.ts +69 -0
  239. package/dist/core/shift.js +120 -0
  240. package/dist/core/shift.js.map +1 -0
  241. package/dist/core/skill-install.d.ts +125 -0
  242. package/dist/core/skill-install.js +328 -0
  243. package/dist/core/skill-install.js.map +1 -0
  244. package/dist/core/skills.d.ts +116 -0
  245. package/dist/core/skills.js +223 -0
  246. package/dist/core/skills.js.map +1 -0
  247. package/dist/core/step-conditions.d.ts +65 -0
  248. package/dist/core/step-conditions.js +102 -0
  249. package/dist/core/step-conditions.js.map +1 -0
  250. package/dist/core/subagents.d.ts +45 -0
  251. package/dist/core/subagents.js +90 -0
  252. package/dist/core/subagents.js.map +1 -0
  253. package/dist/core/team-canon.d.ts +42 -0
  254. package/dist/core/team-canon.js +121 -0
  255. package/dist/core/team-canon.js.map +1 -0
  256. package/dist/core/team-crypto.d.ts +97 -0
  257. package/dist/core/team-crypto.js +175 -0
  258. package/dist/core/team-crypto.js.map +1 -0
  259. package/dist/core/team-hub.d.ts +514 -0
  260. package/dist/core/team-hub.js +964 -0
  261. package/dist/core/team-hub.js.map +1 -0
  262. package/dist/core/team-landing.d.ts +239 -0
  263. package/dist/core/team-landing.js +504 -0
  264. package/dist/core/team-landing.js.map +1 -0
  265. package/dist/core/team-leases.d.ts +44 -0
  266. package/dist/core/team-leases.js +145 -0
  267. package/dist/core/team-leases.js.map +1 -0
  268. package/dist/core/team-memory.d.ts +42 -0
  269. package/dist/core/team-memory.js +84 -0
  270. package/dist/core/team-memory.js.map +1 -0
  271. package/dist/core/team-policy.d.ts +85 -0
  272. package/dist/core/team-policy.js +191 -0
  273. package/dist/core/team-policy.js.map +1 -0
  274. package/dist/core/thread-md.d.ts +6 -0
  275. package/dist/core/thread-md.js +42 -0
  276. package/dist/core/thread-md.js.map +1 -0
  277. package/dist/core/turn-stats.d.ts +61 -0
  278. package/dist/core/turn-stats.js +124 -0
  279. package/dist/core/turn-stats.js.map +1 -0
  280. package/dist/core/updater.d.ts +73 -0
  281. package/dist/core/updater.js +153 -0
  282. package/dist/core/updater.js.map +1 -0
  283. package/dist/core/usage.d.ts +75 -0
  284. package/dist/core/usage.js +163 -0
  285. package/dist/core/usage.js.map +1 -0
  286. package/dist/core/windows-jobs.d.ts +15 -0
  287. package/dist/core/windows-jobs.js +170 -0
  288. package/dist/core/windows-jobs.js.map +1 -0
  289. package/dist/core/worktree-merge.d.ts +57 -0
  290. package/dist/core/worktree-merge.js +123 -0
  291. package/dist/core/worktree-merge.js.map +1 -0
  292. package/dist/core/worktree.d.ts +5 -0
  293. package/dist/core/worktree.js +45 -5
  294. package/dist/core/worktree.js.map +1 -1
  295. package/dist/core/worktrees.d.ts +6 -0
  296. package/dist/core/worktrees.js +21 -0
  297. package/dist/core/worktrees.js.map +1 -0
  298. package/dist/daemon/app-page.d.ts +0 -17
  299. package/dist/daemon/app-page.js +10 -6346
  300. package/dist/daemon/app-page.js.map +1 -1
  301. package/dist/daemon/auth.d.ts +24 -3
  302. package/dist/daemon/auth.js +66 -7
  303. package/dist/daemon/auth.js.map +1 -1
  304. package/dist/daemon/board.d.ts +7 -0
  305. package/dist/daemon/board.js +12 -1
  306. package/dist/daemon/board.js.map +1 -1
  307. package/dist/daemon/brand-icons.d.ts +6 -1
  308. package/dist/daemon/brand-icons.js +11 -3
  309. package/dist/daemon/brand-icons.js.map +1 -1
  310. package/dist/daemon/client.d.ts +665 -1
  311. package/dist/daemon/client.js +381 -3
  312. package/dist/daemon/client.js.map +1 -1
  313. package/dist/daemon/delivery.d.ts +34 -0
  314. package/dist/daemon/delivery.js +63 -0
  315. package/dist/daemon/delivery.js.map +1 -0
  316. package/dist/daemon/deploys.d.ts +62 -0
  317. package/dist/daemon/deploys.js +159 -0
  318. package/dist/daemon/deploys.js.map +1 -0
  319. package/dist/daemon/landing.d.ts +192 -0
  320. package/dist/daemon/landing.js +992 -0
  321. package/dist/daemon/landing.js.map +1 -0
  322. package/dist/daemon/onboard.d.ts +150 -0
  323. package/dist/daemon/onboard.js +390 -0
  324. package/dist/daemon/onboard.js.map +1 -0
  325. package/dist/daemon/proof.d.ts +70 -0
  326. package/dist/daemon/proof.js +172 -0
  327. package/dist/daemon/proof.js.map +1 -0
  328. package/dist/daemon/push.d.ts +19 -1
  329. package/dist/daemon/push.js +90 -3
  330. package/dist/daemon/push.js.map +1 -1
  331. package/dist/daemon/relay.d.ts +82 -0
  332. package/dist/daemon/relay.js +351 -0
  333. package/dist/daemon/relay.js.map +1 -0
  334. package/dist/daemon/routes/agent-discovery.d.ts +4 -0
  335. package/dist/daemon/routes/agent-discovery.js +29 -0
  336. package/dist/daemon/routes/agent-discovery.js.map +1 -0
  337. package/dist/daemon/routes/agent-settings.d.ts +4 -0
  338. package/dist/daemon/routes/agent-settings.js +94 -0
  339. package/dist/daemon/routes/agent-settings.js.map +1 -0
  340. package/dist/daemon/routes/agents.d.ts +4 -0
  341. package/dist/daemon/routes/agents.js +105 -0
  342. package/dist/daemon/routes/agents.js.map +1 -0
  343. package/dist/daemon/routes/alerts.d.ts +4 -0
  344. package/dist/daemon/routes/alerts.js +137 -0
  345. package/dist/daemon/routes/alerts.js.map +1 -0
  346. package/dist/daemon/routes/approvals.d.ts +4 -0
  347. package/dist/daemon/routes/approvals.js +40 -0
  348. package/dist/daemon/routes/approvals.js.map +1 -0
  349. package/dist/daemon/routes/assets.d.ts +3 -0
  350. package/dist/daemon/routes/assets.js +64 -0
  351. package/dist/daemon/routes/assets.js.map +1 -0
  352. package/dist/daemon/routes/board.d.ts +4 -0
  353. package/dist/daemon/routes/board.js +147 -0
  354. package/dist/daemon/routes/board.js.map +1 -0
  355. package/dist/daemon/routes/brain.d.ts +4 -0
  356. package/dist/daemon/routes/brain.js +270 -0
  357. package/dist/daemon/routes/brain.js.map +1 -0
  358. package/dist/daemon/routes/chats.d.ts +4 -0
  359. package/dist/daemon/routes/chats.js +249 -0
  360. package/dist/daemon/routes/chats.js.map +1 -0
  361. package/dist/daemon/routes/clients.d.ts +4 -0
  362. package/dist/daemon/routes/clients.js +49 -0
  363. package/dist/daemon/routes/clients.js.map +1 -0
  364. package/dist/daemon/routes/cloud.d.ts +4 -0
  365. package/dist/daemon/routes/cloud.js +46 -0
  366. package/dist/daemon/routes/cloud.js.map +1 -0
  367. package/dist/daemon/routes/config.d.ts +4 -0
  368. package/dist/daemon/routes/config.js +35 -0
  369. package/dist/daemon/routes/config.js.map +1 -0
  370. package/dist/daemon/routes/context.d.ts +51 -0
  371. package/dist/daemon/routes/context.js +2 -0
  372. package/dist/daemon/routes/context.js.map +1 -0
  373. package/dist/daemon/routes/crews.d.ts +4 -0
  374. package/dist/daemon/routes/crews.js +149 -0
  375. package/dist/daemon/routes/crews.js.map +1 -0
  376. package/dist/daemon/routes/files.d.ts +3 -0
  377. package/dist/daemon/routes/files.js +253 -0
  378. package/dist/daemon/routes/files.js.map +1 -0
  379. package/dist/daemon/routes/git.d.ts +4 -0
  380. package/dist/daemon/routes/git.js +96 -0
  381. package/dist/daemon/routes/git.js.map +1 -0
  382. package/dist/daemon/routes/history.d.ts +4 -0
  383. package/dist/daemon/routes/history.js +101 -0
  384. package/dist/daemon/routes/history.js.map +1 -0
  385. package/dist/daemon/routes/integrations-project.d.ts +3 -0
  386. package/dist/daemon/routes/integrations-project.js +103 -0
  387. package/dist/daemon/routes/integrations-project.js.map +1 -0
  388. package/dist/daemon/routes/integrations.d.ts +3 -0
  389. package/dist/daemon/routes/integrations.js +111 -0
  390. package/dist/daemon/routes/integrations.js.map +1 -0
  391. package/dist/daemon/routes/logs.d.ts +3 -0
  392. package/dist/daemon/routes/logs.js +40 -0
  393. package/dist/daemon/routes/logs.js.map +1 -0
  394. package/dist/daemon/routes/messages.d.ts +4 -0
  395. package/dist/daemon/routes/messages.js +60 -0
  396. package/dist/daemon/routes/messages.js.map +1 -0
  397. package/dist/daemon/routes/observability.d.ts +4 -0
  398. package/dist/daemon/routes/observability.js +247 -0
  399. package/dist/daemon/routes/observability.js.map +1 -0
  400. package/dist/daemon/routes/onboard.d.ts +12 -0
  401. package/dist/daemon/routes/onboard.js +67 -0
  402. package/dist/daemon/routes/onboard.js.map +1 -0
  403. package/dist/daemon/routes/orchestra.d.ts +4 -0
  404. package/dist/daemon/routes/orchestra.js +110 -0
  405. package/dist/daemon/routes/orchestra.js.map +1 -0
  406. package/dist/daemon/routes/pairing.d.ts +4 -0
  407. package/dist/daemon/routes/pairing.js +155 -0
  408. package/dist/daemon/routes/pairing.js.map +1 -0
  409. package/dist/daemon/routes/preview.d.ts +24 -0
  410. package/dist/daemon/routes/preview.js +122 -0
  411. package/dist/daemon/routes/preview.js.map +1 -0
  412. package/dist/daemon/routes/project-actions.d.ts +4 -0
  413. package/dist/daemon/routes/project-actions.js +53 -0
  414. package/dist/daemon/routes/project-actions.js.map +1 -0
  415. package/dist/daemon/routes/project-team.d.ts +4 -0
  416. package/dist/daemon/routes/project-team.js +201 -0
  417. package/dist/daemon/routes/project-team.js.map +1 -0
  418. package/dist/daemon/routes/projects.d.ts +14 -0
  419. package/dist/daemon/routes/projects.js +138 -0
  420. package/dist/daemon/routes/projects.js.map +1 -0
  421. package/dist/daemon/routes/prompts.d.ts +4 -0
  422. package/dist/daemon/routes/prompts.js +44 -0
  423. package/dist/daemon/routes/prompts.js.map +1 -0
  424. package/dist/daemon/routes/proof.d.ts +4 -0
  425. package/dist/daemon/routes/proof.js +22 -0
  426. package/dist/daemon/routes/proof.js.map +1 -0
  427. package/dist/daemon/routes/providers.d.ts +3 -0
  428. package/dist/daemon/routes/providers.js +60 -0
  429. package/dist/daemon/routes/providers.js.map +1 -0
  430. package/dist/daemon/routes/public.d.ts +4 -0
  431. package/dist/daemon/routes/public.js +64 -0
  432. package/dist/daemon/routes/public.js.map +1 -0
  433. package/dist/daemon/routes/queue.d.ts +4 -0
  434. package/dist/daemon/routes/queue.js +140 -0
  435. package/dist/daemon/routes/queue.js.map +1 -0
  436. package/dist/daemon/routes/routing.d.ts +4 -0
  437. package/dist/daemon/routes/routing.js +61 -0
  438. package/dist/daemon/routes/routing.js.map +1 -0
  439. package/dist/daemon/routes/runtime-handler.d.ts +2 -0
  440. package/dist/daemon/routes/runtime-handler.js +63 -0
  441. package/dist/daemon/routes/runtime-handler.js.map +1 -0
  442. package/dist/daemon/routes/search.d.ts +4 -0
  443. package/dist/daemon/routes/search.js +17 -0
  444. package/dist/daemon/routes/search.js.map +1 -0
  445. package/dist/daemon/routes/servers.d.ts +4 -0
  446. package/dist/daemon/routes/servers.js +133 -0
  447. package/dist/daemon/routes/servers.js.map +1 -0
  448. package/dist/daemon/routes/setup.d.ts +3 -0
  449. package/dist/daemon/routes/setup.js +50 -0
  450. package/dist/daemon/routes/setup.js.map +1 -0
  451. package/dist/daemon/routes/shift.d.ts +4 -0
  452. package/dist/daemon/routes/shift.js +30 -0
  453. package/dist/daemon/routes/shift.js.map +1 -0
  454. package/dist/daemon/routes/specs.d.ts +4 -0
  455. package/dist/daemon/routes/specs.js +57 -0
  456. package/dist/daemon/routes/specs.js.map +1 -0
  457. package/dist/daemon/routes/task-delivery.d.ts +4 -0
  458. package/dist/daemon/routes/task-delivery.js +38 -0
  459. package/dist/daemon/routes/task-delivery.js.map +1 -0
  460. package/dist/daemon/routes/team.d.ts +4 -0
  461. package/dist/daemon/routes/team.js +193 -0
  462. package/dist/daemon/routes/team.js.map +1 -0
  463. package/dist/daemon/routes/terminals.d.ts +4 -0
  464. package/dist/daemon/routes/terminals.js +87 -0
  465. package/dist/daemon/routes/terminals.js.map +1 -0
  466. package/dist/daemon/routes/tools.d.ts +4 -0
  467. package/dist/daemon/routes/tools.js +180 -0
  468. package/dist/daemon/routes/tools.js.map +1 -0
  469. package/dist/daemon/routes/updates.d.ts +4 -0
  470. package/dist/daemon/routes/updates.js +89 -0
  471. package/dist/daemon/routes/updates.js.map +1 -0
  472. package/dist/daemon/routes/voice.d.ts +4 -0
  473. package/dist/daemon/routes/voice.js +53 -0
  474. package/dist/daemon/routes/voice.js.map +1 -0
  475. package/dist/daemon/runner.d.ts +186 -0
  476. package/dist/daemon/runner.js +612 -0
  477. package/dist/daemon/runner.js.map +1 -0
  478. package/dist/daemon/runtime/accounting.d.ts +130 -0
  479. package/dist/daemon/runtime/accounting.js +303 -0
  480. package/dist/daemon/runtime/accounting.js.map +1 -0
  481. package/dist/daemon/runtime/agents.d.ts +16 -0
  482. package/dist/daemon/runtime/agents.js +71 -0
  483. package/dist/daemon/runtime/agents.js.map +1 -0
  484. package/dist/daemon/runtime/briefings.d.ts +134 -0
  485. package/dist/daemon/runtime/briefings.js +329 -0
  486. package/dist/daemon/runtime/briefings.js.map +1 -0
  487. package/dist/daemon/runtime/native-usage.d.ts +21 -0
  488. package/dist/daemon/runtime/native-usage.js +71 -0
  489. package/dist/daemon/runtime/native-usage.js.map +1 -0
  490. package/dist/daemon/runtime/queue.d.ts +94 -0
  491. package/dist/daemon/runtime/queue.js +222 -0
  492. package/dist/daemon/runtime/queue.js.map +1 -0
  493. package/dist/daemon/runtime/relay.d.ts +93 -0
  494. package/dist/daemon/runtime/relay.js +240 -0
  495. package/dist/daemon/runtime/relay.js.map +1 -0
  496. package/dist/daemon/runtime/turns.d.ts +202 -0
  497. package/dist/daemon/runtime/turns.js +663 -0
  498. package/dist/daemon/runtime/turns.js.map +1 -0
  499. package/dist/daemon/runtime-support.d.ts +76 -0
  500. package/dist/daemon/runtime-support.js +134 -0
  501. package/dist/daemon/runtime-support.js.map +1 -0
  502. package/dist/daemon/runtime.d.ts +739 -70
  503. package/dist/daemon/runtime.js +2751 -470
  504. package/dist/daemon/runtime.js.map +1 -1
  505. package/dist/daemon/server.d.ts +82 -55
  506. package/dist/daemon/server.js +674 -1435
  507. package/dist/daemon/server.js.map +1 -1
  508. package/dist/daemon/shift.d.ts +70 -0
  509. package/dist/daemon/shift.js +170 -0
  510. package/dist/daemon/shift.js.map +1 -0
  511. package/dist/daemon/specs.d.ts +92 -0
  512. package/dist/daemon/specs.js +285 -0
  513. package/dist/daemon/specs.js.map +1 -0
  514. package/dist/daemon/system.d.ts +208 -0
  515. package/dist/daemon/system.js +557 -0
  516. package/dist/daemon/system.js.map +1 -0
  517. package/dist/daemon/tasks.js +1 -1
  518. package/dist/daemon/team-brain.d.ts +136 -0
  519. package/dist/daemon/team-brain.js +659 -0
  520. package/dist/daemon/team-brain.js.map +1 -0
  521. package/dist/daemon/team-coordinator.d.ts +78 -0
  522. package/dist/daemon/team-coordinator.js +408 -0
  523. package/dist/daemon/team-coordinator.js.map +1 -0
  524. package/dist/daemon/team.d.ts +339 -0
  525. package/dist/daemon/team.js +1433 -0
  526. package/dist/daemon/team.js.map +1 -0
  527. package/dist/daemon/terminals.d.ts +22 -3
  528. package/dist/daemon/terminals.js +236 -16
  529. package/dist/daemon/terminals.js.map +1 -1
  530. package/dist/hub/client.d.ts +120 -0
  531. package/dist/hub/client.js +228 -0
  532. package/dist/hub/client.js.map +1 -0
  533. package/dist/hub/server.d.ts +32 -0
  534. package/dist/hub/server.js +180 -0
  535. package/dist/hub/server.js.map +1 -0
  536. package/dist/hub/supabase-client.d.ts +220 -0
  537. package/dist/hub/supabase-client.js +843 -0
  538. package/dist/hub/supabase-client.js.map +1 -0
  539. package/dist/observability/ask.d.ts +91 -0
  540. package/dist/observability/ask.js +179 -0
  541. package/dist/observability/ask.js.map +1 -0
  542. package/dist/observability/clickhouse.d.ts +33 -0
  543. package/dist/observability/clickhouse.js +48 -0
  544. package/dist/observability/clickhouse.js.map +1 -0
  545. package/dist/observability/decisions.d.ts +176 -0
  546. package/dist/observability/decisions.js +377 -0
  547. package/dist/observability/decisions.js.map +1 -0
  548. package/dist/observability/index.d.ts +89 -0
  549. package/dist/observability/index.js +348 -0
  550. package/dist/observability/index.js.map +1 -0
  551. package/dist/observability/insights.d.ts +151 -0
  552. package/dist/observability/insights.js +319 -0
  553. package/dist/observability/insights.js.map +1 -0
  554. package/dist/observability/logs-query.d.ts +82 -0
  555. package/dist/observability/logs-query.js +102 -0
  556. package/dist/observability/logs-query.js.map +1 -0
  557. package/dist/observability/logs.d.ts +72 -0
  558. package/dist/observability/logs.js +295 -0
  559. package/dist/observability/logs.js.map +1 -0
  560. package/dist/observability/metrics.d.ts +86 -0
  561. package/dist/observability/metrics.js +303 -0
  562. package/dist/observability/metrics.js.map +1 -0
  563. package/dist/observability/otlp.d.ts +127 -0
  564. package/dist/observability/otlp.js +242 -0
  565. package/dist/observability/otlp.js.map +1 -0
  566. package/dist/observability/snapshots.d.ts +43 -0
  567. package/dist/observability/snapshots.js +129 -0
  568. package/dist/observability/snapshots.js.map +1 -0
  569. package/dist/observability/triage.d.ts +49 -0
  570. package/dist/observability/triage.js +284 -0
  571. package/dist/observability/triage.js.map +1 -0
  572. package/dist/providers/adapter.d.ts +50 -0
  573. package/dist/providers/adapter.js +38 -0
  574. package/dist/providers/adapter.js.map +1 -0
  575. package/dist/providers/agent.d.ts +110 -0
  576. package/dist/providers/agent.js +487 -0
  577. package/dist/providers/agent.js.map +1 -0
  578. package/dist/providers/approvals.d.ts +24 -0
  579. package/dist/providers/approvals.js +78 -0
  580. package/dist/providers/approvals.js.map +1 -0
  581. package/dist/providers/attachments.d.ts +20 -0
  582. package/dist/providers/attachments.js +51 -0
  583. package/dist/providers/attachments.js.map +1 -0
  584. package/dist/providers/claude/adapter.d.ts +99 -0
  585. package/dist/providers/claude/adapter.js +790 -0
  586. package/dist/providers/claude/adapter.js.map +1 -0
  587. package/dist/providers/codex/adapter.d.ts +113 -0
  588. package/dist/providers/codex/adapter.js +881 -0
  589. package/dist/providers/codex/adapter.js.map +1 -0
  590. package/dist/providers/codex/instructions.d.ts +11 -0
  591. package/dist/providers/codex/instructions.js +154 -0
  592. package/dist/providers/codex/instructions.js.map +1 -0
  593. package/dist/providers/codex/rpc.d.ts +36 -0
  594. package/dist/providers/codex/rpc.js +104 -0
  595. package/dist/providers/codex/rpc.js.map +1 -0
  596. package/dist/providers/contracts.d.ts +339 -0
  597. package/dist/providers/contracts.js +19 -0
  598. package/dist/providers/contracts.js.map +1 -0
  599. package/dist/providers/directory.d.ts +70 -0
  600. package/dist/providers/directory.js +95 -0
  601. package/dist/providers/directory.js.map +1 -0
  602. package/dist/providers/errors.d.ts +47 -0
  603. package/dist/providers/errors.js +24 -0
  604. package/dist/providers/errors.js.map +1 -0
  605. package/dist/providers/index.d.ts +18 -0
  606. package/dist/providers/index.js +19 -0
  607. package/dist/providers/index.js.map +1 -0
  608. package/dist/providers/ingestion.d.ts +80 -0
  609. package/dist/providers/ingestion.js +330 -0
  610. package/dist/providers/ingestion.js.map +1 -0
  611. package/dist/providers/live.d.ts +22 -0
  612. package/dist/providers/live.js +56 -0
  613. package/dist/providers/live.js.map +1 -0
  614. package/dist/providers/process.d.ts +29 -0
  615. package/dist/providers/process.js +71 -0
  616. package/dist/providers/process.js.map +1 -0
  617. package/dist/providers/reaper.d.ts +33 -0
  618. package/dist/providers/reaper.js +65 -0
  619. package/dist/providers/reaper.js.map +1 -0
  620. package/dist/providers/service.d.ts +87 -0
  621. package/dist/providers/service.js +256 -0
  622. package/dist/providers/service.js.map +1 -0
  623. package/dist/sdk.d.ts +1 -1
  624. package/dist/sdk.js +1 -1
  625. package/dist/sdk.js.map +1 -1
  626. package/dist/types.d.ts +385 -6
  627. package/dist/types.js +1 -0
  628. package/dist/types.js.map +1 -1
  629. package/dist/version.d.ts +7 -0
  630. package/dist/version.js +16 -0
  631. package/dist/version.js.map +1 -0
  632. package/dist/web/app.css +4643 -0
  633. package/dist/web/app.js +22640 -0
  634. package/dist/web/app.js.map +7 -0
  635. package/dist/web/shell.html +39 -0
  636. package/docs/README.md +21 -0
  637. package/docs/SETUP.md +10 -0
  638. package/docs/adapters.md +21 -8
  639. package/docs/brain-continuity.md +186 -0
  640. package/docs/cloud.md +108 -0
  641. package/docs/crews.md +62 -0
  642. package/docs/design-system.md +31 -2
  643. package/docs/integration-notes.md +128 -20
  644. package/docs/loompad.md +105 -0
  645. package/docs/observability.md +220 -0
  646. package/docs/play-store.md +123 -0
  647. package/docs/proposals/agent-teams.md +249 -0
  648. package/docs/proposals/brain-continuity-implementation.md +468 -0
  649. package/docs/proposals/brain-session-continuity.md +623 -0
  650. package/docs/proposals/t3code-port.md +81 -0
  651. package/docs/proposals/t3code-reference.md +95 -0
  652. package/docs/refactoring/AGENT-TEAMS-PROGRESS.md +88 -0
  653. package/docs/refactoring/BOUNDARIES.md +120 -0
  654. package/docs/refactoring/BRAIN-AUDIT.md +176 -0
  655. package/docs/refactoring/BRAIN-NOTES.md +390 -0
  656. package/docs/refactoring/BRAIN-TODO.md +214 -0
  657. package/docs/refactoring/NOTES.md +129 -0
  658. package/docs/refactoring/T3-PORT-NOTES.md +165 -0
  659. package/docs/refactoring/T3-PORT-TODO.md +123 -0
  660. package/docs/refactoring/TODO.md +46 -0
  661. package/docs/refactoring/brain-benchmark-2026-09-28.json +48 -0
  662. package/docs/refactoring/brain-benchmark-audit-2026-09-28.json +48 -0
  663. package/docs/release-notes/v0.2.0.md +35 -0
  664. package/docs/release-notes/v0.2.1.md +59 -0
  665. package/docs/release-notes/v0.2.3.md +77 -0
  666. package/docs/release-notes/v0.2.4.md +128 -0
  667. package/docs/release-notes/v0.2.5.md +153 -0
  668. package/docs/release-notes/v0.2.6.md +62 -0
  669. package/docs/release-notes/v1.0.0.md +120 -0
  670. package/docs/release-notes/v1.0.1.md +77 -0
  671. package/docs/release-notes/v1.1.0.md +71 -0
  672. package/docs/release-notes/v1.1.1.md +38 -0
  673. package/docs/teams-architecture.md +697 -0
  674. package/docs/teams.md +171 -0
  675. package/docs/windows.md +85 -0
  676. package/package.json +22 -9
  677. package/dist/adapters/claude-code.d.ts +0 -26
  678. package/dist/adapters/claude-code.js +0 -209
  679. package/dist/adapters/claude-code.js.map +0 -1
  680. package/dist/adapters/codex.d.ts +0 -51
  681. package/dist/adapters/codex.js +0 -291
  682. package/dist/adapters/codex.js.map +0 -1
  683. package/docs/img/board-linear.png +0 -0
  684. package/docs/img/board-projects.png +0 -0
  685. package/docs/img/brain.png +0 -0
  686. package/docs/img/settings.png +0 -0
  687. package/docs/img/workspace.png +0 -0
@@ -0,0 +1,992 @@
1
+ /**
2
+ * Landing — one project's goal PRs on their way to main (Loom Teams, Phase 4,
3
+ * "land safely"; docs/teams-architecture.md D52–D64).
4
+ *
5
+ * The owner's daemon watches its own goal PRs (D52) and, per PR:
6
+ * - reruns a failing required check once; a pass on rerun is a flake (D53)
7
+ * - hands a real failure's log tail to the goal's orchestrator, which reopens
8
+ * the goal and routes a fix; at most `autoFixAttempts` (D54, D55)
9
+ * - runs a cross-vendor review on open and after each fix, at most 3 times,
10
+ * posting a COMMENT review and the `loom/review` status (D60, D61)
11
+ * - when the owner clicks Land: fresh main in, fast tests, push, auto-merge
12
+ * (D56–D58); stacks land bottom-up (D59)
13
+ * - says when a goal needs someone, adopts a teammate's goal on request, and
14
+ * hands it back when green (D63)
15
+ * - Phase 6, on a repo with no merge queue: the landing train (D20, D79–D82).
16
+ * Land queues the goal in its lanes (path scopes from `landing.lanes`); the
17
+ * slot is a hub lease on `.loom/landing/<lane>`, a hard zone, so one goal per
18
+ * lane holds it. The holder brings fresh base in, runs the fast tests,
19
+ * pushes, waits for green on that head, and merges; a red check gives the
20
+ * slot back and the goal requeues itself once it's green again.
21
+ * - Phase 6: a feed event about one of its PRs (a webhook's check result, a
22
+ * merge) polls that goal now, not at the next 30s tick (D84)
23
+ *
24
+ * Everything outside git and the hub goes through `gh`, injectable for tests.
25
+ */
26
+ import { execFile } from "../core/process.js";
27
+ import fs from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import { logbook } from "../core/logbook.js";
31
+ import { notify } from "../core/notify.js";
32
+ import { isTerminal, newLanding } from "../core/orchestra.js";
33
+ import { sealForTeam } from "../core/team-crypto.js";
34
+ import { addMergeGroupTrigger, doctor, failureVerdict, fixPrompt, logTail, parseReview, pickReviewer, renderReview, reviewFixPrompt, reviewPrompt, reviewState, runIdOf, summarizeChecks, actionsMinutes, triggersMergeGroup, triggersPullRequest, landingRoute, laneClaim, lanesFor, landRunId, MAIN_LANE, queuedReason, trainStep, } from "../core/team-landing.js";
35
+ import { zoneOf } from "../core/team-leases.js";
36
+ export const POLL_MS = 30_000;
37
+ /** D63: a teammate offline this long with a goal that needs someone can be adopted. */
38
+ export const ADOPT_AFTER_MS = 15 * 60_000;
39
+ /** How long after asking for a rerun a still-failing check is taken as the old result. */
40
+ export const RERUN_SETTLE_MS = 90_000;
41
+ /** A turn's fresh push with no checks reported after this long has no CI to wait for. */
42
+ export const TRAIN_SETTLE_MS = 2 * 60_000;
43
+ /** How long a repo's branch rules (merge queue or not) are trusted (D80). */
44
+ export const RULES_TTL_MS = 10 * 60_000;
45
+ export const defaultExec = (cmd, args, cwd, opts) => realExec(cmd, args, cwd, opts);
46
+ const realExec = (cmd, args, cwd, opts = {}) => new Promise((resolve) => {
47
+ const child = execFile(cmd, args, { cwd, timeout: opts.timeoutMs ?? 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, out, errOut) => resolve({ code: err ? (typeof err.code === "number" ? err.code : 1) : 0, out: String(out), err: String(errOut) }));
48
+ if (opts.input !== undefined)
49
+ child.stdin?.end(opts.input);
50
+ });
51
+ export class Landing {
52
+ rt;
53
+ deps;
54
+ busy = new Set(); // runId → a step (fix, review, land) in flight
55
+ timer = null;
56
+ alerted = new Set();
57
+ rerunAt = new Map(); // `${sha}:${check}` → when we asked for the rerun
58
+ rules = new Map(); // `${repo}@${base}`
59
+ inflight = new Map(); // runId → a poll in flight
60
+ constructor(rt, deps) {
61
+ this.rt = rt;
62
+ this.deps = deps;
63
+ }
64
+ start() {
65
+ if (this.timer)
66
+ return;
67
+ this.timer = setInterval(() => void this.tick().catch((e) => logbook.warn("team", "landing tick failed", String(e))), POLL_MS);
68
+ this.timer.unref?.();
69
+ }
70
+ stop() {
71
+ if (this.timer)
72
+ clearInterval(this.timer);
73
+ this.timer = null;
74
+ }
75
+ exec(cmd, args, cwd = this.rt.info.dir, opts) {
76
+ return (this.deps.exec ?? realExec)(cmd, args, cwd, opts);
77
+ }
78
+ async gh(args, cwd = this.rt.info.dir) {
79
+ const r = await this.exec("gh", args, cwd);
80
+ if (r.code !== 0)
81
+ throw new Error((r.err || r.out).trim().slice(0, 500) || `gh ${args[0]} failed`);
82
+ return r.out;
83
+ }
84
+ async git(args, cwd) {
85
+ const r = await this.exec("git", args, cwd);
86
+ if (r.code !== 0)
87
+ throw new Error((r.err || r.out).trim().slice(0, 500));
88
+ return r.out;
89
+ }
90
+ /** Goals with a PR this daemon is responsible for right now. */
91
+ goals() {
92
+ return this.rt.orchestra
93
+ .list()
94
+ .filter((r) => r.landing && r.landing.state !== "merged" && r.landing.state !== "closed");
95
+ }
96
+ set(run, patch) {
97
+ return this.rt.orchestra.setLanding(run.id, patch) ?? run.landing;
98
+ }
99
+ // ── the poll (D52) ──
100
+ async tick() {
101
+ const policy = await this.deps.policy().catch(() => null);
102
+ for (const run of this.goals()) {
103
+ if (this.busy.has(run.id))
104
+ continue;
105
+ await this.pollNow(run, policy);
106
+ }
107
+ }
108
+ /**
109
+ * Poll one goal now, at most one poll per goal at a time: a poll asked for
110
+ * while one runs makes that one go round again (a webhook's check result
111
+ * mid-poll isn't lost), and both callers get its answer.
112
+ */
113
+ pollNow(run, policy) {
114
+ const cur = this.inflight.get(run.id);
115
+ if (cur) {
116
+ cur.again = true;
117
+ return cur.p;
118
+ }
119
+ const slot = { again: false, p: Promise.resolve(run.landing) };
120
+ slot.p = (async () => {
121
+ try {
122
+ const pol = policy === undefined ? await this.deps.policy().catch(() => null) : policy;
123
+ let st = run.landing;
124
+ do {
125
+ slot.again = false;
126
+ const r = this.rt.orchestra.get(run.id) ?? run;
127
+ if (!r.landing || r.landing.state === "merged" || r.landing.state === "closed")
128
+ return r.landing ?? st;
129
+ try {
130
+ st = await this.poll(r, pol);
131
+ }
132
+ catch (e) {
133
+ logbook.warn("team", `landing: couldn't check PR #${r.landing.pr}`, String(e));
134
+ return r.landing;
135
+ }
136
+ } while (slot.again);
137
+ return st;
138
+ }
139
+ finally {
140
+ this.inflight.delete(run.id);
141
+ }
142
+ })();
143
+ this.inflight.set(run.id, slot);
144
+ return slot.p;
145
+ }
146
+ /** One look at one goal PR, and whatever it calls for. */
147
+ async poll(run, policy = null) {
148
+ const l = run.landing;
149
+ if (l.adoptedBy)
150
+ return l; // a teammate holds it (D63)
151
+ if (l.stack?.length)
152
+ return this.pollStack(run, policy);
153
+ const view = JSON.parse(await this.gh(["pr", "view", String(l.pr), "--json", "state,headRefOid,url,reviewDecision"]));
154
+ if (view.state === "MERGED")
155
+ return this.merged(run);
156
+ if (view.state === "CLOSED") {
157
+ await this.releaseSlot(run, "PR closed");
158
+ return this.set(run, { state: "closed" });
159
+ }
160
+ const sha = view.headRefOid;
161
+ if (sha !== l.headSha)
162
+ this.set(run, { headSha: sha });
163
+ // a goal back at work (a fix, a conflict) is the orchestrator's until it delivers
164
+ if (!isTerminal(run.status) && run.status !== "waiting_human") {
165
+ await this.releaseSlot(run, "the goal is being fixed");
166
+ return this.set(run, { state: "fixing" });
167
+ }
168
+ const rows = await this.checks(l.pr);
169
+ const sum = summarizeChecks(rows);
170
+ const cur = this.set(run, {
171
+ checks: { failing: sum.failing.map((c) => c.name), pending: sum.pending.map((c) => c.name), passing: sum.passing.length },
172
+ });
173
+ // flakes: failed at this commit, rerun, now passing (D53)
174
+ for (const c of sum.passing) {
175
+ const k = `${sha}:${c.name}`;
176
+ if (cur.reruns.includes(k) && !cur.flaky.includes(k)) {
177
+ this.set(run, { flaky: [...cur.flaky, k] });
178
+ await this.flaky(run, c.name).catch(() => { });
179
+ }
180
+ }
181
+ const maxFix = policy?.landing.autoFixAttempts ?? 2;
182
+ // a red check on its turn gives the lane back: the goal requeues once it's green (D82)
183
+ if (sum.failing.length && run.landing.slot)
184
+ await this.releaseSlot(run, `"${sum.failing[0].name}" failed on its turn`);
185
+ for (const c of sum.failing) {
186
+ const k = `${sha}:${c.name}`;
187
+ const verdict = failureVerdict(c.name, sha, run.landing.reruns);
188
+ if (verdict === "rerun") {
189
+ const id = runIdOf(c.link);
190
+ this.set(run, { reruns: [...run.landing.reruns, k], state: "failing" });
191
+ this.rerunAt.set(k, Date.now());
192
+ if (id)
193
+ await this.gh(["run", "rerun", id, "--failed"]).catch((e) => logbook.warn("team", "rerun failed", String(e)));
194
+ return run.landing;
195
+ }
196
+ // GitHub takes a moment to show a requested rerun as queued: don't read the old failure as a second one
197
+ if (Date.now() - (this.rerunAt.get(k) ?? 0) < (this.deps.rerunSettleMs ?? RERUN_SETTLE_MS))
198
+ return run.landing;
199
+ if (run.landing.fixAttempts >= maxFix)
200
+ return this.needsHuman(run, `required check "${c.name}" still fails after ${maxFix} fix attempts`);
201
+ if (run.status !== "completed")
202
+ return this.needsHuman(run, `required check "${c.name}" fails and the goal is waiting on you`);
203
+ await this.fix(run, c, maxFix);
204
+ return run.landing;
205
+ }
206
+ // review on open and after each fix push (D60, D61)
207
+ const rev = policy?.review ?? { enabled: true, maxRuns: 3 };
208
+ if (rev.enabled && run.landing.reviewedSha !== sha && run.landing.reviews < rev.maxRuns && !sum.failing.length) {
209
+ void this.review(run, sha, maxFix);
210
+ }
211
+ if (run.landing.landRequested && run.landing.train)
212
+ return this.trainTurn(run, policy, sha, view.reviewDecision, rows, sum);
213
+ // Waiting on a human (GitHub refused the merge, fixes ran out) stays so until
214
+ // something changes — a new commit, or Land again. A later poll used to flip
215
+ // it to "green" and drop the reason, hiding what the owner was alerted about.
216
+ if (run.landing.state === "needs_human" && !run.landing.landRequested && run.landing.needsHumanSha === sha)
217
+ return run.landing;
218
+ if (sum.pending.length)
219
+ return this.set(run, { state: run.landing.landRequested ? "landing" : "pending" });
220
+ if (run.landing.review?.state === "failure" && !run.landing.review.overridden)
221
+ return run.landing;
222
+ if (sum.green || !rows.length) {
223
+ const st = this.set(run, { state: run.landing.landRequested ? "landing" : "green", reason: undefined });
224
+ if (run.from)
225
+ await this.handBack(run).catch(() => { });
226
+ return st;
227
+ }
228
+ return run.landing;
229
+ }
230
+ /** Required checks (D54), or every check when the repo requires none. */
231
+ async checks(pr) {
232
+ const fields = "name,state,bucket,link,workflow";
233
+ // exit 8 = checks pending, 1 = some failing: the JSON is still on stdout
234
+ const req = await this.exec("gh", ["pr", "checks", String(pr), "--required", "--json", fields], this.rt.info.dir);
235
+ const parse = (s) => {
236
+ try {
237
+ return JSON.parse(s);
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ };
243
+ const rows = parse(req.out);
244
+ if (rows && rows.length)
245
+ return rows;
246
+ const all = await this.exec("gh", ["pr", "checks", String(pr), "--json", fields], this.rt.info.dir);
247
+ return parse(all.out) ?? [];
248
+ }
249
+ async fix(run, check, max) {
250
+ if (this.busy.has(run.id))
251
+ return;
252
+ this.busy.add(run.id);
253
+ try {
254
+ const id = runIdOf(check.link);
255
+ const raw = id ? (await this.exec("gh", ["run", "view", id, "--log-failed"], this.rt.info.dir)).out : "";
256
+ const attempt = run.landing.fixAttempts + 1;
257
+ this.set(run, { fixAttempts: attempt, state: "fixing", reason: undefined });
258
+ await this.rt.orchestra.reopen(run.id, fixPrompt({ pr: run.landing.pr, check: check.name, log: logTail(raw || `(no log available — see ${check.link ?? "the PR's checks"})`), attempt, max }), `check ${check.name} failed`);
259
+ }
260
+ finally {
261
+ this.busy.delete(run.id);
262
+ }
263
+ }
264
+ async flaky(run, check) {
265
+ const l = run.landing;
266
+ await this.exec("gh", ["label", "create", "loom:flaky", "--color", "fbca04", "--description", "A check failed, then passed on rerun"], this.rt.info.dir);
267
+ await this.exec("gh", ["pr", "edit", String(l.pr), "--add-label", "loom:flaky"], this.rt.info.dir);
268
+ await this.gh(["pr", "comment", String(l.pr), "--body",
269
+ `**Loom:** \`${check}\` failed, then passed when rerun on the same commit — likely flaky. No agent changed code for it.`]);
270
+ await this.post(run, "check_flaky", { pr: l.pr, check });
271
+ }
272
+ async needsHuman(run, reason) {
273
+ const st = this.set(run, { state: "needs_human", needsHumanSha: run.landing.headSha, reason });
274
+ const key = `${run.id}:${reason}`;
275
+ if (!this.alerted.has(key)) {
276
+ this.alerted.add(key);
277
+ this.alert(run, `PR #${st.pr} needs you: ${reason}`);
278
+ await this.post(run, "goal_needs_someone", { pr: st.pr, url: st.url, runId: run.id, branch: this.rt.orchestra.prBranch(run), reason: reason.slice(0, 200) });
279
+ }
280
+ return st;
281
+ }
282
+ /** D25: the owner's phone (and desktop) hears about what needs them. */
283
+ alert(run, text) {
284
+ this.rt.log.append({ kind: "orchestra", chat: run.chat, payload: { phase: "alert", runId: run.id, text } });
285
+ notify({ title: `Loom · ${this.rt.info.name}`, body: text });
286
+ }
287
+ async merged(run) {
288
+ await this.releaseSlot(run, "merged");
289
+ const mc = await this.exec("gh", ["pr", "view", String(run.landing.pr), "--json", "mergeCommit"], this.rt.info.dir).catch(() => null);
290
+ let mergeSha;
291
+ try {
292
+ mergeSha = mc?.code === 0 ? JSON.parse(mc.out).mergeCommit?.oid : undefined;
293
+ }
294
+ catch {
295
+ mergeSha = undefined;
296
+ }
297
+ const ciMinutes = await this.ciMinutes(run).catch(() => null);
298
+ const st = this.set(run, { state: "merged", landRequested: false, ...(mergeSha ? { mergeSha } : {}), ...(ciMinutes !== null ? { ciMinutes } : {}) });
299
+ await this.post(run, "goal_landed", { runId: run.id, pr: st.pr, costUsd: Math.round(run.costUsd * 100) / 100, ...(ciMinutes !== null ? { ciMinutes } : {}) }, `landed:${run.id}`);
300
+ if (run.from)
301
+ await this.handBack(run).catch(() => { });
302
+ return st;
303
+ }
304
+ /** Actions minutes the goal's branch used (§10: CI cost per goal, no App needed). */
305
+ async ciMinutes(run) {
306
+ const repo = await this.repo();
307
+ if (!repo)
308
+ return null;
309
+ const branch = this.rt.orchestra.prBranch(run);
310
+ const r = await this.exec("gh", ["api", `repos/${repo}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=100`], this.rt.info.dir);
311
+ if (r.code !== 0)
312
+ return null;
313
+ try {
314
+ const runs = JSON.parse(r.out).workflow_runs ?? [];
315
+ return actionsMinutes(runs);
316
+ }
317
+ catch {
318
+ return null;
319
+ }
320
+ }
321
+ // ── review (D60, D61) ──
322
+ async review(run, sha, maxFix = 2) {
323
+ const key = `review:${run.id}`;
324
+ if (this.busy.has(key))
325
+ return;
326
+ this.busy.add(key);
327
+ const l = run.landing;
328
+ try {
329
+ const authors = [run.orchestrator.kind, ...run.tasks.map((t) => t.kind)];
330
+ const reviewer = pickReviewer(authors, this.rt.usableKinds());
331
+ const repo = await this.repo();
332
+ if (!reviewer) {
333
+ this.set(run, { reviews: l.reviews + 1, reviewedSha: sha, review: { state: "skipped", reviewer: null, high: 0, findings: 0, at: Date.now() } });
334
+ if (repo)
335
+ await this.postStatus(repo, sha, "success", "skipped: no agent from another vendor is installed");
336
+ return;
337
+ }
338
+ // "reviewing…" is a courtesy; failing to post it mustn't cost the review
339
+ if (repo)
340
+ await this.postStatus(repo, sha, "pending", `${reviewer} is reviewing`).catch(() => { });
341
+ const diff = await this.gh(["pr", "diff", String(l.pr)]);
342
+ const planFile = path.join(run.dir, "plans", run.id, "PLAN.md");
343
+ const plan = fs.existsSync(planFile) ? fs.readFileSync(planFile, "utf8") : undefined;
344
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "loom-review-"));
345
+ let said = "";
346
+ try {
347
+ said = await this.rt.askAgent(reviewer, tmp, reviewPrompt({ goal: run.goal, ...(plan ? { plan } : {}), diff, pr: l.pr }));
348
+ }
349
+ finally {
350
+ fs.rmSync(tmp, { recursive: true, force: true });
351
+ }
352
+ const parsed = parseReview(said) ?? { summary: "The reviewer's reply had no findings block; treated as no findings.", findings: [] };
353
+ const state = reviewState(parsed.findings);
354
+ const high = parsed.findings.filter((f) => f.severity === "high").length;
355
+ await this.gh(["pr", "review", String(l.pr), "--comment", "--body", renderReview(parsed, reviewer)]);
356
+ // the owner overrode this commit while the review ran: the findings go on the PR, the decision stands (D61)
357
+ const prior = run.landing.review;
358
+ const kept = prior?.overridden && prior.overriddenSha === sha ? { overridden: prior.overridden, overriddenSha: sha } : null;
359
+ if (repo) {
360
+ // The status ends where the owner put it — not at the "reviewing…" this
361
+ // review posted on its way in, which would sit pending for ever.
362
+ if (kept)
363
+ await this.postStatus(repo, sha, "success", `overridden by ${this.deps.github() ?? "the owner"}: ${kept.overridden}`.slice(0, 140));
364
+ else
365
+ await this.postStatus(repo, sha, state, state === "failure" ? `${high} high-severity finding${high === 1 ? "" : "s"}` : "no high-severity findings");
366
+ }
367
+ this.set(run, {
368
+ reviews: run.landing.reviews + 1,
369
+ reviewedSha: sha,
370
+ review: { state, reviewer, high, findings: parsed.findings.length, at: Date.now(), ...(kept ?? {}) },
371
+ });
372
+ await this.post(run, "review_submitted", { pr: l.pr, reviewer, state, high, findings: parsed.findings.length });
373
+ if (state === "failure" && !kept) {
374
+ // high findings share the fix budget with failing checks (D55, D61)
375
+ if (run.landing.fixAttempts >= maxFix) {
376
+ await this.needsHuman(run, `the review found ${high} high-severity problem${high === 1 ? "" : "s"} and the fix budget is spent`);
377
+ }
378
+ else if (run.status === "completed") {
379
+ const attempt = run.landing.fixAttempts + 1;
380
+ this.set(run, { fixAttempts: attempt, state: "fixing" });
381
+ await this.rt.orchestra.reopen(run.id, reviewFixPrompt(parsed.findings, attempt, maxFix), "review found high-severity problems");
382
+ }
383
+ }
384
+ }
385
+ catch (e) {
386
+ logbook.warn("team", `review of PR #${l.pr} failed`, String(e));
387
+ this.set(run, { reviews: run.landing.reviews + 1, reviewedSha: sha });
388
+ }
389
+ finally {
390
+ this.busy.delete(key);
391
+ }
392
+ }
393
+ /** The owner overrides a blocking review, with a reason on record (D61). */
394
+ async overrideReview(runId, reason) {
395
+ const run = this.mustGoal(runId);
396
+ const l = run.landing;
397
+ if (!reason.trim())
398
+ throw new Error("say why the review is wrong");
399
+ const repo = await this.repo();
400
+ if (repo && l.headSha)
401
+ await this.postStatus(repo, l.headSha, "success", `overridden by ${this.deps.github() ?? "the owner"}: ${reason}`.slice(0, 140));
402
+ await this.gh(["pr", "comment", String(l.pr), "--body", `**Loom review overridden** by @${this.deps.github() ?? "the owner"}: ${reason}`]);
403
+ return this.set(run, { review: { ...(l.review ?? { state: "failure", reviewer: null, high: 0, findings: 0, at: Date.now() }), overridden: reason, ...(l.headSha ? { overriddenSha: l.headSha } : {}) } });
404
+ }
405
+ async postStatus(repo, sha, state, description) {
406
+ await this.gh(["api", "-X", "POST", `repos/${repo}/statuses/${sha}`,
407
+ "-f", `state=${state}`, "-f", "context=loom/review", "-f", `description=${description.slice(0, 140)}`]);
408
+ }
409
+ // ── land (D56–D59) ──
410
+ /**
411
+ * The owner clicked Land. Bring fresh main in (one agent attempt at a
412
+ * conflict), run the fast tests, push, and ask GitHub to merge when the
413
+ * rules pass. It keeps going on later polls until the PR merges.
414
+ */
415
+ async land(runId) {
416
+ const run = this.mustGoal(runId);
417
+ if (run.from)
418
+ throw new Error("this goal is a teammate's — hand it back and let them land it");
419
+ if (!isTerminal(run.status))
420
+ throw new Error(`the goal is still ${run.status} — land it once it's done`);
421
+ if (this.busy.has(run.id))
422
+ throw new Error("a fix or land step is already running for this goal");
423
+ const policy = await this.deps.policy().catch(() => null);
424
+ // D20, D80: no merge queue on a team repo → the train; Loom merges when the goal's lanes are its
425
+ if (!run.landing.stack?.length && (await this.route(run)) === "train") {
426
+ this.set(run, { landRequested: true, train: true, state: "landing", reason: undefined });
427
+ return this.pollNow(run, policy);
428
+ }
429
+ this.busy.add(run.id);
430
+ try {
431
+ this.set(run, { landRequested: true, train: false, state: "landing", reason: undefined });
432
+ if (run.landing.stack?.length)
433
+ return await this.landStack(run);
434
+ if (!(await this.pushrebase(run, policy)))
435
+ return run.landing;
436
+ // 4. merge when GitHub's rules pass (the merge queue, or auto-merge)
437
+ await this.autoMerge(run.landing.pr, "--squash");
438
+ return this.set(run, { state: "landing" });
439
+ }
440
+ finally {
441
+ this.busy.delete(run.id);
442
+ }
443
+ }
444
+ /**
445
+ * Pushrebase-lite (D56–D58): fresh base merged into the goal (one agent
446
+ * attempt at a conflict), the fast tests, push. False when it went to a fix
447
+ * or a human instead of pushing.
448
+ */
449
+ async pushrebase(run, policy) {
450
+ const base = run.baseBranch ?? "main";
451
+ // 1. fresh main (D56, D58)
452
+ await this.git(["fetch", "-q", "origin", base], run.dir);
453
+ const merge = await this.exec("git", ["-c", "user.name=Loom Orchestra", "-c", "user.email=orchestra@loom.local", "merge", "--no-edit", "-q", `origin/${base}`], run.dir);
454
+ if (merge.code !== 0) {
455
+ const files = (await this.exec("git", ["diff", "--name-only", "--diff-filter=U"], run.dir)).out.split("\n").filter(Boolean);
456
+ await this.exec("git", ["merge", "--abort"], run.dir);
457
+ await this.conflict(run, base, files, policy);
458
+ return false;
459
+ }
460
+ // 2. fast tests, if the team set any (D57)
461
+ if (policy?.landing.fastTest) {
462
+ const t = await this.exec("sh", ["-c", policy.landing.fastTest], run.dir, { timeoutMs: policy.landing.timeoutMin * 60_000 });
463
+ if (t.code !== 0) {
464
+ const max = policy.landing.autoFixAttempts;
465
+ if (run.landing.fixAttempts >= max) {
466
+ await this.needsHuman(run, `the fast tests (\`${policy.landing.fastTest}\`) fail on fresh ${base}`);
467
+ return false;
468
+ }
469
+ const attempt = run.landing.fixAttempts + 1;
470
+ this.set(run, { fixAttempts: attempt, state: "fixing" });
471
+ await this.rt.orchestra.reopen(run.id, fixPrompt({ pr: run.landing.pr, check: `fast tests: ${policy.landing.fastTest}`, log: logTail(`${t.out}\n${t.err}`), attempt, max }), "fast tests failed before landing");
472
+ return false;
473
+ }
474
+ }
475
+ // 3. push
476
+ await this.git(["push", "-q", "origin", `${run.branch}:${this.rt.orchestra.prBranch(run)}`], run.dir);
477
+ return true;
478
+ }
479
+ // ── the landing train (Phase 6: D20's fallback, D79–D82) ──
480
+ /** Merge queue, train or auto-merge, for this goal (D80). Branch rules are cached per repo for 10 minutes. */
481
+ async route(run) {
482
+ const share = await this.deps.share(this.rt);
483
+ const team = Boolean(share && this.deps.hub() && this.deps.deviceId());
484
+ if (!share)
485
+ return "auto";
486
+ const base = run.baseBranch ?? "main";
487
+ const key = `${share.repo}@${base}`;
488
+ let hit = this.rules.get(key);
489
+ if (!hit || Date.now() - hit.at > RULES_TTL_MS) {
490
+ const r = await this.exec("gh", ["api", `repos/${share.repo}/rules/branches/${base}`], this.rt.info.dir);
491
+ let rules = null;
492
+ if (r.code === 0) {
493
+ try {
494
+ const v = JSON.parse(r.out);
495
+ rules = Array.isArray(v) ? v : null;
496
+ }
497
+ catch {
498
+ rules = null;
499
+ }
500
+ }
501
+ hit = { at: Date.now(), rules };
502
+ this.rules.set(key, hit);
503
+ }
504
+ return landingRoute({ rules: hit.rules, team, stack: Boolean(run.landing?.stack?.length) });
505
+ }
506
+ /** The lanes this goal's PR needs: path scopes matched against its diff (D81). */
507
+ async lanesOf(run, policy) {
508
+ const lanes = policy?.landing.lanes ?? {};
509
+ if (!Object.keys(lanes).length)
510
+ return [MAIN_LANE];
511
+ const pr = await this.exec("gh", ["pr", "diff", String(run.landing.pr), "--name-only"], this.rt.info.dir);
512
+ let files = pr.code === 0 ? pr.out.split("\n").map((f) => f.trim()).filter(Boolean) : null;
513
+ if (!files) {
514
+ const base = run.baseBranch ?? "main";
515
+ const local = await this.exec("git", ["diff", "--name-only", `origin/${base}...${run.branch}`], run.dir);
516
+ files = local.code === 0 ? local.out.split("\n").map((f) => f.trim()).filter(Boolean) : null;
517
+ }
518
+ // what it changes is unknown: it waits for every lane rather than guess one
519
+ if (!files)
520
+ return [...new Set([...Object.keys(lanes), MAIN_LANE])].sort();
521
+ return lanesFor(files, lanes);
522
+ }
523
+ /**
524
+ * One step of a goal in the train: claim its lanes (or queue behind their
525
+ * holder), bring fresh base in and push on a new turn, wait for green on
526
+ * that head, merge.
527
+ */
528
+ async trainTurn(run, policy, sha, reviewDecision, rows, sum) {
529
+ const l = run.landing;
530
+ if (sum.failing.length)
531
+ return l; // the fix loop has it; it requeues once green
532
+ if (l.review?.state === "failure" && !l.review.overridden) {
533
+ await this.releaseSlot(run, "the review blocks it");
534
+ return run.landing;
535
+ }
536
+ if (!l.slot) {
537
+ // red at this commit and rerunning: it rejoins once the rerun says flake (it's green), not before
538
+ if (sum.pending.length && l.reruns.some((k) => k.startsWith(`${sha}:`) && !l.flaky.includes(k))) {
539
+ return this.set(run, { state: "pending", reason: "rerunning a failed check before it rejoins the landing train" });
540
+ }
541
+ // a turn is for a PR that can merge: one waiting on a human approval doesn't hold a lane
542
+ if (/CHANGES_REQUESTED|REVIEW_REQUIRED/i.test(reviewDecision ?? "")) {
543
+ return this.set(run, { state: "queued", reason: "waiting for a reviewer's approval before its turn to land" });
544
+ }
545
+ if (!(await this.claimSlot(run, policy)))
546
+ return run.landing;
547
+ }
548
+ let head = sha;
549
+ let cur = { rows, sum };
550
+ if (run.landing.turnSha !== head) {
551
+ if (!(await this.pushrebase(run, policy))) {
552
+ await this.releaseSlot(run, "its turn found a conflict or failing fast tests");
553
+ return run.landing;
554
+ }
555
+ const pushed = (await this.git(["rev-parse", run.branch], run.dir)).trim();
556
+ if (pushed && pushed !== head) {
557
+ head = pushed;
558
+ const fresh = await this.checks(l.pr);
559
+ cur = { rows: fresh, sum: summarizeChecks(fresh) };
560
+ this.set(run, { checks: { failing: cur.sum.failing.map((c) => c.name), pending: cur.sum.pending.map((c) => c.name), passing: cur.sum.passing.length } });
561
+ }
562
+ this.set(run, { headSha: head, turnSha: head, turnAt: Date.now() });
563
+ if (cur.sum.failing.length)
564
+ return this.set(run, { state: "landing" }); // the next poll's red path takes it
565
+ }
566
+ const step = trainStep({
567
+ holding: true,
568
+ turnSha: run.landing.turnSha,
569
+ headSha: head,
570
+ checks: cur.sum,
571
+ rows: cur.rows.length,
572
+ reviewing: this.busy.has(`review:${run.id}`),
573
+ sinceTurnMs: Date.now() - (run.landing.turnAt ?? 0),
574
+ settleMs: this.deps.trainSettleMs ?? TRAIN_SETTLE_MS,
575
+ });
576
+ if (step !== "merge")
577
+ return this.set(run, { state: "landing", reason: undefined });
578
+ return this.mergeNow(run);
579
+ }
580
+ /**
581
+ * Take every lane the goal needs, in order, or none: a lane held by someone
582
+ * else queues the goal behind them. The hub decides atomically — a lane's
583
+ * slot is a hard zone (D31), so a second claimer is refused.
584
+ */
585
+ async claimSlot(run, policy) {
586
+ const hub = this.deps.hub();
587
+ const device = this.deps.deviceId();
588
+ const share = await this.deps.share(this.rt);
589
+ if (!hub || !device || !share) {
590
+ this.set(run, { state: "queued", reason: "waiting for the team hub" });
591
+ return false;
592
+ }
593
+ const lanes = await this.lanesOf(run, policy);
594
+ this.set(run, { lanes });
595
+ const mine = landRunId(run.id);
596
+ const queued = async (lane, who, holderRun) => {
597
+ const reason = queuedReason(who, lane);
598
+ const news = run.landing.state !== "queued" || run.landing.reason !== reason;
599
+ this.set(run, { state: "queued", slot: false, reason });
600
+ if (news)
601
+ await this.post(run, "land_queued", { runId: run.id, pr: run.landing.pr, lane, lanes, behind: who, behindRun: holderRun });
602
+ return false;
603
+ };
604
+ // look before taking: a goal that would only get some of its lanes doesn't take any
605
+ const held = (await hub.leases(share.teamId, share.repo).catch(() => [])).filter((x) => !x.stale && x.runId !== mine && x.runId.endsWith(":land"));
606
+ for (const lane of lanes) {
607
+ const h = held.find((x) => x.taskId === laneClaim(lane).taskId);
608
+ if (h)
609
+ return queued(lane, h.github, h.runId.replace(/:land$/, ""));
610
+ }
611
+ const keys = this.deps.keys(share.teamId);
612
+ const key = keys[keys.length - 1];
613
+ let got = 0;
614
+ for (const lane of lanes) {
615
+ const c = laneClaim(lane);
616
+ const res = await hub.claimLease(share.teamId, {
617
+ globs: c.globs,
618
+ files: c.files,
619
+ prefixes: c.prefixes,
620
+ hardZones: c.hardZones,
621
+ deviceId: device,
622
+ repo: share.repo,
623
+ runId: mine,
624
+ taskId: c.taskId,
625
+ ...(key ? { sealed: sealForTeam(key, { goal: run.goal.split("\n")[0].slice(0, 200), task: `landing in lane ${lane}` }) } : {}),
626
+ });
627
+ if (!res.lease) {
628
+ // lost a race for a later lane: give back the earlier ones ("rollback" wakes nobody — ticks retry)
629
+ if (got)
630
+ await hub.releaseLeases(share.teamId, mine, `rollback: lane ${lane} is taken`).catch(() => 0);
631
+ const b = res.blockedBy;
632
+ return queued(lane, b?.lease.github ?? null, b ? b.lease.runId.replace(/:land$/, "") : null);
633
+ }
634
+ got++;
635
+ }
636
+ this.set(run, { slot: true, turnSha: undefined, turnAt: undefined, state: "landing", reason: undefined });
637
+ await this.post(run, "land_turn", { runId: run.id, pr: run.landing.pr, lanes });
638
+ return true;
639
+ }
640
+ /** Give the goal's lanes back (and only them: its task leases stay until it lands, D36). */
641
+ async releaseSlot(run, reason) {
642
+ if (!run.landing?.slot)
643
+ return;
644
+ this.set(run, { slot: false, turnSha: undefined, turnAt: undefined });
645
+ const hub = this.deps.hub();
646
+ const share = await this.deps.share(this.rt);
647
+ if (!hub || !share)
648
+ return;
649
+ await hub.releaseLeases(share.teamId, landRunId(run.id), reason).catch((e) => logbook.warn("team", "couldn't release the landing slot", String(e)));
650
+ }
651
+ /** Still the holder of every lane? A slot that went stale (a long sleep) may be someone else's now. */
652
+ async stillHolding(run) {
653
+ const hub = this.deps.hub();
654
+ const share = await this.deps.share(this.rt);
655
+ if (!hub || !share)
656
+ return false;
657
+ let leases;
658
+ try {
659
+ leases = await hub.leases(share.teamId, share.repo);
660
+ }
661
+ catch {
662
+ return true; // a hub hiccup isn't a lost slot; GitHub's own rules still guard the merge
663
+ }
664
+ const mine = leases.filter((x) => x.runId === landRunId(run.id) && !x.stale && x.deviceId === this.deps.deviceId());
665
+ return (run.landing.lanes ?? [MAIN_LANE]).every((lane) => mine.some((x) => x.taskId === laneClaim(lane).taskId));
666
+ }
667
+ /** Its turn, green: merge now (not --auto — the train is the queue), then hand the lanes on. */
668
+ async mergeNow(run) {
669
+ if (!(await this.stillHolding(run))) {
670
+ this.set(run, { slot: false, turnSha: undefined, turnAt: undefined });
671
+ return this.set(run, { state: "queued", reason: "its landing slot lapsed — queuing again" });
672
+ }
673
+ const r = await this.exec("gh", ["pr", "merge", String(run.landing.pr), "--squash"], this.rt.info.dir);
674
+ if (r.code !== 0) {
675
+ await this.releaseSlot(run, "GitHub refused the merge");
676
+ this.set(run, { landRequested: false });
677
+ return this.needsHuman(run, `GitHub refused the merge: ${(r.err || r.out).trim().slice(0, 200)}`);
678
+ }
679
+ return this.merged(run);
680
+ }
681
+ async autoMerge(pr, method) {
682
+ const r = await this.exec("gh", ["pr", "merge", String(pr), "--auto", method], this.rt.info.dir);
683
+ if (r.code === 0)
684
+ return;
685
+ // Auto-merge off for the repo (and nothing pending): the human's click merges now, under GitHub's rules.
686
+ if (/auto.?merge|clean status|not allowed/i.test(r.err + r.out)) {
687
+ await this.gh(["pr", "merge", String(pr), method]);
688
+ return;
689
+ }
690
+ throw new Error((r.err || r.out).trim().slice(0, 300));
691
+ }
692
+ async conflict(run, base, files, policy) {
693
+ const zone = policy ? zoneOf({ globs: files, files, prefixes: [] }, policy.hardZones) : null;
694
+ if (zone)
695
+ return this.needsHuman(run, `fresh ${base} conflicts in the hard zone ${zone} (${files.slice(0, 5).join(", ")}) — a human resolves those`);
696
+ if (run.landing.conflictTried)
697
+ return this.needsHuman(run, `fresh ${base} still conflicts after one agent attempt: ${files.slice(0, 5).join(", ")}`);
698
+ this.set(run, { conflictTried: true, state: "fixing" });
699
+ await this.rt.orchestra.reopen(run.id, [
700
+ `Landing this goal: ${base} moved on, and merging it into the goal conflicts in: ${files.join(", ") || "(unknown files)"}.`,
701
+ `Spawn ONE task (id "fixmerge") for the worker that owns those files: in its worktree run \`git fetch origin ${base} && git merge origin/${base}\`, resolve the conflicts keeping both sides' intent, run the tests, and commit. Then review and finish with done.`,
702
+ "If the two sides can't both be kept, don't guess — ask (the `ask` action).",
703
+ ].join("\n"), `conflict with ${base}`);
704
+ return run.landing;
705
+ }
706
+ // ── stacks (D59): land bottom-up with the merge method, retargeting the next PR ──
707
+ async pollStack(run, _policy) {
708
+ const l = run.landing;
709
+ const stack = [...l.stack];
710
+ for (let i = 0; i < stack.length; i++) {
711
+ const p = stack[i];
712
+ if (p.state === "MERGED")
713
+ continue;
714
+ const v = JSON.parse(await this.gh(["pr", "view", String(p.pr), "--json", "state,headRefOid"]));
715
+ stack[i] = { ...p, state: v.state };
716
+ // A lower slice failing for real (rerun once already) can't be auto-fixed
717
+ // in place — fixes land on the goal's own branch at the top. Fold the
718
+ // stack into one PR and let the normal fix loop take it (D59, D55).
719
+ const top = i === stack.length - 1;
720
+ if (!top && v.state === "OPEN" && v.headRefOid) {
721
+ const failing = summarizeChecks(await this.checks(p.pr)).failing;
722
+ for (const c of failing) {
723
+ const k = `${v.headRefOid}:${c.name}`;
724
+ if (!run.landing.reruns.includes(k)) {
725
+ this.set(run, { reruns: [...run.landing.reruns, k] });
726
+ this.rerunAt.set(k, Date.now());
727
+ const id = runIdOf(c.link);
728
+ if (id)
729
+ await this.gh(["run", "rerun", id, "--failed"]).catch(() => { });
730
+ continue;
731
+ }
732
+ if (Date.now() - (this.rerunAt.get(k) ?? 0) < (this.deps.rerunSettleMs ?? RERUN_SETTLE_MS))
733
+ continue;
734
+ return this.collapseStack(run, stack, `"${c.name}" fails on part ${i + 1} (#${p.pr})`);
735
+ }
736
+ }
737
+ if (v.state === "MERGED") {
738
+ const next = stack[i + 1];
739
+ if (next) {
740
+ await this.gh(["pr", "edit", String(next.pr), "--base", run.baseBranch ?? "main"]);
741
+ stack[i + 1] = { ...next, base: run.baseBranch ?? "main" };
742
+ if (l.landRequested)
743
+ await this.autoMerge(next.pr, "--merge").catch((e) => logbook.warn("team", "stack: couldn't queue the next PR", String(e)));
744
+ }
745
+ }
746
+ }
747
+ this.set(run, { stack });
748
+ if (stack.every((p) => p.state === "MERGED"))
749
+ return this.merged(run);
750
+ return run.landing;
751
+ }
752
+ /**
753
+ * Close the stack's unmerged lower PRs and point the top one at the base
754
+ * branch: one PR again, carrying everything, that the fix loop can fix.
755
+ */
756
+ async collapseStack(run, stack, why) {
757
+ const base = run.baseBranch ?? "main";
758
+ const top = stack[stack.length - 1];
759
+ for (const p of stack.slice(0, -1)) {
760
+ if (p.state === "MERGED" || p.state === "CLOSED")
761
+ continue;
762
+ await this.gh(["pr", "close", String(p.pr), "--comment", `**Loom:** ${why}, which can't be fixed in place on a stacked slice. Folded into #${top.pr}, which now carries this part too.`]).catch(() => { });
763
+ }
764
+ await this.gh(["pr", "edit", String(top.pr), "--base", base]);
765
+ await this.gh(["pr", "comment", String(top.pr), "--body", `**Loom:** the stack was folded into this PR (${why}). It now targets \`${base}\` and carries every part; fixes land here.`]).catch(() => { });
766
+ return this.set(run, { stack: undefined, pr: top.pr, url: top.url, state: "failing" });
767
+ }
768
+ async landStack(run) {
769
+ const lowest = run.landing.stack.find((p) => p.state !== "MERGED");
770
+ if (!lowest)
771
+ return this.merged(run);
772
+ await this.autoMerge(lowest.pr, "--merge");
773
+ return this.set(run, { state: "landing" });
774
+ }
775
+ // ── adopt (D63) ──
776
+ /**
777
+ * Teammates' goal PRs that need someone: the owner said so (out of fix
778
+ * attempts), or its checks fail and the owner has been offline 15+ minutes.
779
+ */
780
+ async adoptable() {
781
+ const share = await this.deps.share(this.rt);
782
+ const me = this.deps.github();
783
+ if (!share || !me)
784
+ return [];
785
+ const feed = this.deps.feed(share.teamId).filter((e) => e.repo === share.repo);
786
+ const presence = this.deps.presence(share.teamId);
787
+ const lastSeen = (who) => Math.max(0, ...presence.filter((p) => p.github === who).map((p) => p.ts));
788
+ const prs = new Map();
789
+ for (const e of feed) {
790
+ const n = Number(e.meta.pr ?? e.meta.number);
791
+ if (!n)
792
+ continue;
793
+ const cur = prs.get(n) ?? { pr: n, url: String(e.meta.url ?? ""), branch: String(e.meta.branch ?? ""), owner: String(e.meta.author ?? e.github ?? ""), reason: "", open: true, needs: false, failing: false };
794
+ if (e.type === "pr_opened")
795
+ Object.assign(cur, { url: String(e.meta.url ?? cur.url), branch: String(e.meta.branch ?? cur.branch), owner: String(e.meta.author ?? cur.owner) });
796
+ if (e.type === "goal_needs_someone")
797
+ Object.assign(cur, { needs: true, reason: String(e.meta.reason ?? "needs someone"), ownerRunId: String(e.meta.runId ?? ""), owner: e.github ?? cur.owner, branch: String(e.meta.branch ?? cur.branch) });
798
+ if (e.type === "check_failed")
799
+ Object.assign(cur, { failing: true });
800
+ if (e.type === "check_passed")
801
+ Object.assign(cur, { failing: false });
802
+ if (e.type === "goal_adopted" || e.type === "goal_returned")
803
+ Object.assign(cur, { needs: false, failing: false });
804
+ if (e.type === "pr_merged" || e.type === "pr_closed" || e.type === "goal_landed")
805
+ cur.open = false;
806
+ prs.set(n, cur);
807
+ }
808
+ const now = Date.now();
809
+ return [...prs.values()]
810
+ .filter((p) => p.open && p.owner && p.owner !== me && p.branch.startsWith("loom/"))
811
+ .filter((p) => p.needs || (p.failing && now - lastSeen(p.owner) > ADOPT_AFTER_MS))
812
+ .map(({ open: _o, needs, failing: _f, ...p }) => ({ ...p, reason: needs ? p.reason : `checks failing and ${p.owner} has been offline 15+ min` }));
813
+ }
814
+ /** Take a teammate's goal: a small run on this machine that makes its PR green. */
815
+ async adopt(pr, opts = {}) {
816
+ const pick = (await this.adoptable()).find((p) => p.pr === pr);
817
+ if (!pick)
818
+ throw new Error(`PR #${pr} isn't waiting on anyone — only a goal that needs someone can be adopted`);
819
+ const ownerRunId = pick.ownerRunId || /^loom\/orchestra\/([^/]+)\//.exec(pick.branch)?.[1];
820
+ const run = await this.rt.orchestra.start({
821
+ goal: [
822
+ `Make ${pick.owner}'s PR #${pr} green. ${pick.reason}.`,
823
+ `You are working on their branch ${pick.branch}; your fixes are pushed back to it. Look at the failing checks (\`gh pr checks ${pr}\`, \`gh run view <id> --log-failed\`), fix the cause with the smallest change, and finish. Don't change what the goal does.`,
824
+ ].join("\n"),
825
+ ...(opts.orchestrator ? { orchestrator: opts.orchestrator } : {}),
826
+ ...(opts.workers ? { workers: opts.workers } : {}),
827
+ from: { branch: pick.branch, pr, url: pick.url, owner: pick.owner, ...(ownerRunId ? { ownerRunId } : {}) },
828
+ });
829
+ await this.post(run, "goal_adopted", { pr, runId: ownerRunId ?? null, by: this.deps.github(), owner: pick.owner, adopterRunId: run.id });
830
+ return run;
831
+ }
832
+ /** The adopted PR is green (or merged): give the goal back (D63). */
833
+ async handBack(run) {
834
+ if (!run.from || run.landing?.returned)
835
+ return;
836
+ this.set(run, { returned: true });
837
+ await this.post(run, "goal_returned", { pr: run.from.pr, runId: run.from.ownerRunId ?? null, by: this.deps.github(), owner: run.from.owner });
838
+ }
839
+ /** The owner's side: a teammate took (or returned) one of our goals. */
840
+ onTeamEvent(e) {
841
+ // D82: a lane may have freed — queued goals try now, not at the next tick.
842
+ // A partial claim's rollback wakes nobody (two goals would wake each other forever).
843
+ if ((e.type === "lease_released" && !/^rollback/.test(String(e.meta.reason ?? ""))) || e.type === "goal_landed") {
844
+ for (const run of this.goals())
845
+ if (run.landing.state === "queued" && run.landing.landRequested)
846
+ void this.pollNow(run);
847
+ }
848
+ // D84: a check result or a merge on one of our PRs (a webhook, or polling) — look now
849
+ if (e.type === "check_failed" || e.type === "check_passed" || e.type === "pr_merged")
850
+ this.wakePr(e);
851
+ if (e.type !== "goal_adopted" && e.type !== "goal_returned")
852
+ return;
853
+ if (e.github === this.deps.github())
854
+ return;
855
+ const run = this.rt.orchestra.list().find((r) => r.landing && (r.id === e.meta.runId || r.landing.pr === Number(e.meta.pr)));
856
+ if (!run)
857
+ return;
858
+ if (e.type === "goal_adopted") {
859
+ this.set(run, { adoptedBy: e.github ?? "a teammate", state: "fixing" });
860
+ this.alert(run, `${e.github ?? "A teammate"} adopted PR #${run.landing.pr} to get it green`);
861
+ }
862
+ else {
863
+ this.set(run, { adoptedBy: undefined, state: "open", reason: undefined });
864
+ void this.rt.orchestra.pullPushed(run).catch(() => { });
865
+ this.alert(run, `${e.github ?? "A teammate"} handed PR #${run.landing.pr} back`);
866
+ }
867
+ }
868
+ wakePr(e) {
869
+ const n = Number(e.meta.number ?? e.meta.pr);
870
+ if (!Number.isInteger(n) || n <= 0)
871
+ return;
872
+ const runs = this.goals().filter((r) => r.landing.pr === n && !r.landing.adoptedBy);
873
+ if (!runs.length)
874
+ return;
875
+ void (async () => {
876
+ const share = await this.deps.share(this.rt).catch(() => null);
877
+ if (e.repo && share && e.repo !== share.repo)
878
+ return; // same number, another repo
879
+ for (const run of runs)
880
+ if (!this.busy.has(run.id))
881
+ void this.pollNow(run);
882
+ })();
883
+ }
884
+ // ── repo doctor (D62) ──
885
+ async doctor() {
886
+ const repo = await this.repo();
887
+ const dir = this.rt.info.dir;
888
+ const head = (await this.exec("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], dir)).out.trim() || "origin/main";
889
+ const branch = head.replace(/^origin\//, "");
890
+ let rules = null;
891
+ if (repo) {
892
+ const r = await this.exec("gh", ["api", `repos/${repo}/rules/branches/${branch}`], dir);
893
+ if (r.code === 0) {
894
+ try {
895
+ rules = JSON.parse(r.out);
896
+ }
897
+ catch {
898
+ rules = null;
899
+ }
900
+ }
901
+ }
902
+ const workflows = await this.workflows(head);
903
+ const fixable = Object.entries(workflows).filter(([, y]) => triggersPullRequest(y) && !triggersMergeGroup(y) && addMergeGroupTrigger(y)).map(([p]) => p);
904
+ return { repo, branch, findings: doctor({ branch, rules, workflows }), fixable };
905
+ }
906
+ async workflows(head) {
907
+ const dir = this.rt.info.dir;
908
+ const ls = await this.exec("git", ["ls-tree", "--name-only", `${head}:.github/workflows`], dir);
909
+ const out = {};
910
+ for (const f of ls.out.split("\n").filter((x) => /\.ya?ml$/.test(x))) {
911
+ const p = `.github/workflows/${f}`;
912
+ const t = await this.exec("git", ["show", `${head}:${p}`], dir);
913
+ if (t.code === 0)
914
+ out[p] = t.out;
915
+ }
916
+ return out;
917
+ }
918
+ /** Open the PR adding `merge_group:` to every pull-request workflow missing it. Never touches settings. */
919
+ async doctorFix() {
920
+ const d = await this.doctor();
921
+ if (!d.fixable.length)
922
+ return { prUrl: null, files: [] };
923
+ const dir = this.rt.info.dir;
924
+ await this.exec("git", ["fetch", "-q", "origin", d.branch], dir);
925
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "loom-doctor-"));
926
+ const wt = path.join(tmp, "wt");
927
+ const branch = "loom/doctor-merge-group";
928
+ try {
929
+ await this.git(["worktree", "add", "-q", "--detach", wt, `origin/${d.branch}`], dir);
930
+ for (const f of d.fixable) {
931
+ const abs = path.join(wt, f);
932
+ const next = addMergeGroupTrigger(fs.readFileSync(abs, "utf8"));
933
+ if (next)
934
+ fs.writeFileSync(abs, next);
935
+ }
936
+ await this.git(["add", ...d.fixable], wt);
937
+ await this.git(["-c", "user.name=Loom", "-c", "user.email=loom@users.noreply.github.com", "commit", "-q", "-m",
938
+ "ci: run workflows in the merge queue (merge_group)", "-m", "Queued PRs wait forever for required checks that don't run on merge_group. Opened by `loom team doctor --fix`."], wt);
939
+ await this.git(["push", "-q", "-f", "origin", `HEAD:refs/heads/${branch}`], wt);
940
+ const out = await this.gh(["pr", "create", "--base", d.branch, "--head", branch, "--title", "ci: run workflows in the merge queue",
941
+ "--body", `Adds \`merge_group:\` to ${d.fixable.map((f) => `\`${f}\``).join(", ")} so required checks also run for PRs in the merge queue.\n\nOpened by \`loom team doctor --fix\` (Loom Teams D62). It changes no repo settings.`], wt);
942
+ return { prUrl: out.match(/https:\/\/\S+/)?.[0] ?? null, files: d.fixable };
943
+ }
944
+ finally {
945
+ await this.exec("git", ["worktree", "remove", "--force", wt], dir);
946
+ fs.rmSync(tmp, { recursive: true, force: true });
947
+ }
948
+ }
949
+ // ── plumbing ──
950
+ mustGoal(runId) {
951
+ const run = this.rt.orchestra.get(runId);
952
+ if (!run)
953
+ throw new Error(`no goal "${runId}"`);
954
+ if (!run.landing) {
955
+ const url = run.delivered?.prUrl;
956
+ const n = Number(/\/pull\/(\d+)/.exec(url ?? "")?.[1]);
957
+ if (!url || !n)
958
+ throw new Error(`goal ${runId} has no PR — deliver it as a PR first`);
959
+ this.rt.orchestra.setLanding(run.id, newLanding(n, url));
960
+ }
961
+ return run;
962
+ }
963
+ async repo() {
964
+ return (await this.deps.share(this.rt))?.repo ?? null;
965
+ }
966
+ async post(run, type, meta, dedupe) {
967
+ const hub = this.deps.hub();
968
+ const device = this.deps.deviceId();
969
+ const share = await this.deps.share(this.rt);
970
+ if (!hub || !share || !device)
971
+ return;
972
+ const keys = this.deps.keys(share.teamId);
973
+ const key = keys[keys.length - 1];
974
+ await hub
975
+ .appendFeed(share.teamId, {
976
+ repo: share.repo,
977
+ type,
978
+ meta,
979
+ deviceId: device,
980
+ ...(key ? { sealed: sealForTeam(key, { goal: run.goal.split("\n")[0].slice(0, 200) }) } : {}),
981
+ ...(dedupe ? { dedupeKey: dedupe } : {}),
982
+ })
983
+ .catch((e) => logbook.warn("team", `couldn't post ${type}`, String(e)));
984
+ }
985
+ status() {
986
+ return this.rt.orchestra
987
+ .list()
988
+ .filter((r) => r.landing)
989
+ .map((r) => ({ runId: r.id, goal: r.goal.split("\n")[0].slice(0, 120), status: r.status, costUsd: r.costUsd, ...(r.from ? { adopted: r.from } : {}), landing: r.landing }));
990
+ }
991
+ }
992
+ //# sourceMappingURL=landing.js.map