elanous 0.2.4 → 0.2.6

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 (455) hide show
  1. package/README.md +7 -6
  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/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_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 +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  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/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -5,19 +5,28 @@ 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';
9
- import { diffFailures, parseFailures } from './gate-diff';
8
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
9
+ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
10
+ import { diffFailures, junitFailures, parseFailures } from './gate-diff';
11
+ import { planShards, readFileDurations } from './shard-plan';
10
12
  import { deriveCdpTestPatterns } from '../test-deterministic';
11
13
  import { emitNodeResult, readGraphContext } from './node-verdict.js';
14
+ import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
15
+ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
16
+ import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
17
+ import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
12
18
 
13
19
  interface CommandResult { rc: number; output: string }
14
20
  export interface GateRunner {
15
21
  command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
16
- sweep(tree: string, logDir?: string): Promise<CommandResult>;
22
+ localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
23
+ sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
17
24
  add(tree: string, commit: string): Promise<void>;
18
25
  remove(tree: string): Promise<void>;
19
26
  snapshot(tree: string, commit: string): Promise<void>;
20
27
  removeSnapshot(tree: string): Promise<void>;
28
+ /** Absolute remote bare mirror path, set before preparing the remote trees. */
29
+ remoteMirror?: string;
21
30
  }
22
31
  export interface GateOptions {
23
32
  commit: string;
@@ -25,8 +34,11 @@ export interface GateOptions {
25
34
  baselineVersion?: string;
26
35
  baselineCommit?: string;
27
36
  remote?: string;
37
+ remoteMirror?: string;
28
38
  instanceRoot?: string;
39
+ ledgerRoot?: string;
29
40
  repo?: string;
41
+ pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string };
30
42
  }
31
43
  export interface GateResult {
32
44
  outcome: 'ok' | 'regression' | 'error';
@@ -34,26 +46,49 @@ export interface GateResult {
34
46
  introduced: string[];
35
47
  preexisting: number;
36
48
  fixed: number;
49
+ baselineSource?: 'ledger' | 'instance' | 'swept';
37
50
  durationMs: number;
38
51
  error?: string;
52
+ stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
53
+ partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
39
54
  }
40
55
 
41
56
  const sha = /^[0-9a-f]{7,40}$/i;
42
57
  const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
43
58
  const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
44
59
  const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
60
+ const defaultRemoteMirror = '~/mirror/elanous-agent.git';
45
61
 
46
62
  function check(result: CommandResult, label: string): void {
47
63
  if (result.rc !== 0) throw new Error(`${label} failed (rc=${result.rc}): ${result.output.slice(-500)}`);
48
64
  }
49
65
 
50
66
  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);
67
+ // A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
68
+ // (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
69
+ // the sweep's own summary is always the «last» one, so read the last match, never the first.
70
+ const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
71
+ let last: RegExpExecArray | null = null;
72
+ for (const m of output.matchAll(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`))) last = m as RegExpExecArray;
73
+ return last;
74
+ };
75
+ const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
76
+ /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
77
+ const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
78
+ export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
52
79
  const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
80
+ const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
81
+ let last: string | undefined;
82
+ for (const line of output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
83
+ const header = /^(?:\.\/)?((?:[\w.-]+\/)*[\w.-]+\.test\.tsx?):/.exec(line.trim());
84
+ if (header && paths.includes(header[1]!)) last = header[1];
85
+ }
86
+ return last;
87
+ };
53
88
 
54
89
  function failuresOf(run: CommandResult, label: string): SweepFailures {
55
90
  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);
91
+ const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
57
92
  const reportedErrors = summaryCount(output, 'error');
58
93
  const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
59
94
  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 +116,222 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
81
116
  return { failures, errors: [...new Set(errors)].sort() };
82
117
  }
83
118
 
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 });
119
+ class StalledPodShards extends Error {
120
+ constructor(readonly stalledShards: NonNullable<GateResult['stalledShards']>, readonly partialSummary: NonNullable<GateResult['partialSummary']>) {
121
+ super(`pod sweep incomplete: stalled shards ${stalledShards.map(({ shard }) => shard).join(', ')}`);
122
+ }
123
+ }
124
+
125
+ export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
126
+ let remoteMirror: string | undefined;
127
+ const localCommand: GateRunner['command'] = async (cmd, args, cwd) => {
128
+ const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
90
129
  return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
91
- });
130
+ };
131
+ const command: GateRunner['command'] = commandOverride ?? (remote
132
+ ? async (cmd, args, cwd) => {
133
+ const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${[cmd, ...args].map(quote).join(' ')}`],
134
+ { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
135
+ return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
136
+ }
137
+ : localCommand);
138
+ const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
139
+ const shardCount = pod.shards ?? 24;
140
+ const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
141
+ if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
142
+ || !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds < 1) throw new Error('invalid pod sweep options');
143
+ const head = await command('git', ['rev-parse', 'HEAD'], tree);
144
+ check(head, 'cut tree HEAD');
145
+ const commit = head.output.trim();
146
+ if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('invalid cut tree HEAD');
147
+ const listed = await command('git', ['ls-files', '*.test.*'], tree);
148
+ check(listed, 'git ls-files tests');
149
+ const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
150
+ if (files.some((file) => file.startsWith('-') || file.startsWith('/') || file.startsWith('./')
151
+ || file.split('/').some((part) => !part || part === '.' || part === '..') || /[\r\n]/.test(file))) throw new Error('unsafe pod test path');
152
+ if (new Set(files).size !== files.length) throw new Error('duplicate pod test path');
153
+ if (!files.length) throw new Error('sweep incomplete: no tests ran');
154
+ const cdp = await command('rg', ['-l', '--glob', '*.test.ts', '-e', 'requireCdpBase', '-e', '9333', 'test', 'scripts'], tree);
155
+ if (cdp.rc !== 0 && cdp.rc !== 1) throw new Error(`CDP pattern discovery failed (rc=${cdp.rc}): ${tail40(cdp.output)}`);
156
+ const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
157
+ // CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
158
+ // bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
159
+ const assignable = files.filter((file) => !cdpPatterns.includes(file) && !file.split('/').some((part) => part.startsWith('.')));
160
+ if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
161
+ const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
162
+ const shards = planShards(assignable, durations, shardCount);
163
+ const known = assignable.filter((file) => durations.has(file)).length;
164
+ debug.log('release-loop.gate', 'pod-shard-plan', {
165
+ shards: shards.length, known, unknown: assignable.length - known,
166
+ maxPlannedSeconds: Math.max(...shards.map((shard) => shard.plannedSeconds)),
167
+ source: durations.size ? pod.durationSource : 'none',
168
+ });
169
+ const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
170
+ if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
171
+ const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
172
+ const bunCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim() || undefined;
173
+ const cachePrefix = bunCache ? `${podBunCacheVolume(bunCache).shellPrefix} ` : '';
174
+ const jobLogTail = (job: PodCommandResult): string => {
175
+ if (podLogTail) return podLogTail(job);
176
+ for (const member of poolScheduler.members) {
177
+ const logs = defaultKubectl(['--context', member.context, '-n', 'elanous-test', 'logs', `job/${job.job}`, '-c', 'child', '--tail=200']);
178
+ if (logs.status === 0 && logs.stdout) return logs.stdout;
179
+ }
180
+ return '';
181
+ };
182
+ type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
183
+ type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
184
+ // 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
185
+ const isolationRemaining = shards.map((item) => item.files.length);
186
+ const estimated = (paths: string[]) => paths.reduce((sum, file) => {
187
+ const value = durations.get(file);
188
+ return sum + (value !== undefined && Number.isFinite(value) && value >= 0 ? value : unknownSeconds);
189
+ }, 0);
190
+ const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
191
+ const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
192
+ const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
193
+ const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
194
+ .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
195
+ // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
196
+ const args = ['bun', 'run', 'test:deterministic', ...ignores, ...paths.map(asPath)].map(quote).join(' ')
197
+ + ' --reporter=junit --reporter-outfile="$HOME/outbox/junit.xml"';
198
+ const start = Date.now();
199
+ let output: string | undefined;
200
+ let junit: string | undefined;
201
+ let rc: number | undefined;
202
+ let jobExitCode: number | undefined;
203
+ let lastFile: string | undefined;
204
+ let jobFailed = false;
205
+ try {
206
+ const job = await podCommand({
207
+ pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
208
+ ...(bunCache ? { bunCache } : {}),
209
+ // 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
210
+ ...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
211
+ name: `gate-${randomUUID()}`,
212
+ command: ['bash', '-lc', `mkdir -p "$HOME/outbox"; ${cachePrefix}(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"`],
213
+ });
214
+ jobExitCode = job.exitCode;
215
+ const logPath = join(job.artifactsDir, 'shard.log');
216
+ const rcPath = join(job.artifactsDir, 'shard.rc');
217
+ output = existsSync(logPath) ? readFileSync(logPath, 'utf8') : undefined;
218
+ const junitPath = join(job.artifactsDir, 'junit.xml');
219
+ junit = existsSync(junitPath) ? readFileSync(junitPath, 'utf8') : undefined;
220
+ const rcText = existsSync(rcPath) ? readFileSync(rcPath, 'utf8').trim() : '';
221
+ rc = /^\d+$/.test(rcText) && Number.isSafeInteger(Number(rcText)) ? Number(rcText) : undefined;
222
+ lastFile = output === undefined ? undefined : lastStartedTestFile(output, paths);
223
+ if (!lastFile && (job.exitCode !== 0 || rc === undefined || (rc !== 0 && rc !== 1))) {
224
+ try { lastFile = lastStartedTestFile(jobLogTail(job).split(/\r?\n/).slice(-200).join('\n'), paths); }
225
+ catch { /* Log retrieval is diagnostic; do not hide the stalled shard. */ }
226
+ }
227
+ } catch {
228
+ jobFailed = true;
229
+ }
230
+ const durationMs = Date.now() - start;
231
+ if (logDir) {
232
+ mkdirSync(logDir, { recursive: true });
233
+ const destination = join(logDir, `pod-${shard}${branch}.log`);
234
+ if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
235
+ else rmSync(destination, { force: true });
236
+ writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths) }) + '\n', { mode: 0o600 });
237
+ const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
238
+ if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
239
+ else rmSync(junitDestination, { force: true });
240
+ }
241
+ debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
242
+ let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
243
+ const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
244
+ const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
245
+ const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
246
+ let reason: ShardReason | undefined;
247
+ let namedFailures: number | undefined;
248
+ let unattributedDetail: string | undefined;
249
+ if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
250
+ else if (!clean || rc === undefined) reason = 'no-output';
251
+ else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
252
+ || Number(ran[2]) === 0 || Number(ran[2]) > paths.length || Number(ran[1]) === 0) reason = 'incomplete';
253
+ else {
254
+ try {
255
+ const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
256
+ if (attributed.failures.length !== fails) reason = 'unattributed';
257
+ } catch (error) {
258
+ reason = 'unattributed';
259
+ // 어떤 수가 어긋났는지(요약·이름·오류 귀속) 첫 줄을 싣는다 — 수만으론 원인 파일을 못 찾았다(09-30 ① 1번 조각).
260
+ unattributedDetail = (error instanceof Error ? error.message : String(error)).split('\n', 1)[0]!.slice(0, 300);
261
+ }
262
+ if (reason === 'unattributed') namedFailures = [...clean.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
263
+ // 콘솔 요약은 실패를 세는데 `(fail)` 이름 줄이 하나도 없으면 같은 조각의 junit 에서 이름을 가져온다
264
+ // (10-01 0.2.6 게이트: `--dots` 로 도는 한 파일 조각이 이름 없이 끝나 게이트 전체가 error 로 멈췄다).
265
+ if (reason === 'unattributed' && namedFailures === 0 && junit) {
266
+ const named = junitFailures(junit);
267
+ if (named.length === fails) {
268
+ const patched = `${clean}\n${named.map((id) => { const cut = id.indexOf(' > '); return `${id.slice(0, cut)}:\n(fail) ${id.slice(cut + 3)}`; }).join('\n')}\n`;
269
+ try {
270
+ if (failuresOf({ rc, output: patched }, `pod-${shard} shard`).failures.length === fails) {
271
+ clean = patched;
272
+ reason = undefined;
273
+ debug.log('release-loop.gate', 'pod-shard-junit-attribution', { shard, files: paths, failures: named });
274
+ }
275
+ } catch { /* 그래도 못 맞추면 원래대로 멈춘다 */ }
276
+ }
277
+ }
278
+ }
279
+ if (reason) {
280
+ // 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
281
+ // 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
282
+ if (depth === 2 && paths.length > 1) {
283
+ const count = Math.min(paths.length, isolationRemaining[shard]!);
284
+ isolationRemaining[shard]! -= count;
285
+ const children = await Promise.all(paths.slice(0, count).map(async (file, index) => {
286
+ const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`);
287
+ debug.log('release-loop.gate', 'pod-shard-isolate', { shard, file, outcome: child.stalled[0]?.reason ?? 'ok' });
288
+ return child;
289
+ }));
290
+ return { runs: children.flatMap((child) => child.runs), stalled: [
291
+ ...children.flatMap((child) => child.stalled),
292
+ ...(paths.length > count ? [{ shard, files: paths.slice(count), reason,
293
+ ...(lastFile ? { lastFile } : {}),
294
+ }] : []),
295
+ ] };
296
+ }
297
+ if (depth === 2 || paths.length === 1) return { runs: [], stalled: [{ shard, files: paths, reason,
298
+ ...(lastFile ? { lastFile } : {}),
299
+ ...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
300
+ }] };
301
+ debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
302
+ const middle = Math.ceil(paths.length / 2);
303
+ const children = await Promise.all([
304
+ runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`),
305
+ runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`),
306
+ ]);
307
+ return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
308
+ }
309
+ return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
310
+ errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
311
+ ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
312
+ };
313
+ // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
314
+ const settled = await Promise.allSettled(shards.map((item, shard) => runShard(item.files, shard)));
315
+ const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
316
+ if (rejected) throw rejected.reason;
317
+ const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
318
+ const stalledShards = results.flatMap((result) => result.stalled);
319
+ const measured = results.flatMap((result) => result.runs);
320
+ const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
321
+ errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
322
+ { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
323
+ if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
324
+ if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
325
+ 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, ''));
326
+ 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` };
327
+ };
92
328
  return {
93
329
  command,
94
- async sweep(tree, logDir) {
330
+ localCommand: commandOverride && !remote ? commandOverride : localCommand,
331
+ get remoteMirror() { return remoteMirror; },
332
+ set remoteMirror(path) { remoteMirror = path; },
333
+ async sweep(tree, logDir, pod) {
334
+ if (pod) return podSweep(tree, logDir, pod);
95
335
  const listed = await command('git', ['ls-files', '*.test.*'], tree);
96
336
  check(listed, 'git ls-files tests');
97
337
  const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
@@ -114,7 +354,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
114
354
  const start = Date.now();
115
355
  const groupIgnores = cdpPatterns.filter((pattern) => group.paths.some((p) => pattern === p || pattern.startsWith(`${p}/`)))
116
356
  .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
117
- const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths], tree);
357
+ const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths.map(asPath)], tree);
118
358
  if (logDir) {
119
359
  mkdirSync(logDir, { recursive: true });
120
360
  writeFileSync(join(logDir, `${group.name}.log`), run.output, { mode: 0o600 });
@@ -124,7 +364,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
124
364
  try { failuresOf(run, `${group.name} shard`); }
125
365
  catch (error) { throw new Error(`${group.name} shard: ${error instanceof Error ? error.message : String(error)}`); }
126
366
  const clean = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
127
- const ran = /Ran (\d+) tests? across (\d+) files?/.exec(clean)!;
367
+ const ran = lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/)!;
128
368
  const passes = summaryCount(clean, 'pass');
129
369
  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
370
  total = { pass: total.pass + (Number.isNaN(passes) ? 0 : passes), fail: total.fail + summaryCount(clean, 'fail'),
@@ -135,14 +375,31 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
135
375
  if (!outputs.length || (total.ran === 0 && total.errors === 0)) throw new Error('sweep incomplete: no tests ran');
136
376
  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
377
  },
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'); },
378
+ async add(tree, commit) {
379
+ if (remote) {
380
+ if (!remoteMirror) throw new Error('remote mirror not prepared');
381
+ check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'remote tree clone');
382
+ check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'remote tree fetch');
383
+ check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'remote tree checkout');
384
+ } else check(await command('git', ['worktree', 'add', '--detach', tree, commit], repo), 'git worktree add');
385
+ },
386
+ async remove(tree) {
387
+ if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'remote tree cleanup');
388
+ else check(await command('git', ['worktree', 'remove', '--force', tree], repo), 'git worktree remove');
389
+ },
140
390
  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');
391
+ if (remote) {
392
+ if (!remoteMirror) throw new Error('remote mirror not prepared');
393
+ check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'baseline snapshot clone');
394
+ check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'baseline snapshot fetch');
395
+ check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'baseline snapshot checkout');
396
+ } else {
397
+ check(await command('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, tree], repo), 'baseline snapshot clone');
398
+ check(await command('git', ['checkout', '--quiet', '--detach', commit], tree), 'baseline snapshot checkout');
399
+ }
143
400
  },
144
401
  async removeSnapshot(tree) {
145
- if (remote) check(await command('rm', ['-r', '--', tree], repo), 'baseline snapshot cleanup');
402
+ if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'baseline snapshot cleanup');
146
403
  else rmSync(tree, { recursive: true, force: true });
147
404
  },
148
405
  };
@@ -179,7 +436,24 @@ function fileOf(id: string): string {
179
436
 
180
437
  export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
181
438
  const start = Date.now();
182
- const root = opts.instanceRoot ?? effectiveInstanceRoot();
439
+ const explicitConfig = getElanousConfigDirOverride();
440
+ const root = explicitConfig ? effectiveInstanceRoot() : (opts.instanceRoot ?? effectiveInstanceRoot());
441
+ const ledger = explicitConfig ? root : (opts.ledgerRoot ?? releaseLedgerRoot());
442
+ const roots = [...new Set([ledger, root])];
443
+ const cachedBaseline = (version: string, commit: string) => {
444
+ for (const [index, location] of roots.entries()) {
445
+ const saved = readBaseline(location, version);
446
+ if (saved?.commit === commit) return { saved, source: index === 0 && location !== root ? 'ledger' as const : 'instance' as const };
447
+ }
448
+ return undefined;
449
+ };
450
+ const baselineReleaseCommit = (version: string) => {
451
+ for (const location of roots) {
452
+ const path = join(location, 'release', version, 'release.json');
453
+ if (existsSync(path)) return baselineCommit(location, version);
454
+ }
455
+ return baselineCommit(root, version);
456
+ };
183
457
  const repo = resolve(opts.repo ?? process.cwd());
184
458
  const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
185
459
  let work: string | undefined;
@@ -191,24 +465,54 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
191
465
  || (opts.baselineVersion && !versionPattern.test(opts.baselineVersion))
192
466
  || (opts.baselineCommit && !sha.test(opts.baselineCommit))) throw new Error('invalid commit or version');
193
467
  if (!opts.baselineVersion) throw new Error('previous release version required (--baseline-version)');
468
+ if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ?? 24) || (opts.pod.shards ?? 24) < 1
469
+ || !Number.isSafeInteger(opts.pod.shardTimeoutSeconds ?? 1200) || (opts.pod.shardTimeoutSeconds ?? 1200) < 1)) throw new Error('invalid pod sweep options');
194
470
  if (opts.remote && !/^(?:[\w.-]+@)?[\w.-]+$/.test(opts.remote)) throw new Error('invalid ssh host');
195
471
  if (opts.remote) {
196
- const temporary = await runner.command('mktemp', ['-d', '/tmp/release-gate-XXXXXXXX'], repo);
472
+ runner.remoteMirror = undefined;
473
+ const configured = opts.remoteMirror ?? defaultRemoteMirror;
474
+ if (!/^(?:\/|~\/)[\w./-]+$/.test(configured) || configured.split('/').includes('..')) throw new Error('invalid remote mirror path');
475
+ const home = configured.startsWith('~/') ? await runner.command('sh', ['-c', 'printf "%s\\n" "$HOME"'], '/tmp') : undefined;
476
+ if (home) check(home, 'remote home');
477
+ const homePath = home?.output.split(/\r?\n/).find((line) => line.startsWith('/'));
478
+ if (home && (!homePath || !/^\/[^\r\n]*$/.test(homePath))) throw new Error('invalid remote home');
479
+ const mirror = configured.startsWith('~/') ? join(homePath!, configured.slice(2)) : configured;
480
+ const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
481
+ cachedBaseline(opts.baselineVersion, baseSha);
482
+ const present = await runner.command('test', ['-d', mirror], '/tmp');
483
+ if (present.rc === 1) {
484
+ check(await runner.command('mkdir', ['-p', dirname(mirror)], '/tmp'), 'remote mirror parent');
485
+ check(await runner.command('git', ['init', '--bare', mirror], '/tmp'), 'remote mirror init');
486
+ } else check(present, 'remote mirror lookup');
487
+ const bare = await runner.command('git', ['--git-dir', mirror, 'rev-parse', '--is-bare-repository'], '/tmp');
488
+ check(bare, 'remote mirror inspection');
489
+ if (!bare.output.split(/\r?\n/).includes('true')) throw new Error('remote mirror is not bare');
490
+ for (const commit of new Set([opts.commit, baseSha])) {
491
+ const ref = `refs/elanous/gate/${commit}`;
492
+ const found = await runner.command('git', ['--git-dir', mirror, 'show-ref', '--verify', '--quiet', ref], '/tmp');
493
+ if (found.rc === 1) check(await runner.localCommand('git', ['push', `${opts.remote}:${mirror}`, `${commit}:${ref}`], repo), `remote push ${commit}`);
494
+ else check(found, `remote mirror ref ${commit}`);
495
+ }
496
+ runner.remoteMirror = mirror;
497
+ const temporary = await runner.command('mktemp', ['-d', '/tmp/release-gate-XXXXXXXX'], '/tmp');
197
498
  check(temporary, 'remote mktemp');
198
- work = temporary.output.trim();
499
+ const paths = temporary.output.split(/\r?\n/).filter((line) => /^\/tmp\/release-gate-[\w-]+$/.test(line));
500
+ if (paths.length !== 1) throw new Error('could not create temporary work directory');
501
+ work = paths[0]!;
199
502
  } 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
503
  const cutTree = join(work, 'cut');
202
504
  trees.push(cutTree);
203
505
  await runner.add(cutTree, opts.commit);
204
506
  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'));
507
+ const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
508
+ opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
206
509
  const cut = failuresOf(cutRun, 'cut sweep');
207
510
  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;
511
+ const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
512
+ const cached = cachedBaseline(opts.baselineVersion, baseSha);
513
+ const trusted = cached?.saved;
514
+ result.baselineSource = trusted ? cached!.source : 'swept';
515
+ debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
212
516
  let baseTree: string | undefined;
213
517
  const getBaseTree = async () => {
214
518
  if (!baseTree) {
@@ -234,16 +538,16 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
234
538
  result.fixed = diff.fixed.length;
235
539
  result.preexisting = diff.common.length;
236
540
  for (const file of new Set(diff.newFailures.map(fileOf))) {
237
- const isolated = await runner.command('bun', ['run', 'test:deterministic', file], cutTree);
541
+ const isolated = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree);
238
542
  const isolatedCut = failuresOf(isolated, `cut isolated ${file}`);
239
543
  const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
240
544
  const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
241
545
  if (candidates.length === 0) continue;
242
546
  const baselineTree = await getBaseTree();
243
- const previous = await runner.command('bun', ['run', 'test:deterministic', file], baselineTree);
547
+ const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree);
244
548
  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);
549
+ if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
550
+ const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
247
551
  if (lookup.rc !== 0 || lookup.output.trim()) throw new Error(`baseline isolated run incomplete: ${file}`);
248
552
  oldFailures = new Set();
249
553
  } else {
@@ -258,6 +562,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
258
562
  result.outcome = result.introduced.length ? 'regression' : 'ok';
259
563
  } catch (error) {
260
564
  result.error = error instanceof Error ? error.message : String(error);
565
+ if (error instanceof StalledPodShards) {
566
+ result.stalledShards = error.stalledShards;
567
+ result.partialSummary = error.partialSummary;
568
+ }
261
569
  } finally {
262
570
  if (baseSnapshot && work) {
263
571
  try { await runner.removeSnapshot(join(work, 'baseline')); }
@@ -269,22 +577,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
269
577
  }
270
578
  if (work) {
271
579
  try {
272
- if (opts.remote) check(await runner.command('rmdir', [work], repo), 'remote temporary directory cleanup');
580
+ if (opts.remote) check(await runner.command('rm', ['-rf', '--', work], '/tmp'), 'remote temporary directory cleanup');
273
581
  else rmSync(work, { recursive: true, force: true });
274
582
  } catch (error) { result.outcome = 'error'; result.error = `cleanup: ${String(error)}`; }
275
583
  }
584
+ if (opts.remote) runner.remoteMirror = undefined;
276
585
  result.durationMs = Date.now() - start;
277
586
  }
278
587
  if (result.outcome !== 'error' && cutFailures) {
279
588
  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); }
589
+ for (const location of roots) {
590
+ const path = failureFile(location, opts.version);
591
+ mkdirSync(dirname(path), { recursive: true });
592
+ const temp = join(dirname(path), `.gate-failures-${process.pid}-${randomUUID()}.tmp`);
593
+ try {
594
+ writeFileSync(temp, JSON.stringify({ commit: opts.commit, failures: cutFailures.failures, ...(cutFailures.errors.length ? { errors: cutFailures.errors } : {}) }, null, 2) + '\n', { mode: 0o600 });
595
+ chmodSync(temp, 0o600);
596
+ renameSync(temp, path);
597
+ } finally { if (existsSync(temp)) rmSync(temp); }
598
+ }
288
599
  } catch (error) {
289
600
  result.outcome = 'error';
290
601
  result.error = `baseline persistence: ${String(error)}`;
@@ -313,20 +624,30 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
313
624
  baselineVersion: typeof fromGraph.previousVersion === 'string' ? fromGraph.previousVersion : undefined,
314
625
  baselineCommit: typeof fromGraph.previousCommit === 'string' ? fromGraph.previousCommit : undefined,
315
626
  remote: typeof fromGraph.gateRemote === 'string' ? fromGraph.gateRemote : undefined,
627
+ remoteMirror: typeof fromGraph.gateRemoteMirror === 'string' ? fromGraph.gateRemoteMirror : undefined,
628
+ pod: typeof fromGraph.gatePodPool === 'string' ? {
629
+ pool: fromGraph.gatePodPool,
630
+ ...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
631
+ ...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
632
+ } : undefined,
316
633
  };
317
634
  for (let i = 0; i < args.length; i++) {
318
635
  const arg = args[i];
319
636
  if (arg === '--help') return 'help';
320
637
  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!];
638
+ 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
639
  if (!key) throw new Error(`unknown option: ${arg}`);
323
640
  const value = args[++i];
324
641
  if (!value || value.startsWith('--')) throw new Error(`value required for ${arg}`);
325
- if (key === 'commit') opts.commit = value;
642
+ if (arg === '--pod-pool') opts.pod = { ...opts.pod, pool: value };
643
+ else if (arg === '--pod-shards') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shards: Number(value) };
644
+ else if (arg === '--pod-shard-timeout-seconds') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shardTimeoutSeconds: Number(value) };
645
+ else if (key === 'commit') opts.commit = value;
326
646
  else if (key === 'version') opts.version = value;
327
647
  else if (key === 'baselineVersion') opts.baselineVersion = value;
328
648
  else if (key === 'baselineCommit') opts.baselineCommit = value;
329
649
  else if (key === 'remote') opts.remote = value;
650
+ else if (key === 'remoteMirror') opts.remoteMirror = value;
330
651
  }
331
652
  return opts;
332
653
  }
@@ -338,7 +659,7 @@ if (import.meta.main) {
338
659
  try {
339
660
  const opts = parseOptions(process.argv.slice(2), process.env);
340
661
  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.');
662
+ 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
663
  process.exit(0);
343
664
  }
344
665
  version = opts.version;
@@ -20,9 +20,13 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
20
20
  export function runPublish(run: CommandRunner = runCommand) {
21
21
  const context = readGraphContext();
22
22
  const version = context.input.version;
23
- // ⛔ 승인 노드가 «approved» 를 남기지 않았으면 발행하지 않는다(그래프 밖에서 부르거나 간선이 바뀌어도).
24
- if (context.outputs['approve-publish']?.outcome !== 'approved') {
25
- return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: approve-publish is not approved' };
23
+ // A measured automatic decision or the existing human approval must authorize publication.
24
+ const auto = context.outputs['auto-approve'];
25
+ if (context.outputs['approve-publish']?.outcome !== 'approved' &&
26
+ !(auto?.outcome === 'ok' && auto.decidedBy === 'release-loop metrics'
27
+ && Array.isArray(auto.metrics) && auto.metrics.length === 8
28
+ && auto.metrics.every((metric: unknown) => metric && typeof metric === 'object' && 'verdict' in metric && metric.verdict === 'pass'))) {
29
+ return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: neither auto-approve nor approve-publish approved' };
26
30
  }
27
31
  for (const node of ['gate', 'pwa', 'upgrade', 'tui', 'prepare', 'docs']) {
28
32
  if (context.outputs[node]?.outcome !== 'ok') return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `publish blocked: ${node} outcome is not ok` };