elanous 0.2.4 → 0.2.5

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 (412) hide show
  1. package/README.md +3 -3
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
  12. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
  20. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
  43. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
  85. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +8 -1
  172. package/graphs/release/release-loop.yaml +6 -1
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  177. package/plugins/job-coach/README.md +12 -0
  178. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  179. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  180. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  181. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  182. package/plugins/job-coach/graphs/run-step.ts +211 -17
  183. package/plugins/job-coach/plugin.json +16 -3
  184. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  185. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  186. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  187. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  188. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  189. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  190. package/scripts/backup/elanous-backup.sh +4 -1
  191. package/scripts/build-docs-site.ts +4 -3
  192. package/scripts/coord-channel-watch.sh +23 -4
  193. package/scripts/coord-post.sh +49 -12
  194. package/scripts/coord-tracks.json +5 -5
  195. package/scripts/eval-bot-vs-grok.ts +79 -0
  196. package/scripts/eval-grok-live-baseline.ts +36 -0
  197. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  198. package/scripts/fleet/node-setup.sh +46 -7
  199. package/scripts/landing-heal/must-fix-match.ts +26 -0
  200. package/scripts/landing-heal/run-mission.ts +114 -20
  201. package/scripts/lib/front-route-measurement.ts +131 -0
  202. package/scripts/lib/tui-sim-keys.ts +42 -0
  203. package/scripts/measure-front-route-classifier.ts +56 -0
  204. package/scripts/media/ocr-leak-scan.sh +21 -0
  205. package/scripts/public-export.ts +2 -3
  206. package/scripts/public-import-pr.ts +80 -6
  207. package/scripts/publish-internal-dist.ts +2 -3
  208. package/scripts/red-sweep-diff.ts +1 -1
  209. package/scripts/release-loop/auto-approve-node.ts +72 -0
  210. package/scripts/release-loop/gate-node.ts +332 -44
  211. package/scripts/release-loop/publish-node.ts +7 -3
  212. package/scripts/release-loop/release-docs.ts +20 -13
  213. package/scripts/release-loop/test-durations.ts +63 -0
  214. package/scripts/release-loop/upgrade-node.ts +3 -2
  215. package/scripts/test-audit-coverage-per-file.sh +16 -0
  216. package/scripts/test-audit-meaningless.ts +88 -0
  217. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  218. package/scripts/test-deterministic.ts +27 -5
  219. package/scripts/tui-sim.ts +59 -14
  220. package/scripts/ux/bump-installed.sh +20 -0
  221. package/scripts/ux/lib/cdp.ts +72 -0
  222. package/scripts/ux/record-page.ts +50 -0
  223. package/skills/explainer-video/SKILL.md +99 -0
  224. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  225. package/skills/explainer-video/engine/build.mjs +431 -0
  226. package/skills/explainer-video/engine/vo.ts +37 -0
  227. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  228. package/src/agent/nested-agent-env.ts +1 -0
  229. package/src/agent-mission/driver.ts +95 -16
  230. package/src/agent-mission/mission-cli.ts +6 -0
  231. package/src/agent-mission/resource-ladder.ts +73 -25
  232. package/src/auth/acp-token.ts +16 -0
  233. package/src/auth/github-app-token.ts +112 -0
  234. package/src/autopilot/gh-rest-fallback.ts +177 -0
  235. package/src/autopilot/pr-manager.ts +51 -4
  236. package/src/boot/acp-server.ts +2 -16
  237. package/src/boot/daemon-tools/web-search.ts +2 -1
  238. package/src/boot/ws-bridge.ts +11 -0
  239. package/src/chat/index.ts +37 -8
  240. package/src/chat/log-scroll-key.ts +12 -0
  241. package/src/chat/submit-trace.ts +30 -0
  242. package/src/cli/agent-cli.ts +7 -2
  243. package/src/cli/decisions-cli.ts +94 -0
  244. package/src/cli/doctor-cli.ts +40 -54
  245. package/src/cli/doctor-readiness.ts +16 -24
  246. package/src/cli/intake-cli.ts +1 -1
  247. package/src/cli/mcp-call.ts +9 -0
  248. package/src/cli/mcp-gateway-cli.ts +7 -3
  249. package/src/cli/plugin-cli.ts +109 -11
  250. package/src/cli/pr-lineage.ts +2 -1
  251. package/src/cli/pwa-start.ts +3 -0
  252. package/src/cli/release-cli.ts +94 -7
  253. package/src/cli/self-update.ts +7 -4
  254. package/src/cli/start-cli.ts +33 -14
  255. package/src/dashboard/acp-boot.ts +3 -0
  256. package/src/dashboard/index.ts +62 -13
  257. package/src/dashboard/input/no-provider-reply.ts +13 -0
  258. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  259. package/src/dashboard/stale-install-check.ts +77 -0
  260. package/src/decisions/decision-ledger.ts +404 -0
  261. package/src/domains/outbound-alert.ts +8 -2
  262. package/src/domains/telegram-kind-route.ts +21 -0
  263. package/src/execution-loop/dispatch-task.ts +139 -0
  264. package/src/execution-loop/gate-decision-summary.ts +29 -0
  265. package/src/execution-loop/gates.ts +197 -0
  266. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  267. package/src/graph-kinds/plugin-nodes.ts +10 -2
  268. package/src/graph-runner/runner.ts +35 -5
  269. package/src/grounding/token.ts +12 -6
  270. package/src/harness/harness-cli-command.ts +68 -9
  271. package/src/harness/harness-pod-dispatch.ts +8 -2
  272. package/src/harness/harness-stop.ts +147 -18
  273. package/src/harness/self-send-decide.ts +26 -17
  274. package/src/harness/self-send-target.ts +22 -1
  275. package/src/harness/worktree-branch-prefix.ts +4 -3
  276. package/src/index.ts +56 -13
  277. package/src/instance/resolve.ts +6 -0
  278. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  279. package/src/intake-plane/digest.ts +11 -2
  280. package/src/live/detail-switch.ts +8 -3
  281. package/src/llm/usable-llm.ts +32 -0
  282. package/src/loops/registry.ts +27 -5
  283. package/src/market/publish.ts +59 -3
  284. package/src/native-tool-catalog.ts +2 -2
  285. package/src/nexus/api/graph-approvals.ts +125 -0
  286. package/src/nexus/api/http-server.ts +60 -6
  287. package/src/nexus/api/log-fabric.ts +10 -1
  288. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  289. package/src/nexus/api/merge-approvals.ts +2 -1
  290. package/src/nexus/api/meta-api.ts +14 -0
  291. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  292. package/src/nexus/api/plugins-market.ts +87 -3
  293. package/src/nexus/api/pod-credential-api.ts +68 -14
  294. package/src/nexus/api/public-routes.ts +7 -0
  295. package/src/nexus/api/trace.ts +57 -6
  296. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  297. package/src/nexus/index.ts +33 -32
  298. package/src/nexus/outbound/router.ts +1 -1
  299. package/src/onboarding.ts +25 -2
  300. package/src/plugins/core/host.ts +14 -0
  301. package/src/plugins/install/market-fetch.ts +4 -3
  302. package/src/plugins/install/plugin-credentials.ts +106 -0
  303. package/src/plugins/install/plugin-install.ts +163 -32
  304. package/src/prompt-library/universal-preamble.ts +8 -2
  305. package/src/release-loop/checklist.ts +191 -0
  306. package/src/release-loop/release-note.ts +6 -3
  307. package/src/self-dev/dev-cli.ts +40 -20
  308. package/src/self-dev/dev-pipeline.ts +77 -1
  309. package/src/self-implement/gate-baseline.ts +3 -1
  310. package/src/self-implement/goal-author.ts +18 -1
  311. package/src/self-implement/host-regate.ts +6 -2
  312. package/src/self-implement/orchestrator.ts +7 -2
  313. package/src/self-implement/seams.ts +4 -2
  314. package/src/skills/tools/dev-harness.ts +42 -0
  315. package/src/steward/steward-cards.ts +45 -0
  316. package/src/steward/triage.ts +50 -14
  317. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  318. package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
  319. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  320. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  321. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  322. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  323. package/src/telegram-report.ts +12 -6
  324. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  325. package/src/tui.ts +3 -2
  326. package/src/user-config.ts +28 -2
  327. package/src/util/spawn-sync-output.ts +15 -0
  328. package/src/version/code-revision.ts +1 -1
  329. package/src/version/packed-revision.json +1 -1
  330. package/src/web-search/free.ts +77 -0
  331. package/src/web-search/index.ts +2 -0
  332. package/src/workflow-runtime/deps-bridge.ts +4 -3
  333. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  334. package/src/workflow-runtime/types.ts +2 -1
  335. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  346. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  348. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  411. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  412. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bun
2
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { debug } from '../../src/debug/log.js';
5
+ import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
6
+ import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
7
+
8
+ export type MetricVerdict = 'pass' | 'fail' | 'unmeasured';
9
+ export interface ApprovalMetric { name: string; value: string; verdict: MetricVerdict }
10
+
11
+ const outcomeMetric = (name: string, node: Record<string, unknown> | undefined): ApprovalMetric => ({
12
+ name, value: node?.outcome === undefined ? 'missing' : String(node.outcome),
13
+ verdict: node?.outcome === undefined ? 'unmeasured' : node.outcome === 'ok' ? 'pass' : 'fail',
14
+ });
15
+
16
+ export function runAutoApprove(context: GraphContext = readGraphContext(), deps: { instanceRoot?: string; repo?: string; bunVersion?: string } = {}) {
17
+ const { version } = context.input;
18
+ const root = deps.instanceRoot ?? effectiveInstanceRoot();
19
+ const repo = deps.repo ?? resolve(import.meta.dir, '../..');
20
+ const gate = context.outputs.gate;
21
+ const prepare = context.outputs.prepare;
22
+ let pinned: string | undefined;
23
+ try { pinned = readFileSync(join(repo, '.bun-version'), 'utf8').trim(); } catch { pinned = undefined; }
24
+ const bunVersion = deps.bunVersion ?? Bun.version;
25
+ const out = prepare?.out;
26
+ let distCount: number | undefined;
27
+ if (prepare?.outcome === 'ok' && typeof out === 'string' && isAbsolute(out)
28
+ && resolve(out) === join(root, 'release', version, 'prepared')) {
29
+ try { distCount = readdirSync(join(out, 'dist'), { withFileTypes: true }).filter((entry) => entry.isFile()).length; }
30
+ catch { distCount = undefined; }
31
+ }
32
+ const gateErrorAbsent = gate && !Object.hasOwn(gate, 'error');
33
+ const metrics: ApprovalMetric[] = [
34
+ { name: 'gate regressions and measurement', value: JSON.stringify({ outcome: gate?.outcome ?? null, introduced: gate?.introduced ?? null, error: gate?.error ?? null }),
35
+ verdict: !gate || gate.outcome === undefined || !Array.isArray(gate.introduced) ? 'unmeasured'
36
+ : gate.outcome === 'ok' && gate.introduced.length === 0 && gateErrorAbsent ? 'pass' : 'fail' },
37
+ { name: 'bun version', value: `running=${bunVersion}; pinned=${pinned ?? 'missing'}`,
38
+ verdict: !pinned ? 'unmeasured' : bunVersion === pinned ? 'pass' : 'fail' },
39
+ outcomeMetric('upgrade', context.outputs.upgrade),
40
+ outcomeMetric('tui', context.outputs.tui),
41
+ outcomeMetric('notes-check', context.outputs['notes-check']),
42
+ { name: 'public leak and prepare completeness', value: `prepare=${prepare?.outcome ?? 'missing'}`,
43
+ verdict: prepare?.outcome === undefined ? 'unmeasured' : prepare.outcome === 'ok' ? 'pass' : 'fail' },
44
+ { name: 'docs notes page', value: `docs=${context.outputs.docs?.outcome ?? 'missing'}; notes=${String(context.outputs.docs?.notes ?? 'missing')}`,
45
+ verdict: context.outputs.docs?.outcome === undefined || context.outputs.docs.notes === undefined ? 'unmeasured'
46
+ : context.outputs.docs.outcome === 'ok' && typeof context.outputs.docs.notes === 'string' && context.outputs.docs.notes.trim() ? 'pass' : 'fail' },
47
+ { name: 'installation files', value: `dist files=${distCount ?? 'missing'}; prepare.out=${String(out ?? 'missing')}`,
48
+ verdict: prepare?.outcome !== 'ok' ? prepare?.outcome === undefined ? 'unmeasured' : 'fail'
49
+ : distCount === undefined ? 'unmeasured' : distCount === 5 ? 'pass' : 'fail' },
50
+ ];
51
+ const outcome = metrics.every((metric) => metric.verdict === 'pass') ? 'ok' as const : 'fail' as const;
52
+ const report = join(root, 'release', version, 'auto-approval.md');
53
+ const lines = [`# v${version} auto-approval`, '', `Outcome: ${outcome}`, '', '| # | Metric | Value | Verdict |', '|---|---|---|---|',
54
+ ...metrics.map((metric, i) => `| ${i + 1} | ${metric.name} | ${metric.value.replaceAll('|', '\\|').replace(/[\r\n]/g, ' ')} | ${metric.verdict} |`),
55
+ '', '## 되돌리는 법', `gh release edit v${version} --draft`, `git tag -d v${version} && git push origin :refs/tags/v${version}`, '해당 docs PR revert', ''];
56
+ mkdirSync(dirname(report), { recursive: true });
57
+ writeFileSync(report, lines.join('\n'));
58
+ debug.log('release-loop.approve', 'auto-decision', { version, outcome, metrics });
59
+ return { outcome, verdict: outcome === 'ok' ? 'pass' as const : 'fail' as const, summary: `auto approval ${outcome}: ${metrics.filter((metric) => metric.verdict === 'pass').length}/8`,
60
+ ...(outcome === 'ok' ? { decidedBy: 'release-loop metrics' as const } : {}), metrics, report };
61
+ }
62
+
63
+ if (import.meta.main) {
64
+ try {
65
+ const result = runAutoApprove();
66
+ emitNodeResult(result);
67
+ process.exitCode = result.outcome === 'ok' ? 0 : 1;
68
+ } catch (error) {
69
+ emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: `auto approval unmeasured: ${error instanceof Error ? error.message : String(error)}` });
70
+ process.exitCode = 1;
71
+ }
72
+ }
@@ -5,19 +5,26 @@ import { tmpdir } from 'node:os';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { debug } from '../../src/debug/log.js';
8
- import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
8
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
9
+ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
9
10
  import { diffFailures, parseFailures } from './gate-diff';
10
11
  import { deriveCdpTestPatterns } from '../test-deterministic';
11
12
  import { emitNodeResult, readGraphContext } from './node-verdict.js';
13
+ import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
14
+ import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
15
+ import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
12
16
 
13
17
  interface CommandResult { rc: number; output: string }
14
18
  export interface GateRunner {
15
19
  command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
16
- sweep(tree: string, logDir?: string): Promise<CommandResult>;
20
+ localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
21
+ sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
17
22
  add(tree: string, commit: string): Promise<void>;
18
23
  remove(tree: string): Promise<void>;
19
24
  snapshot(tree: string, commit: string): Promise<void>;
20
25
  removeSnapshot(tree: string): Promise<void>;
26
+ /** Absolute remote bare mirror path, set before preparing the remote trees. */
27
+ remoteMirror?: string;
21
28
  }
22
29
  export interface GateOptions {
23
30
  commit: string;
@@ -25,8 +32,11 @@ export interface GateOptions {
25
32
  baselineVersion?: string;
26
33
  baselineCommit?: string;
27
34
  remote?: string;
35
+ remoteMirror?: string;
28
36
  instanceRoot?: string;
37
+ ledgerRoot?: string;
29
38
  repo?: string;
39
+ pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number };
30
40
  }
31
41
  export interface GateResult {
32
42
  outcome: 'ok' | 'regression' | 'error';
@@ -34,26 +44,49 @@ export interface GateResult {
34
44
  introduced: string[];
35
45
  preexisting: number;
36
46
  fixed: number;
47
+ baselineSource?: 'ledger' | 'instance' | 'swept';
37
48
  durationMs: number;
38
49
  error?: string;
50
+ stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
51
+ partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
39
52
  }
40
53
 
41
54
  const sha = /^[0-9a-f]{7,40}$/i;
42
55
  const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
43
56
  const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
44
57
  const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
58
+ const defaultRemoteMirror = '~/mirror/elanous-agent.git';
45
59
 
46
60
  function check(result: CommandResult, label: string): void {
47
61
  if (result.rc !== 0) throw new Error(`${label} failed (rc=${result.rc}): ${result.output.slice(-500)}`);
48
62
  }
49
63
 
50
64
  type SweepFailures = { failures: string[]; errors: string[] };
51
- const summaryCount = (output: string, label: string) => Number(new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?:\\n|$)`).exec(output)?.[1] ?? NaN);
65
+ // A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
66
+ // (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
67
+ // the sweep's own summary is always the «last» one, so read the last match, never the first.
68
+ const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
69
+ let last: RegExpExecArray | null = null;
70
+ for (const m of output.matchAll(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`))) last = m as RegExpExecArray;
71
+ return last;
72
+ };
73
+ const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
74
+ /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
75
+ const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
76
+ export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
52
77
  const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
78
+ const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
79
+ let last: string | undefined;
80
+ for (const line of output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
81
+ const header = /^(?:\.\/)?((?:[\w.-]+\/)*[\w.-]+\.test\.tsx?):/.exec(line.trim());
82
+ if (header && paths.includes(header[1]!)) last = header[1];
83
+ }
84
+ return last;
85
+ };
53
86
 
54
87
  function failuresOf(run: CommandResult, label: string): SweepFailures {
55
88
  const output = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
56
- const ran = /Ran (\d+) tests? across ([1-9]\d*) files?/.exec(output);
89
+ const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
57
90
  const reportedErrors = summaryCount(output, 'error');
58
91
  const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
59
92
  const incomplete = (reason: string) => new Error(`${label} incomplete (rc=${run.rc}; Ran=${ran ? ran[0] : 'missing'}; errors=${Number.isNaN(reportedErrors) ? 'missing' : errorCount}; ${reason})\n${tail40(output)}`);
@@ -81,17 +114,192 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
81
114
  return { failures, errors: [...new Set(errors)].sort() };
82
115
  }
83
116
 
84
- /** Commands on the remote host use the same absolute repository path as the caller. */
85
- export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command']): GateRunner {
86
- const command: GateRunner['command'] = commandOverride ?? (async (cmd: string, args: string[], cwd: string): Promise<CommandResult> => {
87
- const executable = remote ? 'ssh' : cmd;
88
- const argv = remote ? [remote, `cd ${quote(cwd)} && ${[cmd, ...args].map(quote).join(' ')}`] : args;
89
- const run = spawnSync(executable, argv, { cwd: remote ? repo : cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
117
+ class StalledPodShards extends Error {
118
+ constructor(readonly stalledShards: NonNullable<GateResult['stalledShards']>, readonly partialSummary: NonNullable<GateResult['partialSummary']>) {
119
+ super(`pod sweep incomplete: stalled shards ${stalledShards.map(({ shard }) => shard).join(', ')}`);
120
+ }
121
+ }
122
+
123
+ export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
124
+ let remoteMirror: string | undefined;
125
+ const localCommand: GateRunner['command'] = async (cmd, args, cwd) => {
126
+ const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
90
127
  return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
91
- });
128
+ };
129
+ const command: GateRunner['command'] = commandOverride ?? (remote
130
+ ? async (cmd, args, cwd) => {
131
+ const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${[cmd, ...args].map(quote).join(' ')}`],
132
+ { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
133
+ return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
134
+ }
135
+ : localCommand);
136
+ const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
137
+ const shardCount = pod.shards ?? 8;
138
+ const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
139
+ if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
140
+ || !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds < 1) throw new Error('invalid pod sweep options');
141
+ const head = await command('git', ['rev-parse', 'HEAD'], tree);
142
+ check(head, 'cut tree HEAD');
143
+ const commit = head.output.trim();
144
+ if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('invalid cut tree HEAD');
145
+ const listed = await command('git', ['ls-files', '*.test.*'], tree);
146
+ check(listed, 'git ls-files tests');
147
+ const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
148
+ if (files.some((file) => file.startsWith('-') || file.startsWith('/') || file.startsWith('./')
149
+ || file.split('/').some((part) => !part || part === '.' || part === '..') || /[\r\n]/.test(file))) throw new Error('unsafe pod test path');
150
+ if (new Set(files).size !== files.length) throw new Error('duplicate pod test path');
151
+ if (!files.length) throw new Error('sweep incomplete: no tests ran');
152
+ const cdp = await command('rg', ['-l', '--glob', '*.test.ts', '-e', 'requireCdpBase', '-e', '9333', 'test', 'scripts'], tree);
153
+ if (cdp.rc !== 0 && cdp.rc !== 1) throw new Error(`CDP pattern discovery failed (rc=${cdp.rc}): ${tail40(cdp.output)}`);
154
+ const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
155
+ // CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
156
+ // bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
157
+ const assignable = files.filter((file) => !cdpPatterns.includes(file) && !file.split('/').some((part) => part.startsWith('.')));
158
+ if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
159
+ const shards = Array.from({ length: Math.min(shardCount, assignable.length) }, () => [] as string[]);
160
+ assignable.forEach((file, index) => shards[index % shards.length]!.push(file));
161
+ const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
162
+ if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
163
+ const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
164
+ const jobLogTail = (job: PodCommandResult): string => {
165
+ if (podLogTail) return podLogTail(job);
166
+ for (const member of poolScheduler.members) {
167
+ const logs = defaultKubectl(['--context', member.context, '-n', 'elanous-test', 'logs', `job/${job.job}`, '-c', 'child', '--tail=200']);
168
+ if (logs.status === 0 && logs.stdout) return logs.stdout;
169
+ }
170
+ return '';
171
+ };
172
+ type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
173
+ type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
174
+ // 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
175
+ const isolationRemaining = shards.map((paths) => paths.length);
176
+ const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
177
+ const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
178
+ .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
179
+ // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
180
+ const args = ['bun', 'run', 'test:deterministic', ...ignores, ...paths.map(asPath)].map(quote).join(' ')
181
+ + ' --reporter=junit --reporter-outfile="$HOME/outbox/junit.xml"';
182
+ const start = Date.now();
183
+ let output: string | undefined;
184
+ let junit: string | undefined;
185
+ let rc: number | undefined;
186
+ let jobExitCode: number | undefined;
187
+ let lastFile: string | undefined;
188
+ let jobFailed = false;
189
+ try {
190
+ const job = await podCommand({
191
+ pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
192
+ // 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
193
+ ...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
194
+ name: `gate-${randomUUID()}`,
195
+ command: ['bash', '-lc', `mkdir -p "$HOME/outbox"; (cd .. && cd repo && bun install && (cd apps/pwa && bun install) && ${args}) 2>&1 | tee "$HOME/outbox/shard.log"; echo \${PIPESTATUS[0]} > "$HOME/outbox/shard.rc"`],
196
+ });
197
+ jobExitCode = job.exitCode;
198
+ const logPath = join(job.artifactsDir, 'shard.log');
199
+ const rcPath = join(job.artifactsDir, 'shard.rc');
200
+ output = existsSync(logPath) ? readFileSync(logPath, 'utf8') : undefined;
201
+ const junitPath = join(job.artifactsDir, 'junit.xml');
202
+ junit = existsSync(junitPath) ? readFileSync(junitPath, 'utf8') : undefined;
203
+ const rcText = existsSync(rcPath) ? readFileSync(rcPath, 'utf8').trim() : '';
204
+ rc = /^\d+$/.test(rcText) && Number.isSafeInteger(Number(rcText)) ? Number(rcText) : undefined;
205
+ lastFile = output === undefined ? undefined : lastStartedTestFile(output, paths);
206
+ if (!lastFile && (job.exitCode !== 0 || rc === undefined || (rc !== 0 && rc !== 1))) {
207
+ try { lastFile = lastStartedTestFile(jobLogTail(job).split(/\r?\n/).slice(-200).join('\n'), paths); }
208
+ catch { /* Log retrieval is diagnostic; do not hide the stalled shard. */ }
209
+ }
210
+ } catch {
211
+ jobFailed = true;
212
+ }
213
+ const durationMs = Date.now() - start;
214
+ if (logDir) {
215
+ mkdirSync(logDir, { recursive: true });
216
+ const destination = join(logDir, `pod-${shard}${branch}.log`);
217
+ if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
218
+ else rmSync(destination, { force: true });
219
+ writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths }) + '\n', { mode: 0o600 });
220
+ const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
221
+ if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
222
+ else rmSync(junitDestination, { force: true });
223
+ }
224
+ debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1 });
225
+ const clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
226
+ const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
227
+ const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
228
+ const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
229
+ let reason: ShardReason | undefined;
230
+ let namedFailures: number | undefined;
231
+ let unattributedDetail: string | undefined;
232
+ if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
233
+ else if (!clean || rc === undefined) reason = 'no-output';
234
+ else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
235
+ || Number(ran[2]) === 0 || Number(ran[2]) > paths.length || Number(ran[1]) === 0) reason = 'incomplete';
236
+ else {
237
+ try {
238
+ const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
239
+ if (attributed.failures.length !== fails) reason = 'unattributed';
240
+ } catch (error) {
241
+ reason = 'unattributed';
242
+ // 어떤 수가 어긋났는지(요약·이름·오류 귀속) 첫 줄을 싣는다 — 수만으론 원인 파일을 못 찾았다(09-30 ① 1번 조각).
243
+ unattributedDetail = (error instanceof Error ? error.message : String(error)).split('\n', 1)[0]!.slice(0, 300);
244
+ }
245
+ if (reason === 'unattributed') namedFailures = [...clean.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
246
+ }
247
+ if (reason) {
248
+ // 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
249
+ // 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
250
+ if (depth === 2 && paths.length > 1) {
251
+ const count = Math.min(paths.length, isolationRemaining[shard]!);
252
+ isolationRemaining[shard]! -= count;
253
+ const children = await Promise.all(paths.slice(0, count).map(async (file, index) => {
254
+ const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`);
255
+ debug.log('release-loop.gate', 'pod-shard-isolate', { shard, file, outcome: child.stalled[0]?.reason ?? 'ok' });
256
+ return child;
257
+ }));
258
+ return { runs: children.flatMap((child) => child.runs), stalled: [
259
+ ...children.flatMap((child) => child.stalled),
260
+ ...(paths.length > count ? [{ shard, files: paths.slice(count), reason,
261
+ ...(lastFile ? { lastFile } : {}),
262
+ }] : []),
263
+ ] };
264
+ }
265
+ if (depth === 2 || paths.length === 1) return { runs: [], stalled: [{ shard, files: paths, reason,
266
+ ...(lastFile ? { lastFile } : {}),
267
+ ...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
268
+ }] };
269
+ debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
270
+ const middle = Math.ceil(paths.length / 2);
271
+ const children = await Promise.all([
272
+ runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`),
273
+ runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`),
274
+ ]);
275
+ return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
276
+ }
277
+ return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
278
+ errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
279
+ ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
280
+ };
281
+ // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
282
+ const settled = await Promise.allSettled(shards.map((paths, shard) => runShard(paths, shard)));
283
+ const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
284
+ if (rejected) throw rejected.reason;
285
+ const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
286
+ const stalledShards = results.flatMap((result) => result.stalled);
287
+ const measured = results.flatMap((result) => result.runs);
288
+ const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
289
+ errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
290
+ { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
291
+ if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
292
+ if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
293
+ const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
294
+ return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n` };
295
+ };
92
296
  return {
93
297
  command,
94
- async sweep(tree, logDir) {
298
+ localCommand: commandOverride && !remote ? commandOverride : localCommand,
299
+ get remoteMirror() { return remoteMirror; },
300
+ set remoteMirror(path) { remoteMirror = path; },
301
+ async sweep(tree, logDir, pod) {
302
+ if (pod) return podSweep(tree, logDir, pod);
95
303
  const listed = await command('git', ['ls-files', '*.test.*'], tree);
96
304
  check(listed, 'git ls-files tests');
97
305
  const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
@@ -114,7 +322,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
114
322
  const start = Date.now();
115
323
  const groupIgnores = cdpPatterns.filter((pattern) => group.paths.some((p) => pattern === p || pattern.startsWith(`${p}/`)))
116
324
  .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
117
- const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths], tree);
325
+ const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths.map(asPath)], tree);
118
326
  if (logDir) {
119
327
  mkdirSync(logDir, { recursive: true });
120
328
  writeFileSync(join(logDir, `${group.name}.log`), run.output, { mode: 0o600 });
@@ -124,7 +332,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
124
332
  try { failuresOf(run, `${group.name} shard`); }
125
333
  catch (error) { throw new Error(`${group.name} shard: ${error instanceof Error ? error.message : String(error)}`); }
126
334
  const clean = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
127
- const ran = /Ran (\d+) tests? across (\d+) files?/.exec(clean)!;
335
+ const ran = lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/)!;
128
336
  const passes = summaryCount(clean, 'pass');
129
337
  if (!Number.isFinite(passes) && Number(ran[1]) > 0) throw new Error(`${group.name} shard incomplete (rc=${run.rc}; pass summary missing)\n${tail40(clean)}`);
130
338
  total = { pass: total.pass + (Number.isNaN(passes) ? 0 : passes), fail: total.fail + summaryCount(clean, 'fail'),
@@ -135,14 +343,31 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
135
343
  if (!outputs.length || (total.ran === 0 && total.errors === 0)) throw new Error('sweep incomplete: no tests ran');
136
344
  return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n` };
137
345
  },
138
- async add(tree, commit) { check(await command('git', ['worktree', 'add', '--detach', tree, commit], repo), 'git worktree add'); },
139
- async remove(tree) { check(await command('git', ['worktree', 'remove', '--force', tree], repo), 'git worktree remove'); },
346
+ async add(tree, commit) {
347
+ if (remote) {
348
+ if (!remoteMirror) throw new Error('remote mirror not prepared');
349
+ check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'remote tree clone');
350
+ check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'remote tree fetch');
351
+ check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'remote tree checkout');
352
+ } else check(await command('git', ['worktree', 'add', '--detach', tree, commit], repo), 'git worktree add');
353
+ },
354
+ async remove(tree) {
355
+ if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'remote tree cleanup');
356
+ else check(await command('git', ['worktree', 'remove', '--force', tree], repo), 'git worktree remove');
357
+ },
140
358
  async snapshot(tree, commit) {
141
- check(await command('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, tree], repo), 'baseline snapshot clone');
142
- check(await command('git', ['checkout', '--quiet', '--detach', commit], tree), 'baseline snapshot checkout');
359
+ if (remote) {
360
+ if (!remoteMirror) throw new Error('remote mirror not prepared');
361
+ check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'baseline snapshot clone');
362
+ check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'baseline snapshot fetch');
363
+ check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'baseline snapshot checkout');
364
+ } else {
365
+ check(await command('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, tree], repo), 'baseline snapshot clone');
366
+ check(await command('git', ['checkout', '--quiet', '--detach', commit], tree), 'baseline snapshot checkout');
367
+ }
143
368
  },
144
369
  async removeSnapshot(tree) {
145
- if (remote) check(await command('rm', ['-r', '--', tree], repo), 'baseline snapshot cleanup');
370
+ if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'baseline snapshot cleanup');
146
371
  else rmSync(tree, { recursive: true, force: true });
147
372
  },
148
373
  };
@@ -179,7 +404,24 @@ function fileOf(id: string): string {
179
404
 
180
405
  export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
181
406
  const start = Date.now();
182
- const root = opts.instanceRoot ?? effectiveInstanceRoot();
407
+ const explicitConfig = getElanousConfigDirOverride();
408
+ const root = explicitConfig ? effectiveInstanceRoot() : (opts.instanceRoot ?? effectiveInstanceRoot());
409
+ const ledger = explicitConfig ? root : (opts.ledgerRoot ?? releaseLedgerRoot());
410
+ const roots = [...new Set([ledger, root])];
411
+ const cachedBaseline = (version: string, commit: string) => {
412
+ for (const [index, location] of roots.entries()) {
413
+ const saved = readBaseline(location, version);
414
+ if (saved?.commit === commit) return { saved, source: index === 0 && location !== root ? 'ledger' as const : 'instance' as const };
415
+ }
416
+ return undefined;
417
+ };
418
+ const baselineReleaseCommit = (version: string) => {
419
+ for (const location of roots) {
420
+ const path = join(location, 'release', version, 'release.json');
421
+ if (existsSync(path)) return baselineCommit(location, version);
422
+ }
423
+ return baselineCommit(root, version);
424
+ };
183
425
  const repo = resolve(opts.repo ?? process.cwd());
184
426
  const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
185
427
  let work: string | undefined;
@@ -191,24 +433,53 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
191
433
  || (opts.baselineVersion && !versionPattern.test(opts.baselineVersion))
192
434
  || (opts.baselineCommit && !sha.test(opts.baselineCommit))) throw new Error('invalid commit or version');
193
435
  if (!opts.baselineVersion) throw new Error('previous release version required (--baseline-version)');
436
+ if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ?? 8) || (opts.pod.shards ?? 8) < 1
437
+ || !Number.isSafeInteger(opts.pod.shardTimeoutSeconds ?? 1200) || (opts.pod.shardTimeoutSeconds ?? 1200) < 1)) throw new Error('invalid pod sweep options');
194
438
  if (opts.remote && !/^(?:[\w.-]+@)?[\w.-]+$/.test(opts.remote)) throw new Error('invalid ssh host');
195
439
  if (opts.remote) {
196
- const temporary = await runner.command('mktemp', ['-d', '/tmp/release-gate-XXXXXXXX'], repo);
440
+ runner.remoteMirror = undefined;
441
+ const configured = opts.remoteMirror ?? defaultRemoteMirror;
442
+ if (!/^(?:\/|~\/)[\w./-]+$/.test(configured) || configured.split('/').includes('..')) throw new Error('invalid remote mirror path');
443
+ const home = configured.startsWith('~/') ? await runner.command('sh', ['-c', 'printf "%s\\n" "$HOME"'], '/tmp') : undefined;
444
+ if (home) check(home, 'remote home');
445
+ const homePath = home?.output.split(/\r?\n/).find((line) => line.startsWith('/'));
446
+ if (home && (!homePath || !/^\/[^\r\n]*$/.test(homePath))) throw new Error('invalid remote home');
447
+ const mirror = configured.startsWith('~/') ? join(homePath!, configured.slice(2)) : configured;
448
+ const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
449
+ cachedBaseline(opts.baselineVersion, baseSha);
450
+ const present = await runner.command('test', ['-d', mirror], '/tmp');
451
+ if (present.rc === 1) {
452
+ check(await runner.command('mkdir', ['-p', dirname(mirror)], '/tmp'), 'remote mirror parent');
453
+ check(await runner.command('git', ['init', '--bare', mirror], '/tmp'), 'remote mirror init');
454
+ } else check(present, 'remote mirror lookup');
455
+ const bare = await runner.command('git', ['--git-dir', mirror, 'rev-parse', '--is-bare-repository'], '/tmp');
456
+ check(bare, 'remote mirror inspection');
457
+ if (!bare.output.split(/\r?\n/).includes('true')) throw new Error('remote mirror is not bare');
458
+ for (const commit of new Set([opts.commit, baseSha])) {
459
+ const ref = `refs/elanous/gate/${commit}`;
460
+ const found = await runner.command('git', ['--git-dir', mirror, 'show-ref', '--verify', '--quiet', ref], '/tmp');
461
+ if (found.rc === 1) check(await runner.localCommand('git', ['push', `${opts.remote}:${mirror}`, `${commit}:${ref}`], repo), `remote push ${commit}`);
462
+ else check(found, `remote mirror ref ${commit}`);
463
+ }
464
+ runner.remoteMirror = mirror;
465
+ const temporary = await runner.command('mktemp', ['-d', '/tmp/release-gate-XXXXXXXX'], '/tmp');
197
466
  check(temporary, 'remote mktemp');
198
- work = temporary.output.trim();
467
+ const paths = temporary.output.split(/\r?\n/).filter((line) => /^\/tmp\/release-gate-[\w-]+$/.test(line));
468
+ if (paths.length !== 1) throw new Error('could not create temporary work directory');
469
+ work = paths[0]!;
199
470
  } else work = mkdtempSync(join(tmpdir(), 'release-gate-'));
200
- if (!work || (opts.remote && !/^\/tmp\/release-gate-[\w-]+$/.test(work))) throw new Error('could not create temporary work directory');
201
471
  const cutTree = join(work, 'cut');
202
472
  trees.push(cutTree);
203
473
  await runner.add(cutTree, opts.commit);
204
474
  for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
205
- const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'));
475
+ const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'), opts.pod);
206
476
  const cut = failuresOf(cutRun, 'cut sweep');
207
477
  cutFailures = cut;
208
- const saved = readBaseline(root, opts.baselineVersion);
209
- const baseSha = opts.baselineCommit ?? baselineCommit(root, opts.baselineVersion);
210
- if (saved && opts.baselineCommit && saved.commit !== baseSha) throw new Error('cached baseline commit does not match requested baseline commit');
211
- const trusted = saved?.commit === baseSha ? saved : undefined;
478
+ const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
479
+ const cached = cachedBaseline(opts.baselineVersion, baseSha);
480
+ const trusted = cached?.saved;
481
+ result.baselineSource = trusted ? cached!.source : 'swept';
482
+ debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
212
483
  let baseTree: string | undefined;
213
484
  const getBaseTree = async () => {
214
485
  if (!baseTree) {
@@ -234,16 +505,16 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
234
505
  result.fixed = diff.fixed.length;
235
506
  result.preexisting = diff.common.length;
236
507
  for (const file of new Set(diff.newFailures.map(fileOf))) {
237
- const isolated = await runner.command('bun', ['run', 'test:deterministic', file], cutTree);
508
+ const isolated = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree);
238
509
  const isolatedCut = failuresOf(isolated, `cut isolated ${file}`);
239
510
  const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
240
511
  const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
241
512
  if (candidates.length === 0) continue;
242
513
  const baselineTree = await getBaseTree();
243
- const previous = await runner.command('bun', ['run', 'test:deterministic', file], baselineTree);
514
+ const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree);
244
515
  let oldFailures: Set<string>;
245
- if (/No tests found/i.test(previous.output) && previous.rc === 1) {
246
- const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], repo);
516
+ if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
517
+ const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
247
518
  if (lookup.rc !== 0 || lookup.output.trim()) throw new Error(`baseline isolated run incomplete: ${file}`);
248
519
  oldFailures = new Set();
249
520
  } else {
@@ -258,6 +529,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
258
529
  result.outcome = result.introduced.length ? 'regression' : 'ok';
259
530
  } catch (error) {
260
531
  result.error = error instanceof Error ? error.message : String(error);
532
+ if (error instanceof StalledPodShards) {
533
+ result.stalledShards = error.stalledShards;
534
+ result.partialSummary = error.partialSummary;
535
+ }
261
536
  } finally {
262
537
  if (baseSnapshot && work) {
263
538
  try { await runner.removeSnapshot(join(work, 'baseline')); }
@@ -269,22 +544,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
269
544
  }
270
545
  if (work) {
271
546
  try {
272
- if (opts.remote) check(await runner.command('rmdir', [work], repo), 'remote temporary directory cleanup');
547
+ if (opts.remote) check(await runner.command('rm', ['-rf', '--', work], '/tmp'), 'remote temporary directory cleanup');
273
548
  else rmSync(work, { recursive: true, force: true });
274
549
  } catch (error) { result.outcome = 'error'; result.error = `cleanup: ${String(error)}`; }
275
550
  }
551
+ if (opts.remote) runner.remoteMirror = undefined;
276
552
  result.durationMs = Date.now() - start;
277
553
  }
278
554
  if (result.outcome !== 'error' && cutFailures) {
279
555
  try {
280
- const path = failureFile(root, opts.version);
281
- mkdirSync(dirname(path), { recursive: true });
282
- const temp = join(dirname(path), `.gate-failures-${process.pid}-${randomUUID()}.tmp`);
283
- try {
284
- writeFileSync(temp, JSON.stringify({ commit: opts.commit, failures: cutFailures.failures, ...(cutFailures.errors.length ? { errors: cutFailures.errors } : {}) }, null, 2) + '\n', { mode: 0o600 });
285
- chmodSync(temp, 0o600);
286
- renameSync(temp, path);
287
- } finally { if (existsSync(temp)) rmSync(temp); }
556
+ for (const location of roots) {
557
+ const path = failureFile(location, opts.version);
558
+ mkdirSync(dirname(path), { recursive: true });
559
+ const temp = join(dirname(path), `.gate-failures-${process.pid}-${randomUUID()}.tmp`);
560
+ try {
561
+ writeFileSync(temp, JSON.stringify({ commit: opts.commit, failures: cutFailures.failures, ...(cutFailures.errors.length ? { errors: cutFailures.errors } : {}) }, null, 2) + '\n', { mode: 0o600 });
562
+ chmodSync(temp, 0o600);
563
+ renameSync(temp, path);
564
+ } finally { if (existsSync(temp)) rmSync(temp); }
565
+ }
288
566
  } catch (error) {
289
567
  result.outcome = 'error';
290
568
  result.error = `baseline persistence: ${String(error)}`;
@@ -313,20 +591,30 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
313
591
  baselineVersion: typeof fromGraph.previousVersion === 'string' ? fromGraph.previousVersion : undefined,
314
592
  baselineCommit: typeof fromGraph.previousCommit === 'string' ? fromGraph.previousCommit : undefined,
315
593
  remote: typeof fromGraph.gateRemote === 'string' ? fromGraph.gateRemote : undefined,
594
+ remoteMirror: typeof fromGraph.gateRemoteMirror === 'string' ? fromGraph.gateRemoteMirror : undefined,
595
+ pod: typeof fromGraph.gatePodPool === 'string' ? {
596
+ pool: fromGraph.gatePodPool,
597
+ ...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
598
+ ...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
599
+ } : undefined,
316
600
  };
317
601
  for (let i = 0; i < args.length; i++) {
318
602
  const arg = args[i];
319
603
  if (arg === '--help') return 'help';
320
604
  if (arg === '--json') continue;
321
- const key = ({ '--commit': 'commit', '--version': 'version', '--baseline-version': 'baselineVersion', '--baseline-commit': 'baselineCommit', '--remote': 'remote' } as Record<string, keyof GateOptions>)[arg!];
605
+ const key = ({ '--commit': 'commit', '--version': 'version', '--baseline-version': 'baselineVersion', '--baseline-commit': 'baselineCommit', '--remote': 'remote', '--remote-mirror': 'remoteMirror', '--pod-pool': 'pod', '--pod-shards': 'pod', '--pod-shard-timeout-seconds': 'pod' } as Record<string, keyof GateOptions>)[arg!];
322
606
  if (!key) throw new Error(`unknown option: ${arg}`);
323
607
  const value = args[++i];
324
608
  if (!value || value.startsWith('--')) throw new Error(`value required for ${arg}`);
325
- if (key === 'commit') opts.commit = value;
609
+ if (arg === '--pod-pool') opts.pod = { ...opts.pod, pool: value };
610
+ else if (arg === '--pod-shards') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shards: Number(value) };
611
+ else if (arg === '--pod-shard-timeout-seconds') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shardTimeoutSeconds: Number(value) };
612
+ else if (key === 'commit') opts.commit = value;
326
613
  else if (key === 'version') opts.version = value;
327
614
  else if (key === 'baselineVersion') opts.baselineVersion = value;
328
615
  else if (key === 'baselineCommit') opts.baselineCommit = value;
329
616
  else if (key === 'remote') opts.remote = value;
617
+ else if (key === 'remoteMirror') opts.remoteMirror = value;
330
618
  }
331
619
  return opts;
332
620
  }
@@ -338,7 +626,7 @@ if (import.meta.main) {
338
626
  try {
339
627
  const opts = parseOptions(process.argv.slice(2), process.env);
340
628
  if (opts === 'help') {
341
- console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
629
+ console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--remote-mirror <path>] [--pod-pool <pool>] [--pod-shards <n>] [--pod-shard-timeout-seconds <n>] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
342
630
  process.exit(0);
343
631
  }
344
632
  version = opts.version;