elanous 0.2.6 → 0.2.8

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 (517) 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-3084c58c1725a40b.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/7032-f4ef698518fba61c.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.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-9aa47b0bc6d570ff.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.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-6a8158b71d1fe9d7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.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-085847b088675978.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.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/5ef72a618a8994f3.css +1 -0
  96. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_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/graphs/test-diet/recipes.yaml +9 -0
  178. package/graphs/test-diet/test-diet.yaml +22 -0
  179. package/package.json +6 -5
  180. package/persona-presets/_index.yaml +10 -0
  181. package/persona-presets/_schema.json +146 -0
  182. package/persona-presets/dana.yaml +26 -0
  183. package/persona-presets/haru.yaml +27 -0
  184. package/persona-presets/kai.yaml +27 -0
  185. package/persona-presets/leo.yaml +27 -0
  186. package/persona-presets/mira.yaml +30 -0
  187. package/persona-presets/nari.yaml +27 -0
  188. package/persona-presets/sora.yaml +27 -0
  189. package/persona-presets/yuna.yaml +27 -0
  190. package/plugins/card-followup/README.md +13 -4
  191. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  192. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  193. package/plugins/card-followup/graphs/run-step.ts +122 -18
  194. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  195. package/plugins/card-followup/plugin.json +1 -1
  196. package/plugins/doc-draft/README.md +13 -0
  197. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  198. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  199. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  200. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  201. package/plugins/doc-draft/plugin.json +24 -0
  202. package/plugins/geo-check/README.md +3 -1
  203. package/plugins/geo-check/graphs/run-step.ts +58 -13
  204. package/plugins/geo-check/plugin.json +1 -1
  205. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  206. package/plugins/job-coach/README.md +1 -1
  207. package/plugins/job-coach/graphs/run-step.ts +2 -2
  208. package/plugins/job-coach/plugin.json +1 -1
  209. package/plugins/job-coach/plugin.ts +1 -1
  210. package/scripts/affinity/endcard.sh +3 -2
  211. package/scripts/affinity/reframe.sh +3 -1
  212. package/scripts/ci-public-export-leak-gate.ts +48 -0
  213. package/scripts/codex-quota-alert.ts +122 -258
  214. package/scripts/coord-tracks.json +4 -4
  215. package/scripts/field-feed/deliver-node.ts +57 -0
  216. package/scripts/field-feed/feed-node.ts +67 -0
  217. package/scripts/field-feed/lib.ts +164 -0
  218. package/scripts/field-feed/reel-node.ts +24 -0
  219. package/scripts/field-feed/start.ts +62 -0
  220. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  221. package/scripts/install.sh +109 -21
  222. package/scripts/landing-heal/clean-checkout.ts +114 -0
  223. package/scripts/landing-heal/run-mission.ts +38 -3
  224. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  225. package/scripts/persona-presets/lint.ts +79 -0
  226. package/scripts/public-export.ts +8 -4
  227. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  228. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  229. package/scripts/release-loop/env-known-failures.ts +31 -0
  230. package/scripts/release-loop/gate-node.ts +108 -22
  231. package/scripts/release-loop/known-issues-node.ts +159 -0
  232. package/scripts/release-loop/npm-publish-node.ts +124 -0
  233. package/scripts/release-loop/ops-upgrade-node.ts +95 -0
  234. package/scripts/release-loop/publish-node.ts +23 -1
  235. package/scripts/release-loop/release-readiness.ts +58 -0
  236. package/scripts/release-loop/shard-plan.ts +17 -0
  237. package/scripts/release-loop/unattended-release.ts +93 -0
  238. package/scripts/storyboard/storyboard.ts +167 -0
  239. package/scripts/test-diet/lib.ts +88 -0
  240. package/scripts/test-diet/node.ts +102 -0
  241. package/scripts/ux/measure-first-text.ts +102 -0
  242. package/scripts/vflow-collect.ts +20 -7
  243. package/scripts/vflow-search.ts +73 -0
  244. package/scripts/video-free-line-test-walker.ts +36 -0
  245. package/scripts/video-pipeline.ts +2 -2
  246. package/skills/explainer-video/SKILL.md +12 -9
  247. package/skills/explainer-video/engine/build.mjs +1 -1
  248. package/skills/explainer-video/engine/reel.mjs +138 -31
  249. package/skills/explainer-video/engine/reel.sh +2 -1
  250. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  251. package/skills/grill-me/SKILL.md +1 -1
  252. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  253. package/src/a2a/server.ts +2 -0
  254. package/src/acp/auth-methods.ts +30 -0
  255. package/src/acp/elanous-aware-peers.ts +30 -0
  256. package/src/acp/server.ts +96 -20
  257. package/src/agent/monad-agent-turn.ts +25 -6
  258. package/src/agent/shared-app-tools.ts +5 -3
  259. package/src/agent-substrate/review-intent.ts +6 -2
  260. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  261. package/src/auth/github-app-token.ts +4 -1
  262. package/src/autopilot/track-agent-shadow.ts +151 -0
  263. package/src/autopilot/track-agent.ts +23 -3
  264. package/src/boot/acp-server-help.ts +16 -0
  265. package/src/boot/daemon-prompt-turn.ts +31 -10
  266. package/src/boot/daemon-tools/index.ts +7 -1
  267. package/src/budget/codex-credit-pace.ts +84 -0
  268. package/src/budget/codex-credit-plan.ts +154 -0
  269. package/src/budget/codex-reset-credit-state.ts +2 -0
  270. package/src/channel-import/detect.ts +191 -0
  271. package/src/channel-import/run.ts +111 -0
  272. package/src/chat/chat-cli.ts +4 -1
  273. package/src/chat/short-question.ts +40 -0
  274. package/src/cli/agent-cli.ts +24 -15
  275. package/src/cli/ask-bare.ts +42 -0
  276. package/src/cli/cli-user-error.ts +25 -0
  277. package/src/cli/codex-credit-plan-view.ts +25 -0
  278. package/src/cli/config-test-sync.ts +4 -0
  279. package/src/cli/decisions-cli.ts +13 -3
  280. package/src/cli/dependence-gauge.ts +237 -0
  281. package/src/cli/doctor-bundle.ts +128 -0
  282. package/src/cli/doctor-cli.ts +41 -1
  283. package/src/cli/doctor-local-llm.ts +95 -0
  284. package/src/cli/doctor-modes.ts +48 -0
  285. package/src/cli/import-cli.ts +60 -0
  286. package/src/cli/intake-cli.ts +12 -2
  287. package/src/cli/intake-source-cli.ts +58 -0
  288. package/src/cli/machine-cli.ts +23 -0
  289. package/src/cli/mcp-login.ts +13 -4
  290. package/src/cli/msg-cli.ts +137 -0
  291. package/src/cli/persona-cli.ts +116 -0
  292. package/src/cli/plugin-cli.ts +25 -0
  293. package/src/cli/pr-cli.ts +36 -0
  294. package/src/cli/provider-cli.ts +5 -0
  295. package/src/cli/release-cli.ts +50 -8
  296. package/src/cli/release-run-if-ready.ts +48 -0
  297. package/src/cli/setup-cli.ts +27 -6
  298. package/src/cli/setup-recommend.ts +8 -0
  299. package/src/cli/skills-cli.ts +173 -0
  300. package/src/cli/start-cli.ts +13 -4
  301. package/src/cli/usage-cli.ts +11 -1
  302. package/src/codex/setup.ts +37 -16
  303. package/src/dashboard/index.ts +18 -2
  304. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
  305. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  306. package/src/decisions/decision-cards.ts +291 -0
  307. package/src/decisions/decision-ledger.ts +5 -2
  308. package/src/decisions/discord-decision-cards.ts +133 -0
  309. package/src/decisions/telegram-decision-cards.ts +100 -0
  310. package/src/directives/version-at.ts +77 -0
  311. package/src/discord.ts +73 -6
  312. package/src/domains/core-tools.ts +37 -14
  313. package/src/domains/outbound-alert.ts +94 -12
  314. package/src/domains/repeated-failure.ts +1 -1
  315. package/src/error-report/report.ts +200 -0
  316. package/src/exec-requests/planner.ts +146 -0
  317. package/src/exec-requests/runner.ts +378 -0
  318. package/src/exec-requests/store.ts +78 -0
  319. package/src/field/field-feed-auto.ts +48 -0
  320. package/src/field/field-media.ts +8 -3
  321. package/src/field/field-reel.ts +203 -0
  322. package/src/grok/credential.ts +7 -5
  323. package/src/harness/aside-run-status.ts +33 -0
  324. package/src/harness/browser-verify.ts +48 -17
  325. package/src/harness/harness-cli-command.ts +25 -3
  326. package/src/harness/image-load-state.ts +21 -0
  327. package/src/harness/process-garbage.ts +45 -0
  328. package/src/hooks/error-report.ts +210 -0
  329. package/src/hooks/receiver.ts +41 -1
  330. package/src/import/apply.ts +128 -0
  331. package/src/import/plan.ts +197 -0
  332. package/src/index.ts +168 -21
  333. package/src/input-core/text-entry.ts +13 -5
  334. package/src/instance/leader.ts +13 -1
  335. package/src/intake-plane/collect-github.ts +5 -3
  336. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  337. package/src/intake-plane/discord-seat-work.ts +56 -0
  338. package/src/intake-plane/intake-sources.ts +202 -0
  339. package/src/intake-plane/items.ts +28 -12
  340. package/src/intake-plane/seat-answer.ts +96 -0
  341. package/src/intake-plane/seat-doc-route.ts +221 -0
  342. package/src/intake-plane/submit-intake-work.ts +9 -1
  343. package/src/intake-plane/telegram-seat-work.ts +72 -0
  344. package/src/llm/model-defaults.ts +24 -1
  345. package/src/llm.ts +5 -4
  346. package/src/machines/machine-ledger.ts +127 -0
  347. package/src/market/pack-internal-refs.ts +28 -0
  348. package/src/market/publish.ts +31 -4
  349. package/src/maturity/feature-maturity.ts +65 -0
  350. package/src/model-tier/turn-tier.ts +24 -0
  351. package/src/msg/msg-store.ts +149 -0
  352. package/src/native-tool-catalog.ts +2 -1
  353. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  354. package/src/nexus/api/consult-requests.ts +128 -0
  355. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  356. package/src/nexus/api/error-reports.ts +108 -0
  357. package/src/nexus/api/exec-requests.ts +51 -0
  358. package/src/nexus/api/field-uploads.ts +40 -3
  359. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  360. package/src/nexus/api/graph-approvals.ts +71 -4
  361. package/src/nexus/api/http-server.ts +82 -8
  362. package/src/nexus/api/outbound-report.ts +7 -0
  363. package/src/nexus/api/running-runs-background.ts +68 -0
  364. package/src/nexus/api/running-runs-child.ts +6 -0
  365. package/src/nexus/api/seat-requests.ts +254 -0
  366. package/src/nexus/api/share-captures.ts +91 -0
  367. package/src/nexus/api/skill-problems.ts +56 -0
  368. package/src/nexus/api/terminals.ts +24 -8
  369. package/src/nexus/api/trace-run-scan.ts +37 -0
  370. package/src/nexus/api/trace.ts +19 -3
  371. package/src/nexus/index.ts +28 -1
  372. package/src/nexus/setup-status.ts +11 -7
  373. package/src/notify/llm-budget-alert.ts +16 -10
  374. package/src/oauth/browser-availability.ts +14 -0
  375. package/src/oauth/codex-account-rotation.ts +26 -4
  376. package/src/oauth/codex-account-store.ts +20 -2
  377. package/src/oauth/codex-browser-login.ts +173 -0
  378. package/src/oauth/codex-quota-policy.ts +16 -0
  379. package/src/oauth/codex.ts +4 -1
  380. package/src/oauth/fallback-chain.ts +5 -5
  381. package/src/oauth/store.ts +42 -5
  382. package/src/onboarding/continue-here.ts +34 -0
  383. package/src/onboarding/entry-hints.ts +8 -0
  384. package/src/onboarding/full-screen-io.ts +35 -3
  385. package/src/onboarding/help/skills.md +1 -1
  386. package/src/onboarding/non-interactive.ts +1 -0
  387. package/src/onboarding/subscription-hint.ts +27 -0
  388. package/src/onboarding/web-first.ts +90 -0
  389. package/src/onboarding.ts +287 -26
  390. package/src/persona/presets.ts +105 -0
  391. package/src/plugins/maker/node-install.ts +92 -0
  392. package/src/plugins/maker/node-maker.ts +139 -0
  393. package/src/plugins/maker/plugin-maker.ts +19 -7
  394. package/src/policy/llm-policy.ts +268 -0
  395. package/src/prompt-library/session-guidance.ts +7 -1
  396. package/src/prompt-library/universal-preamble.ts +2 -2
  397. package/src/relay/relay-cli.ts +20 -0
  398. package/src/relay/relay-server.ts +220 -0
  399. package/src/release-loop/checklist.ts +37 -13
  400. package/src/self-dev/decompose.ts +1 -0
  401. package/src/self-dev/dev-pipeline.ts +6 -3
  402. package/src/self-dev/orchestrate.ts +18 -11
  403. package/src/self-dev/run-record-mirror.ts +44 -0
  404. package/src/self-dev/run-store.ts +12 -3
  405. package/src/self-implement/harness-policy-gates.ts +4 -1
  406. package/src/self-implement/report-origin.ts +12 -1
  407. package/src/session/chat.ts +17 -5
  408. package/src/shell-env-bootstrap.ts +44 -7
  409. package/src/skills/connect-claude-plugin.ts +142 -0
  410. package/src/skills/connect.ts +70 -0
  411. package/src/skills/index.ts +29 -1
  412. package/src/skills/repair.ts +124 -0
  413. package/src/skills/runner.ts +11 -2
  414. package/src/skills/shared-agent-skill-roots.ts +25 -0
  415. package/src/skills/trigger-extract.ts +1 -1
  416. package/src/steward/failure-streak.ts +78 -0
  417. package/src/steward/steward-cards.ts +24 -0
  418. package/src/steward/triage-budget.ts +127 -0
  419. package/src/steward/triage-plan.ts +75 -0
  420. package/src/steward/triage.ts +244 -42
  421. package/src/task-cards/card-store.ts +1 -1
  422. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  423. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  424. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  425. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  426. package/src/telegram-commands.ts +13 -0
  427. package/src/telegram.ts +72 -2
  428. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  429. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  430. package/src/user-config.ts +189 -15
  431. package/src/version/packed-revision.json +1 -1
  432. package/src/video-pipeline/recipes/character.ts +42 -5
  433. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  434. package/src/video-pipeline/recipes/free-line.ts +16 -3
  435. package/src/video-pipeline/recipes/remote.ts +7 -1
  436. package/src/video-pipeline/vflow-reference.ts +109 -0
  437. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  446. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  515. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  516. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  517. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
@@ -8,7 +8,9 @@ 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 { planShards, readFileDurations } from './shard-plan';
11
+ import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
12
+ import { baselineFromCutLogs } from './baseline-from-logs';
13
+ import { planShards, readFileDurations, readFileMemory } from './shard-plan';
12
14
  import { deriveCdpTestPatterns } from '../test-deterministic';
13
15
  import { emitNodeResult, readGraphContext } from './node-verdict.js';
14
16
  import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.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,19 @@ 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
+ /** GT1b — a file whose measured peak is at least this runs alone at the isolation limit from the start: one such file
116
+ * sank a whole 203-file root shard at 16Gi (0.2.6 shard 12 · `unwired-exports` 9.9 GB) and cost two halving rounds. */
117
+ export const POD_HEAVY_FILE_MB = 4096;
118
+ /** Per-file peak memory, read from the cut tree (absent → no heavy files; the halving path still catches OOM). */
119
+ export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-2026-10-01.tsv';
120
+ /** Integration tests that the Pod sweep does not run — each is judged by a real run elsewhere at the cut.
121
+ * 0.2.7 run 5 (10-01): `scripts/install.test.ts` (real `bun pm pack` → `bun add` installs, 301 s alone on node-b)
122
+ * sat in a shard of its own past the 1200 s deadline with no output and stopped the whole gate. Judged on the
123
+ * mbp at the cut instead (TD1 moves it to the integration line). Keep this list short and explained. */
124
+ /** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
125
+ export const POD_SHARD_FILE_CAP = 27;
126
+
127
+ export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
79
128
  const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
80
129
  const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
81
130
  let last: string | undefined;
@@ -92,10 +141,15 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
92
141
  const reportedErrors = summaryCount(output, 'error');
93
142
  const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
94
143
  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
144
  const count = summaryCount(output, 'fail');
97
145
  const failures = parseFailures(output);
98
146
  const occurrences = [...output.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
147
+ // A test that leaves process.exitCode set leaks a non-zero exit into a run whose own summary is complete and clean
148
+ // (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.
149
+ const leakedExit = !!ran && run.rc !== 0 && run.rc !== 1 && count === 0 && errorCount === 0 && occurrences === 0
150
+ && !/^# Unhandled error between tests/m.test(output);
151
+ const rc = leakedExit ? 0 : run.rc;
152
+ if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (rc !== 0 && rc !== 1)) throw incomplete('summary/exit');
99
153
  // Bun reports file-level unhandled errors separately from test failures.
100
154
  const errors: string[] = [];
101
155
  let file: string | undefined;
@@ -110,7 +164,7 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
110
164
  }
111
165
  if (errors.length !== errorCount) throw incomplete(`error attribution identified=${errors.length}`);
112
166
  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)) {
167
+ || (rc === 1 && count === 0 && errorCount === 0) || (rc === 0 && count + errorCount > 0)) {
114
168
  throw incomplete(`failure attribution incomplete: summary=${count}, identified=${failures.length}`);
115
169
  }
116
170
  return { failures, errors: [...new Set(errors)].sort() };
@@ -156,10 +210,20 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
156
210
  const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
157
211
  // CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
158
212
  // 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('.')));
213
+ const assignable = files.filter((file) => !cdpPatterns.includes(file) && !POD_SWEEP_INTEGRATION_ONLY.includes(file)
214
+ && !file.split('/').some((part) => part.startsWith('.')));
215
+ const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
216
+ if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
160
217
  if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
161
218
  const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
162
- const shards = planShards(assignable, durations, shardCount);
219
+ const memory = readFileMemory(join(tree, POD_MEMORY_SOURCE));
220
+ const heavy = new Set(assignable.filter((file) => (memory.get(file) ?? 0) >= POD_HEAVY_FILE_MB));
221
+ const light = assignable.filter((file) => !heavy.has(file));
222
+ const shards = [
223
+ ...(light.length ? planShards(light, durations, shardCount) : []),
224
+ ...[...heavy].map((file) => ({ files: [file], plannedSeconds: durations.get(file) ?? 0 })),
225
+ ];
226
+ if (heavy.size) debug.log('release-loop.gate', 'pod-heavy-alone', { files: [...heavy], thresholdMb: POD_HEAVY_FILE_MB, memoryLimit: POD_ISOLATION_MEMORY_LIMIT });
163
227
  const known = assignable.filter((file) => durations.has(file)).length;
164
228
  debug.log('release-loop.gate', 'pod-shard-plan', {
165
229
  shards: shards.length, known, unknown: assignable.length - known,
@@ -169,7 +233,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
169
233
  const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
170
234
  if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
171
235
  const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
172
- const bunCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim() || undefined;
236
+ const configCache = typeof pod.bunCache === 'string' ? pod.bunCache.trim() : '';
237
+ const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
238
+ const bunCache = configCache || envCache || undefined;
239
+ debug.log('release-loop.gate', 'pod-bun-cache', { source: configCache ? 'config' : envCache ? 'env' : 'none' });
173
240
  const cachePrefix = bunCache ? `${podBunCacheVolume(bunCache).shellPrefix} ` : '';
174
241
  const jobLogTail = (job: PodCommandResult): string => {
175
242
  if (podLogTail) return podLogTail(job);
@@ -179,7 +246,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
179
246
  }
180
247
  return '';
181
248
  };
182
- type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
249
+ type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number; passedIds: string[] };
183
250
  type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
184
251
  // 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
185
252
  const isolationRemaining = shards.map((item) => item.files.length);
@@ -207,7 +274,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
207
274
  pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
208
275
  ...(bunCache ? { bunCache } : {}),
209
276
  // 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
210
- ...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
277
+ ...(paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
211
278
  name: `gate-${randomUUID()}`,
212
279
  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
280
  });
@@ -233,7 +300,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
233
300
  const destination = join(logDir, `pod-${shard}${branch}.log`);
234
301
  if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
235
302
  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 });
303
+ 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
304
  const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
238
305
  if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
239
306
  else rmSync(junitDestination, { force: true });
@@ -298,6 +365,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
298
365
  ...(lastFile ? { lastFile } : {}),
299
366
  ...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
300
367
  }] };
368
+ // GT1 — a big shard that failed at the root goes straight to cap-sized chunks instead of halving twice: memory
369
+ // piles up when one bun process runs many files (0.2.7: only bundles of 54+ OOMed), and each halving round costs
370
+ // a Pod start, an install and a rerun. A failing chunk then isolates its files (depth 2), as before.
371
+ if (depth === 0 && paths.length > POD_SHARD_FILE_CAP) {
372
+ const chunks = Array.from({ length: Math.ceil(paths.length / POD_SHARD_FILE_CAP) }, (_, i) => paths.slice(i * POD_SHARD_FILE_CAP, (i + 1) * POD_SHARD_FILE_CAP));
373
+ debug.log('release-loop.gate', 'pod-shard-cap-split', { shard, files: paths.length, chunks: chunks.length, cap: POD_SHARD_FILE_CAP, reason });
374
+ const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`)));
375
+ return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
376
+ }
301
377
  debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
302
378
  const middle = Math.ceil(paths.length / 2);
303
379
  const children = await Promise.all([
@@ -308,7 +384,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
308
384
  }
309
385
  return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
310
386
  errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
311
- ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
387
+ ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
312
388
  };
313
389
  // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
314
390
  const settled = await Promise.allSettled(shards.map((item, shard) => runShard(item.files, shard)));
@@ -323,7 +399,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
323
399
  if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
324
400
  if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
325
401
  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` };
402
+ 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`,
403
+ passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort() };
327
404
  };
328
405
  return {
329
406
  command,
@@ -455,7 +532,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
455
532
  return baselineCommit(root, version);
456
533
  };
457
534
  const repo = resolve(opts.repo ?? process.cwd());
458
- const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
535
+ const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
459
536
  let work: string | undefined;
460
537
  let baseSnapshot = false;
461
538
  let cutFailures: SweepFailures | undefined;
@@ -510,8 +587,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
510
587
  cutFailures = cut;
511
588
  const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
512
589
  const cached = cachedBaseline(opts.baselineVersion, baseSha);
513
- const trusted = cached?.saved;
514
- result.baselineSource = trusted ? cached!.source : 'swept';
590
+ const recorded = cached ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), baseSha);
591
+ const trusted = cached?.saved ?? (recorded?.complete ? recorded : undefined);
592
+ result.baselineSource = cached ? cached.source : recorded?.complete ? 'cut-logs' : 'swept';
515
593
  debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
516
594
  let baseTree: string | undefined;
517
595
  const getBaseTree = async () => {
@@ -534,7 +612,14 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
534
612
  baseline = failuresOf(baseRun, 'baseline sweep');
535
613
  }
536
614
  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]);
615
+ const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
616
+ const cutIds = [...cut.failures, ...cut.errors];
617
+ const baselineIds = [...baseline.failures, ...baseline.errors];
618
+ const countedCut = splitKnownEnv(cutIds, known);
619
+ const countedBaseline = splitKnownEnv(baselineIds, known);
620
+ result.knownEnv = countedCut.knownEnv.length;
621
+ result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
622
+ const diff = diffFailures(countedCut.counted, countedBaseline.counted);
538
623
  result.fixed = diff.fixed.length;
539
624
  result.preexisting = diff.common.length;
540
625
  for (const file of new Set(diff.newFailures.map(fileOf))) {
@@ -584,7 +669,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
584
669
  if (opts.remote) runner.remoteMirror = undefined;
585
670
  result.durationMs = Date.now() - start;
586
671
  }
587
- if (result.outcome !== 'error' && cutFailures) {
672
+ if (cutFailures && !result.stalledShards?.length) {
588
673
  try {
589
674
  for (const location of roots) {
590
675
  const path = failureFile(location, opts.version);
@@ -608,10 +693,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
608
693
  export function graphGateResult(result: GateResult, graph: boolean) {
609
694
  return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
610
695
  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'}` };
696
+ summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}` };
612
697
  }
613
698
 
614
- function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
699
+ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
615
700
  const context = env.ELANOUS_GRAPH_CONTEXT && args.every((arg) => arg === '--json') ? readGraphContext(env) : undefined;
616
701
  const fromGraph: Record<string, unknown> = context?.input ?? {};
617
702
  const graphCommit = context?.outputs['version-release']?.commit;
@@ -629,6 +714,7 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
629
714
  pool: fromGraph.gatePodPool,
630
715
  ...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
631
716
  ...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
717
+ ...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
632
718
  } : undefined,
633
719
  };
634
720
  for (let i = 0; i < args.length; i++) {
@@ -665,7 +751,7 @@ if (import.meta.main) {
665
751
  version = opts.version;
666
752
  result = await judgeGate(opts);
667
753
  } catch (error) {
668
- result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, durationMs: Date.now() - start, error: String(error) };
754
+ result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
669
755
  }
670
756
  if (result.error) console.error(result.error);
671
757
  debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { emitNodeResult, readGraphContext, runCommand, type CommandRunner, type GraphContext } from './node-verdict.js';
5
+
6
+ const FALLBACK = 'Known issues have been reported for this release.';
7
+ // No fix schedule is promised here — the inputs do not carry one (review R2).
8
+ const INTRO = 'The following issues are known in this release.';
9
+ // A known-issues bullet describes a problem that is still there; «fixed / resolved» wording turns it into a false claim.
10
+ const resolved = /\b(?:fixed|resolved|no longer|has been addressed|is addressed|was addressed)\b/i;
11
+ const unsafe = /[^\x20-\x7e]|#\d+|\/Users\/|\.test\.ts\b|\[(?:TC|MK|OP|UX|S|T|F|O)\]|\b(?:TC|MK|OP|UX)\s+track\b|\b(?:PR|pull request)\s*\d+\b|\b(?:scripts|src|test)\/\S+|\bfile:\/\/|(?<![\w:/])\/(?!\/)[\w.-]+(?:\/[\w.-]+)*|\b[A-Z]:\\(?:[^\s\\]+\\)*[^\s\\]+|\b(?:RFC|HANDOFF|MANUAL)-/i;
12
+
13
+ type Issue = { id: string; title?: string; evidence?: string; note?: string };
14
+
15
+ function issues(value: unknown, kind: 'checklist' | 'accepted'): Issue[] {
16
+ if (value === undefined) return [];
17
+ if (!Array.isArray(value) || !value.every((item) => item && typeof item === 'object'
18
+ && typeof item.id === 'string' && item.id.trim()
19
+ && (kind === 'checklist' ? typeof item.title === 'string' && typeof item.evidence === 'string' : typeof item.note === 'string'))) {
20
+ throw new Error(`invalid ${kind} known issues`);
21
+ }
22
+ return value as Issue[];
23
+ }
24
+
25
+ function generatedBullets(raw: string, sourceIds: string[]): string[] {
26
+ const envelope: unknown = JSON.parse(raw.trim());
27
+ const reply = envelope && typeof envelope === 'object' && !Array.isArray(envelope) && 'reply' in envelope
28
+ ? envelope.reply : envelope;
29
+ if (typeof reply !== 'string') throw new Error('empty ask reply');
30
+ const entries: unknown = JSON.parse(reply.replace(/^```(?:json)?\s*\n|\n```\s*$/g, '').trim());
31
+ if (!Array.isArray(entries) || entries.length !== sourceIds.length || new Set(sourceIds).size !== sourceIds.length) {
32
+ throw new Error('known issues response does not cover every input ID');
33
+ }
34
+ const byId = new Map<string, string>();
35
+ for (const entry of entries) {
36
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)
37
+ || typeof entry.id !== 'string' || typeof entry.bullet !== 'string'
38
+ || byId.has(entry.id) || !sourceIds.includes(entry.id)) {
39
+ throw new Error('known issues response does not cover every input ID');
40
+ }
41
+ byId.set(entry.id, entry.bullet);
42
+ }
43
+ if (sourceIds.some((id) => !byId.has(id))) throw new Error('known issues response does not cover every input ID');
44
+ return sourceIds.map((id) => byId.get(id)!);
45
+ }
46
+
47
+ function coversIssue(bullet: string, issue: Issue, others: Issue[]): boolean {
48
+ const source = issue.title ?? issue.note ?? '';
49
+ const words = source.toLowerCase().match(/[a-z]{5,}/g) ?? [];
50
+ const otherWords = new Set(others.flatMap((other) => ((other.title ?? other.note ?? '').toLowerCase().match(/[a-z]{5,}/g) ?? []).map((word) => word.slice(0, 5))));
51
+ const publicWords = new Set((bullet.toLowerCase().match(/[a-z]{5,}/g) ?? []).map((word) => word.slice(0, 5)));
52
+ const distinctive = [...new Set(words.map((word) => word.slice(0, 5)))].filter((word) => !otherWords.has(word));
53
+ const negative = /\b(?:not|never|no|cannot|can't|doesn't|don't|won't|fails? to|isn't|aren't)\b/i;
54
+ if (negative.test(source) && !negative.test(bullet)) return false;
55
+ // A source with no English words to compare (e.g. a Korean checklist title) is covered structurally —
56
+ // one bullet per input ID — instead of blocking the release (review R2).
57
+ if (words.length === 0) return true;
58
+ return distinctive.length > 0 && distinctive.filter((word) => publicWords.has(word)).length >= Math.min(issue.note === undefined ? 1 : 2, distinctive.length);
59
+ }
60
+
61
+ function cleanBullet(text: string): string | null {
62
+ const line = text.trim();
63
+ if (!line || /^[-*]\s/.test(line) || unsafe.test(line) || /[{}<>\[\]`]/.test(line) || (line.match(/[.!?](?:\s|$)/g) ?? []).length > 2) return null;
64
+ return line;
65
+ }
66
+
67
+ function command(run: CommandRunner, program: string, args: string[], cwd?: string): string {
68
+ const result = run(program, args, cwd);
69
+ if (result.status !== 0) throw new Error(`${program} ${args[0]} failed: ${result.stderr || result.stdout}`);
70
+ return result.stdout;
71
+ }
72
+
73
+ export function runKnownIssues(context: GraphContext = readGraphContext(), run: CommandRunner = runCommand) {
74
+ const version = context.input.version;
75
+ const worktree = context.outputs.docs?.worktree;
76
+ if (typeof worktree !== 'string' || !worktree || !existsSync(worktree)) throw new Error('docs 워크트리 없음');
77
+ if (context.outputs.docs?.branch !== `release-docs/${version}`) throw new Error('docs branch/version mismatch');
78
+ const notes = join(worktree, 'release/public/docs/releases', `${version}.md`);
79
+ if (!existsSync(notes)) throw new Error(`release notes missing: ${notes}`);
80
+ const checklist = issues(context.outputs['checklist-gate']?.knownIssues, 'checklist');
81
+ const accepted = issues(context.input.acceptedRegressions, 'accepted');
82
+ const sources = { checklist: checklist.length, accepted: accepted.length };
83
+ if (!checklist.length && !accepted.length) return { outcome: 'ok' as const, verdict: 'pass' as const, summary: '알려진 문제 없음', count: 0, bullets: [] as string[], sources, fallbackUsed: false };
84
+
85
+ let bullets: string[];
86
+ let fallbackUsed = false;
87
+ const allIssues = [...checklist, ...accepted];
88
+ const sourceIds = allIssues.map((issue) => issue.id);
89
+ const prompt = `Write public English release-note bullets for these known issues. Describe only user-visible impact; include a one-line workaround when available. No internal names, test paths, track names, PR numbers, or people. Each bullet must have at most two sentences. Return ONLY a JSON array with exactly one object {"id":"input ID","bullet":"English public sentence"} for each input, using each input ID exactly once. Do not include IDs in bullet text. Input: ${JSON.stringify({ checklist, acceptedRegressions: accepted })}`;
90
+ let response: ReturnType<CommandRunner>;
91
+ try {
92
+ response = run('elanous', ['--test', 'ask', '--json', prompt]);
93
+ } catch {
94
+ response = { status: 1, stdout: '', stderr: '' };
95
+ }
96
+ if (response.status !== 0) {
97
+ bullets = [FALLBACK];
98
+ fallbackUsed = true;
99
+ } else {
100
+ try {
101
+ bullets = generatedBullets(response.stdout, sourceIds).map((line, index) => {
102
+ const safe = cleanBullet(line);
103
+ if (!safe) {
104
+ fallbackUsed = true;
105
+ return FALLBACK;
106
+ }
107
+ if (resolved.test(safe)) throw new Error(`bullet for ${sourceIds[index]} claims the issue is resolved`);
108
+ if (!coversIssue(safe, allIssues[index]!, allIssues.filter((_, otherIndex) => otherIndex !== index))) {
109
+ throw new Error(`bullet does not identify input ${sourceIds[index]}`);
110
+ }
111
+ return safe;
112
+ });
113
+ if (new Set(bullets).size !== bullets.length && bullets.some((bullet) => bullet !== FALLBACK)) {
114
+ throw new Error('known issues response repeats an issue instead of covering each input');
115
+ }
116
+ bullets = [...new Set(bullets)];
117
+ } catch (error) {
118
+ throw new Error(`known issues coverage not verified: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ }
121
+ const original = readFileSync(notes, 'utf8');
122
+ const heading = /^## Known issues\s*$/m;
123
+ const match = heading.exec(original);
124
+ let updated: string;
125
+ if (match) {
126
+ const start = match.index + match[0].length;
127
+ const next = /^## /gm;
128
+ next.lastIndex = start;
129
+ const end = next.exec(original)?.index ?? original.length;
130
+ const section = original.slice(start, end);
131
+ const additions = bullets.filter((bullet) => !section.split(/\r?\n/).some((line) => line.trim() === `- ${bullet}`));
132
+ const before = original.slice(0, end);
133
+ updated = additions.length ? `${before}${before.endsWith('\n') ? '' : '\n'}${additions.map((bullet) => `- ${bullet}`).join('\n')}\n\n${original.slice(end)}` : original;
134
+ } else {
135
+ updated = `${original.trimEnd()}\n\n## Known issues\n\n${INTRO}\n\n${bullets.map((bullet) => `- ${bullet}`).join('\n')}\n`;
136
+ }
137
+ if (updated !== original) {
138
+ writeFileSync(notes, updated);
139
+ command(run, 'git', ['add', '--', `release/public/docs/releases/${version}.md`], worktree);
140
+ command(run, 'git', ['commit', '-m', `docs: known issues for ${version}`], worktree);
141
+ }
142
+ // Push whenever the branch is ahead of its remote — a rerun after a failed push finds the bullets already in the file (review R2).
143
+ command(run, 'git', ['fetch', 'origin', `release-docs/${version}`], worktree);
144
+ const ahead = Number(command(run, 'git', ['rev-list', '--count', `origin/release-docs/${version}..HEAD`], worktree).trim());
145
+ if (!Number.isFinite(ahead)) throw new Error('could not compare release-docs branch with its remote');
146
+ if (ahead > 0) command(run, 'git', ['push', 'origin', `release-docs/${version}`], worktree);
147
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `known issues: ${bullets.length}`, count: bullets.length, bullets, sources, fallbackUsed };
148
+ }
149
+
150
+ if (import.meta.main) {
151
+ try {
152
+ const result = runKnownIssues();
153
+ emitNodeResult(result);
154
+ process.exitCode = 0;
155
+ } catch (error) {
156
+ emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: error instanceof Error ? error.message : String(error), count: 0, bullets: [], sources: { checklist: 0, accepted: 0 }, fallbackUsed: false });
157
+ process.exitCode = 1;
158
+ }
159
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { debug } from '../../src/debug/log.js';
7
+ import { sendOutbound } from '../../src/domains/outbound-alert.js';
8
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
9
+ import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
10
+
11
+ type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
12
+ type Exec = (command: string, args: string[], env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
13
+ type Registry = (url: string) => Promise<{ status: number; body: unknown }>;
14
+ export interface NpmPublishDeps {
15
+ exec?: Exec;
16
+ registry?: Registry;
17
+ sleep?: (ms: number) => Promise<void>;
18
+ stateDir?: string;
19
+ /** Test seam — the production root whose secrets/npm-token is the last fallback. */
20
+ productionRoot?: string;
21
+ alert?: (text: string, kind: string) => boolean;
22
+ log?: (version: string, waitedMinutes: number) => void;
23
+ }
24
+
25
+ const exec: Exec = (command, args, env) => {
26
+ const result = spawnSync(command, args, { encoding: 'utf8', env: env ?? process.env, maxBuffer: 1024 * 1024, timeout: 300_000 });
27
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? String(result.error ?? '') };
28
+ };
29
+ const registry: Registry = async (url) => {
30
+ const response = await fetch(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(15_000) });
31
+ return { status: response.status, body: response.ok ? await response.json() : null };
32
+ };
33
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
34
+ const PACKAGE_URL = 'https://registry.npmjs.org/elanous';
35
+
36
+ function redact(text: string, token: string): string {
37
+ return token ? text.replaceAll(token, '[REDACTED]') : text;
38
+ }
39
+ function reason(error: unknown, token: string): string {
40
+ return redact(error instanceof Error ? error.message : String(error), token).replace(/[\r\n]+/g, ' ').slice(0, 500);
41
+ }
42
+ function metadata(value: unknown): { name?: string; version?: string } {
43
+ if (!value || typeof value !== 'object') return {};
44
+ return value as { name?: string; version?: string };
45
+ }
46
+
47
+ export async function runNpmPublish(context: GraphContext = readGraphContext(), deps: NpmPublishDeps = {}): Promise<Result> {
48
+ const version = context.input.version;
49
+ let token = '';
50
+ try {
51
+ if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
52
+ const stateDir = deps.stateDir ?? effectiveInstanceRoot();
53
+ // REL3 — the archive prepare actually built (its output) wins over this process's universe: a resumed run can
54
+ // resolve another universe (10-01 0.2.7: prepared in the pilot test universe, resumed in production).
55
+ const prepared = context.outputs.prepare?.candidate;
56
+ const candidate = typeof prepared === 'string' && prepared.endsWith('.tgz') ? prepared : join(stateDir, 'release', version, 'prepared', 'dist', 'elanous.tgz');
57
+ const run = deps.exec ?? exec;
58
+ const readRegistry = deps.registry ?? registry;
59
+ const wait = deps.sleep ?? sleep;
60
+ const waitMinutes = context.input.npmWaitMinutes ?? 40;
61
+ if (typeof waitMinutes !== 'number' || !Number.isFinite(waitMinutes) || waitMinutes < 0 || waitMinutes > 40) throw new Error('input.npmWaitMinutes must be between 0 and 40');
62
+ const archive = run('tar', ['-xOzf', candidate, 'package/package.json']);
63
+ if (archive.status !== 0) throw new Error(`npm archive invalid: ${archive.stderr}`);
64
+ const pkg = metadata(JSON.parse(archive.stdout));
65
+ if (pkg.name !== 'elanous' || pkg.version !== version) throw new Error('npm archive package name/version mismatch');
66
+ const inspect = async (): Promise<{ visible: boolean; tagged: boolean }> => {
67
+ const [manifest, tags] = await Promise.all([readRegistry(`${PACKAGE_URL}/${version}`), readRegistry(PACKAGE_URL)]);
68
+ if (manifest.status !== 200 && manifest.status !== 404) throw new Error(`npm registry version lookup failed (${manifest.status})`);
69
+ if (tags.status !== 200) throw new Error(`npm registry tags lookup failed (${tags.status})`);
70
+ const latest = tags.body && typeof tags.body === 'object' && 'dist-tags' in tags.body
71
+ ? (tags.body as { 'dist-tags'?: { latest?: unknown } })['dist-tags']?.latest : undefined;
72
+ if (manifest.status === 200 && (metadata(manifest.body).name !== 'elanous' || metadata(manifest.body).version !== version))
73
+ throw new Error('npm registry version metadata mismatch');
74
+ return { visible: manifest.status === 200, tagged: manifest.status === 200 && latest === version };
75
+ };
76
+ const before = await inspect();
77
+ if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with latest tag` };
78
+ if (!before.visible) {
79
+ // The npm token is a machine secret (like the GitHub App key): input → this universe → the production root.
80
+ const tokenFile = context.input.npmTokenFile ?? [join(stateDir, 'secrets', 'npm-token'), join(deps.productionRoot ?? prodInstanceRoot(), 'secrets', 'npm-token')].find((p) => existsSync(p)) ?? join(stateDir, 'secrets', 'npm-token');
81
+ if (typeof tokenFile !== 'string' || !tokenFile.trim()) throw new Error('input.npmTokenFile must be a path');
82
+ token = readFileSync(tokenFile, 'utf8').trim();
83
+ if (!token) throw new Error('npm token file empty');
84
+ const dir = mkdtempSync(join(tmpdir(), 'release-npmrc-'));
85
+ let published;
86
+ try {
87
+ const npmrc = join(dir, 'npmrc');
88
+ writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}\n`, { mode: 0o600, flag: 'wx' });
89
+ published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag', 'latest', '--access', 'public'], { ...process.env, NPM_CONFIG_USERCONFIG: npmrc });
90
+ } finally { rmSync(dir, { recursive: true, force: true }); }
91
+ const output = `${published.stdout}\n${published.stderr}`;
92
+ const staged = published.status !== 0 && /\bE409\b/i.test(output) && /previously staged/i.test(output);
93
+ if (published.status !== 0 && !staged) throw new Error(`npm publish failed (rc=${published.status}): ${redact(output, token)}`);
94
+ }
95
+ const polls = Math.ceil(waitMinutes * 60 / 30);
96
+ let last = before;
97
+ for (let i = 0; i <= polls; i++) {
98
+ last = await inspect();
99
+ if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with latest tag` };
100
+ if (i < polls) await wait(Math.min(30_000, waitMinutes * 60_000 - i * 30_000));
101
+ }
102
+ if (last.visible) throw new Error(`npm ${version} visible but dist-tags.latest did not reach ${version} within ${waitMinutes} minutes`);
103
+ try { (deps.log ?? ((v, minutes) => debug.log('release-loop.npm', 'staged-not-visible', { version: v, waitedMinutes: minutes })))(version, waitMinutes); } catch { /* observation must not mask npm's staged state */ }
104
+ try { (deps.alert ?? sendOutbound)(`npm ${version} 가 스테이징에 멈춤 — 계정 소유자 확인 필요`, 'alert'); } catch { /* alert failure must not block docs */ }
105
+ return { outcome: 'ok', verdict: 'pass', npm: 'staged-not-visible', humanAction: 'npm-support', summary: 'npm 스테이징 — 레지스트리에 아직 없음' };
106
+ } catch (error) {
107
+ return { outcome: 'fail', verdict: 'fail', summary: `npm ${version} failed: ${reason(error, token)}` };
108
+ }
109
+ }
110
+
111
+ if (import.meta.main) {
112
+ let version = '';
113
+ try {
114
+ const context = readGraphContext();
115
+ version = context.input.version;
116
+ const result = await runNpmPublish(context);
117
+ debug.log('release-loop.npm', 'result', { version, outcome: result.outcome, npm: result.npm });
118
+ emitNodeResult(result);
119
+ process.exitCode = result.outcome === 'ok' ? 0 : 1;
120
+ } catch {
121
+ emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: 'npm publish context unavailable' });
122
+ process.exitCode = 1;
123
+ }
124
+ }