elanous 0.2.6 → 0.2.7

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 (450) hide show
  1. package/.env.example +10 -7
  2. package/README.md +4 -0
  3. package/apps/pwa/out/404/index.html +1 -1
  4. package/apps/pwa/out/404.html +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
  18. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
  24. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/page-55d50e9b0006a91d.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
  95. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
  96. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
  97. package/apps/pwa/out/approvals/index.html +1 -1
  98. package/apps/pwa/out/approvals/index.txt +22 -22
  99. package/apps/pwa/out/autopilot/index.html +1 -1
  100. package/apps/pwa/out/autopilot/index.txt +22 -22
  101. package/apps/pwa/out/board/index.html +1 -1
  102. package/apps/pwa/out/board/index.txt +22 -22
  103. package/apps/pwa/out/botlab/index.html +1 -1
  104. package/apps/pwa/out/botlab/index.txt +21 -21
  105. package/apps/pwa/out/bots/index.html +1 -1
  106. package/apps/pwa/out/bots/index.txt +21 -21
  107. package/apps/pwa/out/chat/index.html +1 -1
  108. package/apps/pwa/out/chat/index.txt +21 -21
  109. package/apps/pwa/out/consult/index.html +1 -0
  110. package/apps/pwa/out/consult/index.txt +32 -0
  111. package/apps/pwa/out/control/index.html +1 -1
  112. package/apps/pwa/out/control/index.txt +22 -22
  113. package/apps/pwa/out/design-check/index.html +1 -1
  114. package/apps/pwa/out/design-check/index.txt +22 -22
  115. package/apps/pwa/out/editor/index.html +1 -1
  116. package/apps/pwa/out/editor/index.txt +22 -22
  117. package/apps/pwa/out/exec/index.html +1 -0
  118. package/apps/pwa/out/exec/index.txt +32 -0
  119. package/apps/pwa/out/field/index.html +1 -0
  120. package/apps/pwa/out/field/index.txt +32 -0
  121. package/apps/pwa/out/healthz +1 -1
  122. package/apps/pwa/out/index.html +1 -1
  123. package/apps/pwa/out/index.txt +21 -21
  124. package/apps/pwa/out/intake/index.html +1 -1
  125. package/apps/pwa/out/intake/index.txt +22 -22
  126. package/apps/pwa/out/live/index.html +1 -1
  127. package/apps/pwa/out/live/index.txt +22 -22
  128. package/apps/pwa/out/market/index.html +1 -1
  129. package/apps/pwa/out/market/index.txt +21 -21
  130. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  131. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  132. package/apps/pwa/out/morning/index.html +1 -1
  133. package/apps/pwa/out/morning/index.txt +22 -22
  134. package/apps/pwa/out/observatory/index.html +1 -1
  135. package/apps/pwa/out/observatory/index.txt +22 -22
  136. package/apps/pwa/out/reflection/index.html +1 -1
  137. package/apps/pwa/out/reflection/index.txt +22 -22
  138. package/apps/pwa/out/scheduler/index.html +1 -1
  139. package/apps/pwa/out/scheduler/index.txt +22 -22
  140. package/apps/pwa/out/sessions/index.html +1 -1
  141. package/apps/pwa/out/sessions/index.txt +21 -21
  142. package/apps/pwa/out/settings/devices/index.html +1 -1
  143. package/apps/pwa/out/settings/devices/index.txt +22 -22
  144. package/apps/pwa/out/settings/index.html +1 -1
  145. package/apps/pwa/out/settings/index.txt +22 -22
  146. package/apps/pwa/out/setup/done/index.html +1 -1
  147. package/apps/pwa/out/setup/done/index.txt +22 -22
  148. package/apps/pwa/out/setup/index.html +1 -1
  149. package/apps/pwa/out/setup/index.txt +22 -22
  150. package/apps/pwa/out/share/index.html +1 -1
  151. package/apps/pwa/out/share/index.txt +22 -22
  152. package/apps/pwa/out/showroom/index.html +1 -1
  153. package/apps/pwa/out/showroom/index.txt +21 -21
  154. package/apps/pwa/out/tasks/index.html +1 -1
  155. package/apps/pwa/out/tasks/index.txt +22 -22
  156. package/apps/pwa/out/term/index.html +1 -1
  157. package/apps/pwa/out/term/index.txt +22 -22
  158. package/apps/pwa/out/trace/index.html +1 -1
  159. package/apps/pwa/out/trace/index.txt +22 -22
  160. package/apps/pwa/out/vault/index.html +1 -1
  161. package/apps/pwa/out/vault/index.txt +22 -22
  162. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  163. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  164. package/apps/pwa/out/workflows/index.html +1 -1
  165. package/apps/pwa/out/workflows/index.txt +22 -22
  166. package/apps/pwa/out/workspace/index.html +1 -1
  167. package/apps/pwa/out/workspace/index.txt +22 -22
  168. package/apps/pwa/out/worktrees/index.html +1 -1
  169. package/apps/pwa/out/worktrees/index.txt +22 -22
  170. package/bin/elanous.cjs +107 -0
  171. package/graphs/field/field-feed.yaml +24 -0
  172. package/graphs/field/recipes.yaml +12 -0
  173. package/graphs/landing/landing-heal.yaml +3 -1
  174. package/graphs/landing/recipes.yaml +3 -0
  175. package/graphs/release/recipes.yaml +10 -0
  176. package/graphs/release/release-loop.yaml +10 -3
  177. package/package.json +5 -5
  178. package/plugins/card-followup/README.md +13 -4
  179. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  180. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  181. package/plugins/card-followup/graphs/run-step.ts +122 -18
  182. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  183. package/plugins/card-followup/plugin.json +1 -1
  184. package/plugins/doc-draft/README.md +13 -0
  185. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  186. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  187. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  188. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  189. package/plugins/doc-draft/plugin.json +24 -0
  190. package/plugins/geo-check/README.md +3 -1
  191. package/plugins/geo-check/graphs/run-step.ts +24 -3
  192. package/plugins/geo-check/plugin.json +1 -1
  193. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  194. package/plugins/job-coach/README.md +1 -1
  195. package/plugins/job-coach/graphs/run-step.ts +2 -2
  196. package/plugins/job-coach/plugin.json +1 -1
  197. package/plugins/job-coach/plugin.ts +1 -1
  198. package/scripts/affinity/endcard.sh +3 -2
  199. package/scripts/affinity/reframe.sh +3 -1
  200. package/scripts/ci-public-export-leak-gate.ts +48 -0
  201. package/scripts/codex-quota-alert.ts +122 -258
  202. package/scripts/coord-tracks.json +4 -4
  203. package/scripts/field-feed/deliver-node.ts +52 -0
  204. package/scripts/field-feed/feed-node.ts +67 -0
  205. package/scripts/field-feed/lib.ts +162 -0
  206. package/scripts/field-feed/reel-node.ts +24 -0
  207. package/scripts/field-feed/start.ts +62 -0
  208. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  209. package/scripts/install.sh +92 -13
  210. package/scripts/landing-heal/clean-checkout.ts +114 -0
  211. package/scripts/landing-heal/run-mission.ts +38 -3
  212. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  213. package/scripts/public-export.ts +8 -4
  214. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  215. package/scripts/release-loop/env-known-failures.ts +31 -0
  216. package/scripts/release-loop/gate-node.ts +81 -19
  217. package/scripts/release-loop/known-issues-node.ts +159 -0
  218. package/scripts/release-loop/npm-publish-node.ts +118 -0
  219. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  220. package/scripts/release-loop/release-readiness.ts +58 -0
  221. package/scripts/release-loop/unattended-release.ts +93 -0
  222. package/scripts/ux/measure-first-text.ts +102 -0
  223. package/scripts/vflow-collect.ts +20 -7
  224. package/scripts/vflow-search.ts +73 -0
  225. package/scripts/video-free-line-test-walker.ts +36 -0
  226. package/scripts/video-pipeline.ts +2 -2
  227. package/skills/explainer-video/SKILL.md +12 -9
  228. package/skills/explainer-video/engine/build.mjs +1 -1
  229. package/skills/explainer-video/engine/reel.mjs +138 -31
  230. package/skills/explainer-video/engine/reel.sh +2 -1
  231. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  232. package/skills/grill-me/SKILL.md +1 -1
  233. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  234. package/src/a2a/server.ts +2 -0
  235. package/src/acp/elanous-aware-peers.ts +30 -0
  236. package/src/acp/server.ts +91 -15
  237. package/src/agent/monad-agent-turn.ts +25 -6
  238. package/src/agent/shared-app-tools.ts +5 -3
  239. package/src/agent-substrate/review-intent.ts +6 -2
  240. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  241. package/src/auth/github-app-token.ts +4 -1
  242. package/src/autopilot/track-agent-shadow.ts +151 -0
  243. package/src/autopilot/track-agent.ts +23 -3
  244. package/src/boot/acp-server-help.ts +16 -0
  245. package/src/boot/daemon-prompt-turn.ts +31 -10
  246. package/src/boot/daemon-tools/index.ts +7 -1
  247. package/src/budget/codex-credit-pace.ts +84 -0
  248. package/src/budget/codex-credit-plan.ts +154 -0
  249. package/src/budget/codex-reset-credit-state.ts +2 -0
  250. package/src/chat/chat-cli.ts +4 -1
  251. package/src/chat/short-question.ts +40 -0
  252. package/src/cli/agent-cli.ts +24 -15
  253. package/src/cli/ask-bare.ts +42 -0
  254. package/src/cli/cli-user-error.ts +25 -0
  255. package/src/cli/codex-credit-plan-view.ts +25 -0
  256. package/src/cli/dependence-gauge.ts +237 -0
  257. package/src/cli/doctor-cli.ts +5 -0
  258. package/src/cli/doctor-modes.ts +48 -0
  259. package/src/cli/intake-cli.ts +12 -2
  260. package/src/cli/intake-source-cli.ts +58 -0
  261. package/src/cli/machine-cli.ts +23 -0
  262. package/src/cli/msg-cli.ts +137 -0
  263. package/src/cli/plugin-cli.ts +25 -0
  264. package/src/cli/pr-cli.ts +36 -0
  265. package/src/cli/provider-cli.ts +5 -0
  266. package/src/cli/release-cli.ts +42 -1
  267. package/src/cli/release-run-if-ready.ts +48 -0
  268. package/src/cli/setup-cli.ts +27 -6
  269. package/src/cli/setup-recommend.ts +8 -0
  270. package/src/cli/usage-cli.ts +11 -1
  271. package/src/codex/setup.ts +27 -8
  272. package/src/dashboard/index.ts +1 -1
  273. package/src/discord.ts +56 -6
  274. package/src/domains/core-tools.ts +37 -14
  275. package/src/domains/outbound-alert.ts +50 -11
  276. package/src/exec-requests/planner.ts +140 -0
  277. package/src/exec-requests/runner.ts +338 -0
  278. package/src/exec-requests/store.ts +76 -0
  279. package/src/field/field-media.ts +8 -3
  280. package/src/field/field-reel.ts +196 -0
  281. package/src/grok/credential.ts +7 -5
  282. package/src/harness/aside-run-status.ts +33 -0
  283. package/src/harness/browser-verify.ts +48 -17
  284. package/src/harness/harness-cli-command.ts +25 -3
  285. package/src/harness/image-load-state.ts +21 -0
  286. package/src/harness/process-garbage.ts +45 -0
  287. package/src/index.ts +86 -14
  288. package/src/instance/leader.ts +13 -1
  289. package/src/intake-plane/collect-github.ts +5 -3
  290. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  291. package/src/intake-plane/discord-seat-work.ts +56 -0
  292. package/src/intake-plane/intake-sources.ts +202 -0
  293. package/src/intake-plane/items.ts +28 -12
  294. package/src/intake-plane/seat-doc-route.ts +207 -0
  295. package/src/intake-plane/submit-intake-work.ts +9 -1
  296. package/src/intake-plane/telegram-seat-work.ts +72 -0
  297. package/src/llm/model-defaults.ts +24 -1
  298. package/src/llm.ts +5 -4
  299. package/src/machines/machine-ledger.ts +127 -0
  300. package/src/market/pack-internal-refs.ts +28 -0
  301. package/src/market/publish.ts +31 -4
  302. package/src/maturity/feature-maturity.ts +65 -0
  303. package/src/model-tier/turn-tier.ts +24 -0
  304. package/src/msg/msg-store.ts +149 -0
  305. package/src/native-tool-catalog.ts +2 -1
  306. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  307. package/src/nexus/api/consult-requests.ts +117 -0
  308. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  309. package/src/nexus/api/exec-requests.ts +51 -0
  310. package/src/nexus/api/field-uploads.ts +40 -3
  311. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  312. package/src/nexus/api/graph-approvals.ts +71 -4
  313. package/src/nexus/api/http-server.ts +61 -7
  314. package/src/nexus/api/seat-requests.ts +254 -0
  315. package/src/nexus/api/trace-run-scan.ts +37 -0
  316. package/src/nexus/api/trace.ts +19 -3
  317. package/src/nexus/index.ts +3 -0
  318. package/src/nexus/setup-status.ts +4 -5
  319. package/src/notify/llm-budget-alert.ts +16 -10
  320. package/src/oauth/codex-account-rotation.ts +26 -4
  321. package/src/oauth/codex-account-store.ts +20 -2
  322. package/src/oauth/codex-browser-login.ts +173 -0
  323. package/src/oauth/codex.ts +4 -1
  324. package/src/oauth/fallback-chain.ts +5 -5
  325. package/src/oauth/store.ts +42 -5
  326. package/src/onboarding/continue-here.ts +29 -0
  327. package/src/onboarding/entry-hints.ts +8 -0
  328. package/src/onboarding/full-screen-io.ts +35 -3
  329. package/src/onboarding/help/skills.md +1 -1
  330. package/src/onboarding/non-interactive.ts +1 -0
  331. package/src/onboarding/subscription-hint.ts +27 -0
  332. package/src/onboarding/web-first.ts +83 -0
  333. package/src/onboarding.ts +230 -23
  334. package/src/plugins/maker/node-install.ts +92 -0
  335. package/src/plugins/maker/node-maker.ts +139 -0
  336. package/src/plugins/maker/plugin-maker.ts +19 -7
  337. package/src/policy/llm-policy.ts +268 -0
  338. package/src/prompt-library/session-guidance.ts +7 -1
  339. package/src/prompt-library/universal-preamble.ts +2 -2
  340. package/src/relay/relay-cli.ts +20 -0
  341. package/src/relay/relay-server.ts +220 -0
  342. package/src/release-loop/checklist.ts +9 -8
  343. package/src/self-dev/decompose.ts +1 -0
  344. package/src/self-dev/dev-pipeline.ts +6 -3
  345. package/src/self-dev/run-record-mirror.ts +44 -0
  346. package/src/self-dev/run-store.ts +12 -3
  347. package/src/self-implement/harness-policy-gates.ts +4 -1
  348. package/src/self-implement/report-origin.ts +12 -1
  349. package/src/session/chat.ts +17 -5
  350. package/src/skills/shared-agent-skill-roots.ts +25 -0
  351. package/src/steward/failure-streak.ts +78 -0
  352. package/src/steward/steward-cards.ts +24 -0
  353. package/src/steward/triage-budget.ts +127 -0
  354. package/src/steward/triage-plan.ts +75 -0
  355. package/src/steward/triage.ts +244 -42
  356. package/src/task-cards/card-store.ts +1 -1
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  358. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  359. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  360. package/src/telegram.ts +38 -2
  361. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  362. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  363. package/src/user-config.ts +80 -4
  364. package/src/version/packed-revision.json +1 -1
  365. package/src/video-pipeline/recipes/character.ts +42 -5
  366. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  367. package/src/video-pipeline/recipes/free-line.ts +16 -3
  368. package/src/video-pipeline/recipes/remote.ts +7 -1
  369. package/src/video-pipeline/vflow-reference.ts +109 -0
  370. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  379. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  448. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  449. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  450. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -7,6 +7,7 @@ import { debug } from '../../src/debug/log.js';
7
7
  import { emitDecision } from '../../src/live/detail-switch.js';
8
8
  import { parse } from '../../src/agent-substrate/pr-comment-meta.js';
9
9
  import { classifyReviewRounds, matchMustFix, type MustFixVerdict } from './must-fix-match.js';
10
+ import { verifyCleanCheckout, type CheckoutResult } from './clean-checkout.js';
10
11
 
11
12
  interface PullRequest {
12
13
  number: number;
@@ -17,7 +18,7 @@ interface PullRequest {
17
18
  files: Array<{ path: string }>;
18
19
  }
19
20
  interface Match { pr: number; text: string; verdict: MustFixVerdict | 'rereviewed' | 'open'; run?: string; round?: number; resolvedIn?: number[] }
20
- interface GraphContext { runId?: string; input?: { windowMinutes?: number; mustFixByPr?: Record<string, string[]> }; outputs?: Record<string, unknown>; decision?: typeof emitDecision }
21
+ interface GraphContext { runId?: string; input?: { windowMinutes?: number; mustFixByPr?: Record<string, string[]>; postVerified?: boolean }; outputs?: Record<string, unknown>; decision?: typeof emitDecision }
21
22
  export type RunCommand = (args: string[]) => string;
22
23
  // 상주 루프(30분마다)는 사람 계정 한도를 쓰지 않는다 — 부른 쪽이 GH_TOKEN 을 주지 않았으면 GitHub App 설치 토큰으로(09-29 한도 소진).
23
24
  // 발급 함수마다 한 번(프로세스 전역 한 칸이면 앞선 호출의 진짜 토큰이 주입한 가짜를 이긴다 — 09-29 시험 실패 출력에 토큰이 찍혔다).
@@ -93,7 +94,7 @@ export function resolvedInPrs(body: string | undefined): number[] {
93
94
  return match ? [...match[1]!.matchAll(/#(\d+)/g)].map((m) => Number(m[1])) : [];
94
95
  }
95
96
 
96
- export function runMission(mission: string, ctx: GraphContext, run: RunCommand = gh, now = new Date()): Record<string, unknown> {
97
+ export function runMission(mission: string, ctx: GraphContext, run: RunCommand = gh, now = new Date(), verify = verifyCleanCheckout): Record<string, unknown> {
97
98
  switch (mission) {
98
99
  case 'collect': {
99
100
  const minutes = ctx.input?.windowMinutes ?? 60;
@@ -156,6 +157,37 @@ export function runMission(mission: string, ctx: GraphContext, run: RunCommand =
156
157
  }).map((pr) => pr.number);
157
158
  return { outcome: 'ok', verifyNeeded, verified };
158
159
  }
160
+ case 'clean-checkout': {
161
+ const needed = output(ctx, 'verify-needed').verifyNeeded as number[];
162
+ const candidates = pullRequests(ctx).filter((pr) => needed.includes(pr.number))
163
+ .sort((a, b) => Date.parse(a.mergedAt ?? '') - Date.parse(b.mergedAt ?? '') || a.number - b.number).slice(0, 2);
164
+ const results: CheckoutResult[] = [];
165
+ for (const pr of candidates) {
166
+ let mergeSha = '';
167
+ try {
168
+ const details = JSON.parse(run(['pr', 'view', String(pr.number), '--json', 'mergeCommit'])) as { mergeCommit?: { oid?: string } };
169
+ mergeSha = details.mergeCommit?.oid ?? '';
170
+ if (!/^[a-f0-9]{40,64}$/i.test(mergeSha)) throw new Error('mergeCommit.oid unavailable or invalid');
171
+ } catch (error) {
172
+ results.push({ pr: pr.number, mergeSha: '', ok: false, steps: [{ name: 'merge commit', ok: false, seconds: 0, tail: String(error).split('\n').slice(-20).join('\n') }] });
173
+ debug.log('landing-heal', 'clean-checkout', { pr: pr.number, ok: false, seconds: 0 });
174
+ continue;
175
+ }
176
+ let result: CheckoutResult;
177
+ try {
178
+ result = verify({ pr: pr.number, mergeSha, files: pr.files.map((file) => file.path) });
179
+ } catch (error) {
180
+ result = { pr: pr.number, mergeSha, ok: false, steps: [{ name: 'clean checkout', ok: false, seconds: 0, tail: String(error).split('\n').slice(-20).join('\n') }] };
181
+ }
182
+ results.push(result);
183
+ debug.log('landing-heal', 'clean-checkout', { pr: pr.number, ok: result.ok, seconds: result.steps.reduce((sum, step) => sum + step.seconds, 0) });
184
+ if (result.ok && ctx.input?.postVerified === true) {
185
+ const summary = result.steps.filter((step) => step.name !== 'worktree remove').map((step) => step.name).join(' · ');
186
+ run(['pr', 'comment', String(pr.number), '--body', `landing-verified: ${mergeSha.slice(0, 8)} · clean checkout · ${summary}`]);
187
+ }
188
+ }
189
+ return { outcome: 'ok', results };
190
+ }
159
191
  case 'report': {
160
192
  const collected = pullRequests(ctx);
161
193
  const prs = collected.length;
@@ -165,6 +197,9 @@ export function runMission(mission: string, ctx: GraphContext, run: RunCommand =
165
197
  const mustFixRereviewed = matches.filter((item) => item.verdict === 'rereviewed').length;
166
198
  const mustFixUnresolved = mustFixOpen + mustFixUnknown;
167
199
  const verifyNeeded = output(ctx, 'verify-needed').verifyNeeded as number[];
200
+ const checkoutResults = ctx.outputs?.['clean-checkout'] ? output(ctx, 'clean-checkout').results as CheckoutResult[] : undefined;
201
+ const checkoutPassed = checkoutResults?.filter((item) => item.ok).length ?? 0;
202
+ const checkoutFailed = checkoutResults?.filter((item) => !item.ok).map((item) => item.pr) ?? [];
168
203
  const mergedPrs = new Set(collected.filter((pr) => pr.state === 'MERGED').map((pr) => pr.number));
169
204
  const landedOpen = matches.filter((item) => item.verdict === 'open' && mergedPrs.has(item.pr));
170
205
  const unresolved = matches.filter((item) => item.verdict === 'unresolved').length;
@@ -174,7 +209,7 @@ export function runMission(mission: string, ctx: GraphContext, run: RunCommand =
174
209
  (ctx.decision ?? emitDecision)({
175
210
  kind: 'VERIFY',
176
211
  what: `착지·치유 관측: PR ${prs}건${open ? ` — 열린 must-fix ${open}건(PR ${openPrs.map((pr) => `#${pr}`).join(', ')})` : ''}`,
177
- reason: `열린 채 착지 ${open}건 · 미해결 ${unresolved}건 · 미상 ${mustFixUnknown}건 · 다시 봄 ${mustFixRereviewed}건 · 착지 뒤 검증 필요 ${verifyNeeded.length}건`,
212
+ reason: `열린 채 착지 ${open}건 · 미해결 ${unresolved}건 · 미상 ${mustFixUnknown}건 · 다시 봄 ${mustFixRereviewed}건 · 착지 뒤 검증 필요 ${verifyNeeded.length}건${checkoutResults ? ` · 검증 성공 ${checkoutPassed} · 실패 ${checkoutFailed.length}(PR ${checkoutFailed.map((pr) => `#${pr}`).join(', ') || '없음'})` : ''}`,
178
213
  purpose: '착지 후속 작업 관측', target: 'landing-heal', ...(ctx.runId ? { runId: ctx.runId } : {}),
179
214
  });
180
215
  return { outcome: 'ok', prs, mustFixUnresolved, mustFixOpen, mustFixUnknown, mustFixRereviewed, verifyNeeded };
@@ -43,7 +43,8 @@ const tab = 'button[aria-label^="switch to "]';
43
43
  const terminal = '.xterm';
44
44
  const connected = '[aria-live="polite"]';
45
45
  // ChatInput 의 입력창(`apps/pwa/src/components/chat/ChatInput.tsx` placeholder) — testid 가 없어 placeholder 로 고른다.
46
- const chatInput = 'textarea[placeholder^="message"]';
46
+ // Placeholder text is user copy (10-01 #22558 localized it and silently broke C1·C2a·C2b) — match both locales.
47
+ const chatInput = 'textarea[placeholder^="message"], textarea[placeholder^="메시지"]';
47
48
  const intakeField = '[data-testid="intake-front-door-field"]';
48
49
  const terminalIdentity = `(() => { const sessionId = localStorage.getItem('elanous.daemon.sessionId'); const terminalId = Array.from(document.querySelectorAll('button[aria-label^="switch to "]')).find(node => node.title?.includes('지금 보는 터미널'))?.getAttribute('aria-label')?.slice('switch to '.length); return sessionId && terminalId ? { sessionId, terminalId } : null; })()`;
49
50
  const identity = (value: unknown): { sessionId: string; terminalId: string } | null => {
@@ -11,6 +11,7 @@
11
11
  * 표지 = LEAK_MARKERS(줄 단위 흔적 · 문서 링크는 private 에 «실재»하는 이름만) — rc 는 이것만 센다.
12
12
  * 참고 = `runtime-path-outside-export`(공개 코드가 공개본에 없는 경로를 쥔다) — rc 에 안 센다 · 판정은 public-export-test-run 의 exportOnly=0.
13
13
  * bun scripts/public-export.ts --out <dir> # 트리 복사 ⊕ replace 적용 ⊕ 유출 검사(있으면 rc=1 · 복사는 한다)
14
+ * bun scripts/public-export.ts --leak-check --files a b … # 그 파일들만 — prepare 와 같은 변환·치환 «뒤»에 잰다(pr land 가 부른다)
14
15
  *
15
16
  * ⛔ 목록 밖은 «전부» private 에 남는다 — 이 스크립트가 무엇을 «더» 넣는 일은 없다.
16
17
  * ⛔ 원천은 `git ls-files`(추적 파일)다 — 작업 트리의 untracked·ignored 는 원리상 안 간다.
@@ -379,6 +380,9 @@ export function run(argv: readonly string[], root = resolve(import.meta.dir, '..
379
380
  const pathIndex = argv.indexOf('--path');
380
381
  const pathPrefix = pathIndex >= 0 ? argv[pathIndex + 1] : undefined;
381
382
  if (pathIndex >= 0 && !pathPrefix) { console.error('⛔ --path needs a prefix'); return 2; }
383
+ const filesIndex = argv.indexOf('--files');
384
+ const onlyFiles = filesIndex >= 0 ? new Set(argv.slice(filesIndex + 1).filter((a) => !a.startsWith('--'))) : undefined;
385
+ const inScope = (f: string) => (!pathPrefix || f.startsWith(pathPrefix)) && (!onlyFiles || onlyFiles.has(f));
382
386
  const outIndex = argv.indexOf('--out');
383
387
  const out = outIndex >= 0 ? argv[outIndex + 1] : undefined;
384
388
  if (outIndex >= 0 && !out) { console.error('⛔ --out needs a directory'); return 2; }
@@ -411,9 +415,9 @@ export function run(argv: readonly string[], root = resolve(import.meta.dir, '..
411
415
  if (!existsSync(join(root, src))) { console.error(`⛔ replace source missing: ${src}`); return 2; }
412
416
  contents.set(dest, readFileSync(join(root, src), 'utf8'));
413
417
  }
414
- const scanned = pathPrefix ? files.filter((f) => f.startsWith(pathPrefix)) : files;
418
+ const scanned = files.filter(inScope);
415
419
  const realDocs = privateDocNames(tracked, files);
416
- const before = [...scanLeaks(root, scanned, LEAK_MARKERS, undefined, realDocs), ...scanRuntimePaths(root, files, tracked).filter((h) => !pathPrefix || h.file.startsWith(pathPrefix))];
420
+ const before = [...scanLeaks(root, scanned, LEAK_MARKERS, undefined, realDocs), ...scanRuntimePaths(root, files, tracked).filter((h) => inScope(h.file))];
417
421
  const transformedByRules = transformExportFiles(contents, config, files);
418
422
  // ⭐ 개인 치환표(저장소 밖) — 규칙 변환 «뒤»에 건다. 없으면 «못 봤다»로 막는다.
419
423
  const redactions = loadPrivateRedactions();
@@ -445,12 +449,12 @@ export function run(argv: readonly string[], root = resolve(import.meta.dir, '..
445
449
  console.error(`exported ${files.length} files (+${Object.keys(config.replace).length} replaced) → ${target}`);
446
450
  }
447
451
  const hits = [
448
- ...gitIgnoredExportFiles(root, files).filter((f) => !pathPrefix || f.startsWith(pathPrefix)).map((file) => ({ marker: 'export-gitignored', file, line: 0, text: '공개본 .gitignore 에 걸린다 — 커밋하면 사라진다' })),
452
+ ...gitIgnoredExportFiles(root, files).filter(inScope).map((file) => ({ marker: 'export-gitignored', file, line: 0, text: '공개본 .gitignore 에 걸린다 — 커밋하면 사라진다' })),
449
453
  ...scanLeaks(root, scanned, LEAK_MARKERS, transformed.contents, realDocs),
450
454
  ...(redactions ? scanLeaks(root, scanned, privateIdentifierMarkers(redactions), transformed.contents, realDocs) : []),
451
455
  ...(!redactions && !noPrivateList ? [{ marker: 'private-identifiers-unchecked', file: privateRedactionsPath(), line: 0, text: 'private redaction list missing' }] : []),
452
456
  ];
453
- const advisory = scanRuntimePaths(root, files, tracked, transformed.contents).filter((h) => !pathPrefix || h.file.startsWith(pathPrefix));
457
+ const advisory = scanRuntimePaths(root, files, tracked, transformed.contents).filter((h) => inScope(h.file));
454
458
  const summary = summarizeLeaks(hits);
455
459
  const advisorySummary = summarizeLeaks(advisory);
456
460
  const beforeSummary = summarizeLeaks(before);
@@ -0,0 +1,92 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { junitFailures, parseFailures } from './gate-diff';
4
+
5
+ export function baselineFromCutLogs(dir: string, expectedCommit?: string): { failures: string[]; errors: string[]; files: number; complete: boolean } | undefined {
6
+ if (!existsSync(dir)) return undefined;
7
+ let names: string[];
8
+ try { names = readdirSync(dir); }
9
+ catch { return undefined; }
10
+ const metadata = (name: string): { rc?: unknown; files?: unknown; shardCount?: unknown; commit?: unknown } | undefined => {
11
+ try {
12
+ const value: unknown = JSON.parse(readFileSync(join(dir, name), 'utf8'));
13
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
14
+ } catch { return undefined; }
15
+ };
16
+ const logs = names.filter((name) => /^pod-\d+(?:-[\w-]+)?\.log$/.test(name));
17
+ if (!logs.length) return undefined;
18
+
19
+ // A split parent's output is partial; only its measured leaves belong to the cut.
20
+ const leaves = logs.filter((name) => !names.some((other) => other.startsWith(name.slice(0, -4) + '-')
21
+ && /^pod-\d+(?:-[\w-]+)?\.json$/.test(other)));
22
+ const failures = new Set<string>();
23
+ const errors = new Set<string>();
24
+ const files = new Set<string>();
25
+ let measured = true;
26
+ const measuredFiles = new Set<string>();
27
+ for (const name of leaves) {
28
+ const prefix = name.slice(0, -4);
29
+ const junitName = `${prefix}.junit.xml`;
30
+ let output: string;
31
+ let xml: string;
32
+ try {
33
+ output = readFileSync(join(dir, name), 'utf8');
34
+ xml = names.includes(junitName) ? readFileSync(join(dir, junitName), 'utf8') : '';
35
+ } catch {
36
+ measured = false;
37
+ continue;
38
+ }
39
+ const named = new Set([...parseFailures(output), ...junitFailures(xml)]);
40
+ for (const id of named) failures.add(id);
41
+ for (const match of xml.matchAll(/<testsuite\b[^>]*\bfile="([^"]+)"/g)) files.add(match[1]!);
42
+ const shard = names.includes(`${prefix}.json`) ? metadata(`${prefix}.json`) : undefined;
43
+ const ran = [...output.matchAll(/Ran (\d+) tests? across (\d+) files?/g)].at(-1);
44
+ const reported = [...output.matchAll(/(?:^|\n)\s*(\d+) fail\s*(?=\n|$)/g)].at(-1);
45
+ const junitFiles = new Set([...xml.matchAll(/<testsuite\b[^>]*\bfile="([^"]+)"/g)].map((match) => match[1]!));
46
+ if (!shard || (shard.rc !== 0 && shard.rc !== 1) || (expectedCommit && shard.commit !== expectedCommit)
47
+ || !ran || Number(ran[1]) < 1 || junitFiles.size !== Number(ran[2])
48
+ || !reported || Number(reported[1]) !== named.size || !Array.isArray(shard.files)
49
+ || !shard.files.every((file): file is string => typeof file === 'string')) measured = false;
50
+ else for (const file of shard.files) measuredFiles.add(file);
51
+ let file: string | undefined;
52
+ let attributedErrors = 0;
53
+ for (const raw of output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
54
+ const line = raw.trim();
55
+ const header = /^(?:\.\/)?((?:[\w.-]+\/)+[\w.-]+\.test\.tsx?):(?:\s|$)/.exec(line);
56
+ if (header) file = header[1];
57
+ if (/^# Unhandled error between tests/.test(line)) {
58
+ if (!file) measured = false;
59
+ else {
60
+ errors.add(`${file} > [error]`);
61
+ attributedErrors++;
62
+ file = undefined;
63
+ }
64
+ }
65
+ }
66
+ const reportedErrors = [...output.matchAll(/(?:^|\n)\s*(\d+) errors?\s*(?=\n|$)/g)].at(-1);
67
+ if ((reportedErrors ? Number(reportedErrors[1]) : 0) !== attributedErrors
68
+ || (shard?.rc === 1 && named.size + attributedErrors === 0)
69
+ || (shard?.rc === 0 && named.size + attributedErrors > 0)) measured = false;
70
+ }
71
+
72
+ const plannedFiles = new Set<string>();
73
+ const roots = names.filter((name) => /^pod-\d+\.json$/.test(name));
74
+ const allMetadata = names.filter((name) => /^pod-\d+(?:-[\w-]+)?\.json$/.test(name)).map(metadata);
75
+ if (allMetadata.some((shard) => !shard || (expectedCommit && shard.commit !== expectedCommit))) measured = false;
76
+ const rootMetadata = roots.map(metadata);
77
+ for (const shard of rootMetadata) {
78
+ if (!shard || (expectedCommit && shard.commit !== expectedCommit)
79
+ || !Array.isArray(shard.files) || !shard.files.every((file): file is string => typeof file === 'string')) measured = false;
80
+ else for (const file of shard.files) plannedFiles.add(file);
81
+ }
82
+ const count = rootMetadata[0]?.shardCount;
83
+ const complete = measured && rootMetadata.every((shard) => shard?.shardCount === count)
84
+ && roots.length === count
85
+ && plannedFiles.size > 0 && plannedFiles.size === measuredFiles.size
86
+ && [...plannedFiles].every((file) => measuredFiles.has(file))
87
+ && files.size === measuredFiles.size && [...measuredFiles].every((file) => files.has(file))
88
+ && typeof count === 'number' && Number.isSafeInteger(count) && count > 0
89
+ && Array.from({ length: count }, (_, index) => index).every((index) => names.includes(`pod-${index}.json`)
90
+ && leaves.some((name) => name.startsWith(`pod-${index}.`) || name.startsWith(`pod-${index}-`)));
91
+ return { failures: [...failures].sort(), errors: [...errors].sort(), files: files.size, complete };
92
+ }
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /** The identity is the complete Bun failure id, not its containing test file. */
5
+ export function loadEnvKnownFailures(repoRoot: string): Set<string> {
6
+ const path = join(repoRoot, 'test/env-known-failures.json');
7
+ if (!existsSync(path)) return new Set();
8
+ let data: unknown;
9
+ try { data = JSON.parse(readFileSync(path, 'utf8')); }
10
+ catch (error) { throw new Error(`invalid environment known failures: ${path}`, { cause: error }); }
11
+ if (!data || typeof data !== 'object' || Array.isArray(data)
12
+ || !('env' in data) || data.env !== 'linux-pod'
13
+ || !('measuredAt' in data) || typeof data.measuredAt !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(data.measuredAt)
14
+ || !('tests' in data) || !Array.isArray(data.tests)
15
+ || !data.tests.every((entry: unknown) => entry && typeof entry === 'object' && !Array.isArray(entry)
16
+ && 'id' in entry && typeof entry.id === 'string'
17
+ && /^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx? > \S.*$/.test(entry.id)
18
+ && 'reason' in entry && typeof entry.reason === 'string' && entry.reason.trim().length > 0)) {
19
+ throw new Error(`invalid environment known failures: ${path}`);
20
+ }
21
+ const ids = data.tests.map((entry: { id: string }) => entry.id);
22
+ if (new Set(ids).size !== ids.length) throw new Error(`invalid environment known failures: duplicate id in ${path}`);
23
+ return new Set(ids);
24
+ }
25
+
26
+ export function splitKnownEnv(failures: string[], known: Set<string>): { counted: string[]; knownEnv: string[] } {
27
+ return {
28
+ counted: failures.filter((id) => !known.has(id)),
29
+ knownEnv: failures.filter((id) => known.has(id)),
30
+ };
31
+ }
@@ -8,6 +8,8 @@ import { debug } from '../../src/debug/log.js';
8
8
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
9
9
  import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
10
10
  import { diffFailures, junitFailures, parseFailures } from './gate-diff';
11
+ import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
12
+ import { baselineFromCutLogs } from './baseline-from-logs';
11
13
  import { planShards, readFileDurations } from './shard-plan';
12
14
  import { deriveCdpTestPatterns } from '../test-deterministic';
13
15
  import { emitNodeResult, readGraphContext } from './node-verdict.js';
@@ -16,7 +18,7 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
16
18
  import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
17
19
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
18
20
 
19
- interface CommandResult { rc: number; output: string }
21
+ interface CommandResult { rc: number; output: string; passedIds?: string[] }
20
22
  export interface GateRunner {
21
23
  command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
22
24
  localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
@@ -38,7 +40,7 @@ export interface GateOptions {
38
40
  instanceRoot?: string;
39
41
  ledgerRoot?: string;
40
42
  repo?: string;
41
- pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string };
43
+ pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string };
42
44
  }
43
45
  export interface GateResult {
44
46
  outcome: 'ok' | 'regression' | 'error';
@@ -46,7 +48,9 @@ export interface GateResult {
46
48
  introduced: string[];
47
49
  preexisting: number;
48
50
  fixed: number;
49
- baselineSource?: 'ledger' | 'instance' | 'swept';
51
+ knownEnv: number;
52
+ knownEnvCleared: string[];
53
+ baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
50
54
  durationMs: number;
51
55
  error?: string;
52
56
  stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
@@ -64,6 +68,38 @@ function check(result: CommandResult, label: string): void {
64
68
  }
65
69
 
66
70
  type SweepFailures = { failures: string[]; errors: string[] };
71
+
72
+ /** Only named, completed JUnit cases can prove a listed failure passed in this cut. */
73
+ function junitPassedIds(xml: string): string[] {
74
+ const passed = new Set<string>();
75
+ const suites: string[] = [];
76
+ let file: string | undefined;
77
+ let open: { id: string; passed: boolean } | undefined;
78
+ const decode = (value: string) => value.replace(/&(?:quot|apos|lt|gt|amp|#\d+|#x[\da-fA-F]+);/g, (entity) => {
79
+ const named: Record<string, string> = { '&quot;': '"', '&apos;': "'", '&lt;': '<', '&gt;': '>', '&amp;': '&' };
80
+ if (entity in named) return named[entity]!;
81
+ const code = entity.startsWith('&#x') ? Number.parseInt(entity.slice(3, -1), 16) : Number(entity.slice(2, -1));
82
+ return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity;
83
+ });
84
+ for (const match of xml.matchAll(/<(\/?)\s*(testsuite|testcase|failure|error|skipped)\b([^>]*?)(\/?)>/g)) {
85
+ const [, closing, tag, attrs, selfClosing] = match;
86
+ const attr = (name: string) => { const found = new RegExp(`\\b${name}="([^"]*)"`).exec(attrs!); return found ? decode(found[1]!) : undefined; };
87
+ if (tag === 'testsuite') {
88
+ if (closing) { suites.pop(); if (!suites.length) file = undefined; }
89
+ else if (!selfClosing) { suites.push(attr('name') ?? ''); if (suites.length === 1) file = attr('file') ?? attr('name'); }
90
+ } else if (tag === 'testcase') {
91
+ if (closing) { if (open?.passed) passed.add(open.id); open = undefined; }
92
+ else {
93
+ const name = [...suites.slice(1), attr('name') ?? ''].join(' > ');
94
+ const path = attr('file') ?? file;
95
+ const id = path && name ? `${path} > ${name}` : undefined;
96
+ if (selfClosing) { if (id) passed.add(id); }
97
+ else open = id ? { id, passed: true } : undefined;
98
+ }
99
+ } else if (!closing && open) open.passed = false;
100
+ }
101
+ return [...passed].sort();
102
+ }
67
103
  // A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
68
104
  // (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
69
105
  // the sweep's own summary is always the «last» one, so read the last match, never the first.
@@ -76,6 +112,11 @@ const summaryCount = (output: string, label: string) => Number(lastMatch(output,
76
112
  /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
77
113
  const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
78
114
  export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
115
+ /** Integration tests that the Pod sweep does not run — each is judged by a real run elsewhere at the cut.
116
+ * 0.2.7 run 5 (10-01): `scripts/install.test.ts` (real `bun pm pack` → `bun add` installs, 301 s alone on node-b)
117
+ * sat in a shard of its own past the 1200 s deadline with no output and stopped the whole gate. Judged on the
118
+ * mbp at the cut instead (TD1 moves it to the integration line). Keep this list short and explained. */
119
+ export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
79
120
  const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
80
121
  const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
81
122
  let last: string | undefined;
@@ -92,10 +133,15 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
92
133
  const reportedErrors = summaryCount(output, 'error');
93
134
  const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
94
135
  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)}`);
95
- if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (run.rc !== 0 && run.rc !== 1)) throw incomplete('summary/exit');
96
136
  const count = summaryCount(output, 'fail');
97
137
  const failures = parseFailures(output);
98
138
  const occurrences = [...output.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
139
+ // A test that leaves process.exitCode set leaks a non-zero exit into a run whose own summary is complete and clean
140
+ // (0.2.7 gate: agent-cli at v0.2.6 · 12 pass · 0 fail · rc 2). The summary is the verdict; read that exit as clean.
141
+ const leakedExit = !!ran && run.rc !== 0 && run.rc !== 1 && count === 0 && errorCount === 0 && occurrences === 0
142
+ && !/^# Unhandled error between tests/m.test(output);
143
+ const rc = leakedExit ? 0 : run.rc;
144
+ if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (rc !== 0 && rc !== 1)) throw incomplete('summary/exit');
99
145
  // Bun reports file-level unhandled errors separately from test failures.
100
146
  const errors: string[] = [];
101
147
  let file: string | undefined;
@@ -110,7 +156,7 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
110
156
  }
111
157
  if (errors.length !== errorCount) throw incomplete(`error attribution identified=${errors.length}`);
112
158
  if (!Number.isFinite(count) || count !== occurrences || (count > 0 && failures.length === 0)
113
- || (run.rc === 1 && count === 0 && errorCount === 0) || (run.rc === 0 && count + errorCount > 0)) {
159
+ || (rc === 1 && count === 0 && errorCount === 0) || (rc === 0 && count + errorCount > 0)) {
114
160
  throw incomplete(`failure attribution incomplete: summary=${count}, identified=${failures.length}`);
115
161
  }
116
162
  return { failures, errors: [...new Set(errors)].sort() };
@@ -156,7 +202,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
156
202
  const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
157
203
  // CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
158
204
  // 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('.')));
205
+ const assignable = files.filter((file) => !cdpPatterns.includes(file) && !POD_SWEEP_INTEGRATION_ONLY.includes(file)
206
+ && !file.split('/').some((part) => part.startsWith('.')));
207
+ const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
208
+ if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
160
209
  if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
161
210
  const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
162
211
  const shards = planShards(assignable, durations, shardCount);
@@ -169,7 +218,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
169
218
  const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
170
219
  if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
171
220
  const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
172
- const bunCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim() || undefined;
221
+ const configCache = typeof pod.bunCache === 'string' ? pod.bunCache.trim() : '';
222
+ const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
223
+ const bunCache = configCache || envCache || undefined;
224
+ debug.log('release-loop.gate', 'pod-bun-cache', { source: configCache ? 'config' : envCache ? 'env' : 'none' });
173
225
  const cachePrefix = bunCache ? `${podBunCacheVolume(bunCache).shellPrefix} ` : '';
174
226
  const jobLogTail = (job: PodCommandResult): string => {
175
227
  if (podLogTail) return podLogTail(job);
@@ -179,7 +231,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
179
231
  }
180
232
  return '';
181
233
  };
182
- type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
234
+ type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number; passedIds: string[] };
183
235
  type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
184
236
  // 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
185
237
  const isolationRemaining = shards.map((item) => item.files.length);
@@ -233,7 +285,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
233
285
  const destination = join(logDir, `pod-${shard}${branch}.log`);
234
286
  if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
235
287
  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 });
288
+ writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths), shardCount: shards.length, commit }) + '\n', { mode: 0o600 });
237
289
  const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
238
290
  if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
239
291
  else rmSync(junitDestination, { force: true });
@@ -308,7 +360,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
308
360
  }
309
361
  return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
310
362
  errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
311
- ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
363
+ ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
312
364
  };
313
365
  // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
314
366
  const settled = await Promise.allSettled(shards.map((item, shard) => runShard(item.files, shard)));
@@ -323,7 +375,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
323
375
  if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
324
376
  if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
325
377
  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` };
378
+ 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`,
379
+ passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort() };
327
380
  };
328
381
  return {
329
382
  command,
@@ -455,7 +508,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
455
508
  return baselineCommit(root, version);
456
509
  };
457
510
  const repo = resolve(opts.repo ?? process.cwd());
458
- const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
511
+ const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
459
512
  let work: string | undefined;
460
513
  let baseSnapshot = false;
461
514
  let cutFailures: SweepFailures | undefined;
@@ -510,8 +563,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
510
563
  cutFailures = cut;
511
564
  const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
512
565
  const cached = cachedBaseline(opts.baselineVersion, baseSha);
513
- const trusted = cached?.saved;
514
- result.baselineSource = trusted ? cached!.source : 'swept';
566
+ const recorded = cached ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), baseSha);
567
+ const trusted = cached?.saved ?? (recorded?.complete ? recorded : undefined);
568
+ result.baselineSource = cached ? cached.source : recorded?.complete ? 'cut-logs' : 'swept';
515
569
  debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
516
570
  let baseTree: string | undefined;
517
571
  const getBaseTree = async () => {
@@ -534,7 +588,14 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
534
588
  baseline = failuresOf(baseRun, 'baseline sweep');
535
589
  }
536
590
  for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
537
- const diff = diffFailures([...cut.failures, ...cut.errors], [...baseline.failures, ...baseline.errors]);
591
+ const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
592
+ const cutIds = [...cut.failures, ...cut.errors];
593
+ const baselineIds = [...baseline.failures, ...baseline.errors];
594
+ const countedCut = splitKnownEnv(cutIds, known);
595
+ const countedBaseline = splitKnownEnv(baselineIds, known);
596
+ result.knownEnv = countedCut.knownEnv.length;
597
+ result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
598
+ const diff = diffFailures(countedCut.counted, countedBaseline.counted);
538
599
  result.fixed = diff.fixed.length;
539
600
  result.preexisting = diff.common.length;
540
601
  for (const file of new Set(diff.newFailures.map(fileOf))) {
@@ -584,7 +645,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
584
645
  if (opts.remote) runner.remoteMirror = undefined;
585
646
  result.durationMs = Date.now() - start;
586
647
  }
587
- if (result.outcome !== 'error' && cutFailures) {
648
+ if (cutFailures && !result.stalledShards?.length) {
588
649
  try {
589
650
  for (const location of roots) {
590
651
  const path = failureFile(location, opts.version);
@@ -608,10 +669,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
608
669
  export function graphGateResult(result: GateResult, graph: boolean) {
609
670
  return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
610
671
  verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
611
- summary: result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}` };
672
+ summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}` };
612
673
  }
613
674
 
614
- function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
675
+ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
615
676
  const context = env.ELANOUS_GRAPH_CONTEXT && args.every((arg) => arg === '--json') ? readGraphContext(env) : undefined;
616
677
  const fromGraph: Record<string, unknown> = context?.input ?? {};
617
678
  const graphCommit = context?.outputs['version-release']?.commit;
@@ -629,6 +690,7 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
629
690
  pool: fromGraph.gatePodPool,
630
691
  ...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
631
692
  ...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
693
+ ...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
632
694
  } : undefined,
633
695
  };
634
696
  for (let i = 0; i < args.length; i++) {
@@ -665,7 +727,7 @@ if (import.meta.main) {
665
727
  version = opts.version;
666
728
  result = await judgeGate(opts);
667
729
  } catch (error) {
668
- result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, durationMs: Date.now() - start, error: String(error) };
730
+ result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
669
731
  }
670
732
  if (result.error) console.error(result.error);
671
733
  debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });