elanous 0.2.7 → 0.2.9

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 (296) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
  51. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +30 -29
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +30 -29
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +30 -29
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +27 -26
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +27 -26
  62. package/apps/pwa/out/chat/index.html +1 -1
  63. package/apps/pwa/out/chat/index.txt +27 -26
  64. package/apps/pwa/out/consult/index.html +1 -1
  65. package/apps/pwa/out/consult/index.txt +30 -29
  66. package/apps/pwa/out/control/index.html +1 -1
  67. package/apps/pwa/out/control/index.txt +30 -29
  68. package/apps/pwa/out/design-check/index.html +1 -1
  69. package/apps/pwa/out/design-check/index.txt +30 -29
  70. package/apps/pwa/out/editor/index.html +1 -1
  71. package/apps/pwa/out/editor/index.txt +30 -29
  72. package/apps/pwa/out/exec/index.html +1 -1
  73. package/apps/pwa/out/exec/index.txt +30 -29
  74. package/apps/pwa/out/field/index.html +1 -1
  75. package/apps/pwa/out/field/index.txt +30 -29
  76. package/apps/pwa/out/healthz +1 -1
  77. package/apps/pwa/out/index.html +1 -1
  78. package/apps/pwa/out/index.txt +27 -26
  79. package/apps/pwa/out/intake/index.html +1 -1
  80. package/apps/pwa/out/intake/index.txt +30 -29
  81. package/apps/pwa/out/live/index.html +1 -1
  82. package/apps/pwa/out/live/index.txt +30 -29
  83. package/apps/pwa/out/market/index.html +1 -1
  84. package/apps/pwa/out/market/index.txt +27 -26
  85. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  86. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  87. package/apps/pwa/out/morning/index.html +1 -1
  88. package/apps/pwa/out/morning/index.txt +30 -29
  89. package/apps/pwa/out/observatory/index.html +1 -1
  90. package/apps/pwa/out/observatory/index.txt +30 -29
  91. package/apps/pwa/out/ops/checklist/index.html +1 -0
  92. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  93. package/apps/pwa/out/ops/release/index.html +1 -0
  94. package/apps/pwa/out/ops/release/index.txt +30 -0
  95. package/apps/pwa/out/reflection/index.html +1 -1
  96. package/apps/pwa/out/reflection/index.txt +28 -27
  97. package/apps/pwa/out/scheduler/index.html +1 -1
  98. package/apps/pwa/out/scheduler/index.txt +30 -29
  99. package/apps/pwa/out/sessions/index.html +1 -1
  100. package/apps/pwa/out/sessions/index.txt +27 -26
  101. package/apps/pwa/out/settings/devices/index.html +1 -1
  102. package/apps/pwa/out/settings/devices/index.txt +30 -29
  103. package/apps/pwa/out/settings/index.html +1 -1
  104. package/apps/pwa/out/settings/index.txt +30 -29
  105. package/apps/pwa/out/setup/done/index.html +1 -1
  106. package/apps/pwa/out/setup/done/index.txt +30 -29
  107. package/apps/pwa/out/setup/index.html +1 -1
  108. package/apps/pwa/out/setup/index.txt +30 -29
  109. package/apps/pwa/out/share/index.html +1 -1
  110. package/apps/pwa/out/share/index.txt +30 -29
  111. package/apps/pwa/out/showroom/index.html +1 -1
  112. package/apps/pwa/out/showroom/index.txt +27 -26
  113. package/apps/pwa/out/tasks/index.html +1 -1
  114. package/apps/pwa/out/tasks/index.txt +30 -29
  115. package/apps/pwa/out/term/index.html +1 -1
  116. package/apps/pwa/out/term/index.txt +30 -29
  117. package/apps/pwa/out/trace/index.html +1 -1
  118. package/apps/pwa/out/trace/index.txt +30 -29
  119. package/apps/pwa/out/vault/index.html +1 -1
  120. package/apps/pwa/out/vault/index.txt +30 -29
  121. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  122. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  123. package/apps/pwa/out/workflows/index.html +1 -1
  124. package/apps/pwa/out/workflows/index.txt +30 -29
  125. package/apps/pwa/out/workspace/index.html +1 -1
  126. package/apps/pwa/out/workspace/index.txt +30 -29
  127. package/apps/pwa/out/worktrees/index.html +1 -1
  128. package/apps/pwa/out/worktrees/index.txt +30 -29
  129. package/graphs/release/recipes.yaml +3 -0
  130. package/graphs/release/release-loop.yaml +3 -1
  131. package/graphs/test-diet/nightly-audit.yaml +18 -0
  132. package/graphs/test-diet/recipes.yaml +12 -0
  133. package/graphs/test-diet/test-diet.yaml +22 -0
  134. package/package.json +2 -1
  135. package/persona-presets/_index.yaml +10 -0
  136. package/persona-presets/_schema.json +146 -0
  137. package/persona-presets/dana.yaml +26 -0
  138. package/persona-presets/haru.yaml +27 -0
  139. package/persona-presets/kai.yaml +27 -0
  140. package/persona-presets/leo.yaml +27 -0
  141. package/persona-presets/mira.yaml +30 -0
  142. package/persona-presets/nari.yaml +27 -0
  143. package/persona-presets/sora.yaml +27 -0
  144. package/persona-presets/yuna.yaml +27 -0
  145. package/plugins/doc-draft/graphs/run-step.ts +1 -1
  146. package/plugins/geo-check/graphs/run-step.ts +34 -10
  147. package/scripts/field-feed/deliver-node.ts +8 -3
  148. package/scripts/field-feed/lib.ts +2 -0
  149. package/scripts/install.sh +20 -11
  150. package/scripts/persona-presets/lint.ts +79 -0
  151. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  152. package/scripts/release-loop/cutoff.ts +20 -2
  153. package/scripts/release-loop/export-check-node.ts +85 -0
  154. package/scripts/release-loop/gate-node.ts +45 -7
  155. package/scripts/release-loop/node-verdict.ts +8 -1
  156. package/scripts/release-loop/npm-publish-node.ts +10 -4
  157. package/scripts/release-loop/ops-upgrade-node.ts +11 -3
  158. package/scripts/release-loop/publish-node.ts +23 -1
  159. package/scripts/release-loop/shard-plan.ts +17 -0
  160. package/scripts/storyboard/storyboard.ts +167 -0
  161. package/scripts/test-diet/lib.ts +89 -0
  162. package/scripts/test-diet/node.ts +144 -0
  163. package/src/acp/auth-methods.ts +30 -0
  164. package/src/acp/server.ts +5 -5
  165. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  166. package/src/auth/setup-link-tokens.ts +153 -0
  167. package/src/budget/unified-usage.ts +75 -0
  168. package/src/channel-bot-token.ts +37 -10
  169. package/src/channel-import/detect.ts +191 -0
  170. package/src/channel-import/run.ts +111 -0
  171. package/src/chat/index.ts +9 -9
  172. package/src/cli/config-test-sync.ts +4 -0
  173. package/src/cli/decisions-cli.ts +13 -3
  174. package/src/cli/doctor-bundle.ts +128 -0
  175. package/src/cli/doctor-cli.ts +36 -1
  176. package/src/cli/doctor-local-llm.ts +95 -0
  177. package/src/cli/import-cli.ts +60 -0
  178. package/src/cli/mcp-login.ts +13 -4
  179. package/src/cli/op-proxy-sync.ts +83 -0
  180. package/src/cli/persona-cli.ts +116 -0
  181. package/src/cli/pr-cli.ts +6 -2
  182. package/src/cli/release-cli.ts +49 -8
  183. package/src/cli/skills-cli.ts +173 -0
  184. package/src/cli/start-cli.ts +13 -4
  185. package/src/codex/setup.ts +15 -13
  186. package/src/dashboard/dashboard-status-lines.ts +45 -0
  187. package/src/dashboard/first-screen-band.ts +35 -0
  188. package/src/dashboard/index.ts +87 -14
  189. package/src/dashboard/provider-slash-runtime.ts +28 -11
  190. package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
  191. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  192. package/src/dashboard/turn-stream-formatter.ts +1 -1
  193. package/src/decisions/decision-cards.ts +291 -0
  194. package/src/decisions/decision-ledger.ts +5 -2
  195. package/src/decisions/discord-decision-cards.ts +133 -0
  196. package/src/decisions/telegram-decision-cards.ts +100 -0
  197. package/src/directives/version-at.ts +77 -0
  198. package/src/discord.ts +17 -0
  199. package/src/domains/outbound-alert.ts +44 -1
  200. package/src/domains/repeated-failure.ts +1 -1
  201. package/src/error-report/report.ts +200 -0
  202. package/src/exec-requests/planner.ts +8 -2
  203. package/src/exec-requests/runner.ts +64 -24
  204. package/src/exec-requests/store.ts +2 -0
  205. package/src/field/field-feed-auto.ts +48 -0
  206. package/src/field/field-reel.ts +8 -1
  207. package/src/hooks/error-report.ts +210 -0
  208. package/src/hooks/receiver.ts +41 -1
  209. package/src/import/apply.ts +128 -0
  210. package/src/import/plan.ts +197 -0
  211. package/src/index.ts +98 -11
  212. package/src/input-core/text-entry.ts +13 -5
  213. package/src/intake-plane/seat-answer.ts +96 -0
  214. package/src/intake-plane/seat-doc-route.ts +17 -3
  215. package/src/maturity/feature-maturity.ts +2 -0
  216. package/src/nexus/api/consult-requests.ts +19 -8
  217. package/src/nexus/api/error-reports.ts +108 -0
  218. package/src/nexus/api/http-server.ts +41 -1
  219. package/src/nexus/api/meta-api.ts +27 -0
  220. package/src/nexus/api/operator.ts +61 -0
  221. package/src/nexus/api/ops-api.ts +139 -0
  222. package/src/nexus/api/ops-seats-sources.ts +65 -0
  223. package/src/nexus/api/ops-seats.ts +114 -0
  224. package/src/nexus/api/outbound-report.ts +7 -0
  225. package/src/nexus/api/public-routes.ts +14 -1
  226. package/src/nexus/api/running-runs-background.ts +68 -0
  227. package/src/nexus/api/running-runs-child.ts +6 -0
  228. package/src/nexus/api/setup-claim.ts +21 -0
  229. package/src/nexus/api/share-captures.ts +91 -0
  230. package/src/nexus/api/skill-problems.ts +56 -0
  231. package/src/nexus/api/terminals.ts +24 -8
  232. package/src/nexus/config/secrets/file-backend.ts +56 -18
  233. package/src/nexus/index.ts +25 -1
  234. package/src/nexus/setup-status.ts +7 -2
  235. package/src/oauth/browser-availability.ts +14 -0
  236. package/src/oauth/codex-quota-policy.ts +16 -0
  237. package/src/onboarding/continue-here.ts +13 -8
  238. package/src/onboarding/first-run-doctor.ts +54 -0
  239. package/src/onboarding/web-first.ts +8 -1
  240. package/src/onboarding.ts +66 -12
  241. package/src/persona/presets.ts +105 -0
  242. package/src/release-loop/checklist.ts +33 -69
  243. package/src/release-loop/feature-store.ts +327 -0
  244. package/src/release-loop/manifest.ts +59 -5
  245. package/src/seat-dispatch/ceo-commands.ts +65 -0
  246. package/src/self-dev/orchestrate.ts +18 -11
  247. package/src/shell-env-bootstrap.ts +44 -7
  248. package/src/skills/connect-claude-plugin.ts +142 -0
  249. package/src/skills/connect.ts +70 -0
  250. package/src/skills/index.ts +29 -1
  251. package/src/skills/repair.ts +124 -0
  252. package/src/skills/runner.ts +11 -2
  253. package/src/skills/tools/usage-slash.ts +8 -7
  254. package/src/skills/trigger-extract.ts +1 -1
  255. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  256. package/src/telegram-commands.ts +29 -0
  257. package/src/telegram-run.ts +83 -13
  258. package/src/telegram.ts +34 -0
  259. package/src/thinking-line.ts +28 -20
  260. package/src/tui/chat/execution-badge.ts +1 -1
  261. package/src/user-config.ts +111 -13
  262. package/src/ux-sim/turn-states.ts +1 -1
  263. package/src/version/packed-revision.json +1 -1
  264. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
  294. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
  295. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
  296. /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
@@ -158,18 +158,42 @@ async function run(step: string): Promise<void> {
158
158
  if (!table.length) throw new Error('missing score table');
159
159
  const gaps = table.filter(cell => cell.error || !cell.mention || cell.citation === false).map(cell => ({ question: cell.question, engine: cell.engine, error: cell.error ?? null, mention: cell.mention, citation: cell.citation }));
160
160
  if (!gaps.length) { output({ suggestions: [] }); return; }
161
- try {
162
- const raw = parseArray(reply(await invoke(['ask', '--json', `Using ONLY this score table, return a JSON array of 3 to 5 improvement suggestions. Each must have "suggestion" (FAQ question, FAQPage/Organization structured data, comparison document, etc.) and "evidence" containing a question and engine from the missing cells. Evidence cells: ${JSON.stringify(gaps)}. Score table: ${JSON.stringify(table)}`])));
163
- if (!Array.isArray(raw) || raw.length < 3 || raw.length > 5) throw new Error('invalid suggestion count');
164
- const suggestions = raw.map(item => {
161
+ // A5c — the model often rewords a question or the engine name; one such item used to fail the whole step,
162
+ // so the same input passed and failed by turns. Evidence is now picked by id, matched leniently, and a wrong
163
+ // item is dropped (not fatal). Only «no valid suggestion at all» retries once, with a stricter prompt.
164
+ const cells = gaps.map((cell, id) => ({ id, ...cell }));
165
+ const norm = (value: unknown) => text(value).toLowerCase().replace(/["'“”‘’`]/g, '').replace(/\s+/g, ' ').trim();
166
+ const pick = (evidence: unknown) => {
167
+ if (typeof evidence === 'number') return cells[evidence];
168
+ const ev = object(evidence);
169
+ if (typeof ev.id === 'number' && cells[ev.id]) return cells[ev.id];
170
+ const q = norm(ev.question), e = norm(ev.engine);
171
+ const exact = cells.find(cell => norm(cell.question) === q && (!e || norm(cell.engine) === e));
172
+ if (exact) return exact;
173
+ const byQuestion = cells.filter(cell => q && (norm(cell.question).includes(q) || q.includes(norm(cell.question))));
174
+ return byQuestion.length === 1 ? byQuestion[0] : undefined;
175
+ };
176
+ const ask = async (strict: boolean) => parseArray(reply(await invoke(['ask', '--json', `Using ONLY this score table, return a JSON array of 3 to 5 improvement suggestions. Each item: {"suggestion": "<FAQ question, FAQPage/Organization structured data, comparison document, etc.>", "evidence": <id of one cell below>}.${strict ? ' The evidence MUST be one of the listed ids as a number. Return only the JSON array.' : ''} Evidence cells (pick by id): ${JSON.stringify(cells)}. Score table: ${JSON.stringify(table)}`])));
177
+ const collect = (raw: unknown) => {
178
+ if (!Array.isArray(raw)) return [];
179
+ const seen = new Set<string>();
180
+ return raw.slice(0, 5).flatMap(item => {
165
181
  const candidate = object(item);
166
- const evidence = object(candidate.evidence);
167
- const match = gaps.find(cell => cell.question === evidence.question && cell.engine === evidence.engine);
168
- if (!text(candidate.suggestion) || !match) throw new Error('suggestion missing valid score evidence');
169
- return { suggestion: text(candidate.suggestion), evidence: { question: match.question, engine: match.engine } };
182
+ const match = pick(candidate.evidence);
183
+ const suggestion = text(candidate.suggestion);
184
+ if (!suggestion || !match || seen.has(suggestion)) return [];
185
+ seen.add(suggestion);
186
+ return [{ suggestion, evidence: { question: match.question, engine: match.engine } }];
170
187
  });
171
- output({ suggestions }, 1);
172
- } catch (e) { output({ outcome: 'fail', suggestions: [], error: errorLine(e) }, 1, 1); }
188
+ };
189
+ let calls = 0;
190
+ try {
191
+ calls++;
192
+ let suggestions = collect(await ask(false));
193
+ if (!suggestions.length) { calls++; suggestions = collect(await ask(true)); }
194
+ if (!suggestions.length) throw new Error('suggestion missing valid score evidence');
195
+ output({ suggestions }, calls);
196
+ } catch (e) { output({ outcome: 'fail', suggestions: [], error: errorLine(e) }, calls || 1, 1); }
173
197
  } else if (step === 'report') {
174
198
  const table = rows(object(outputs.score).table);
175
199
  const answers = rows(object(outputs.answers).answers);
@@ -5,7 +5,7 @@
5
5
  // ⛔ 인스타그램에 올리지 않는다 — 실제 Meta 게시는 다음 판.
6
6
  import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
- import { sendOutbound } from '../../src/domains/outbound-alert.js';
8
+ import { deliver as sendReport } from '../../src/domains/outbound-alert.js';
9
9
  import { coverHtml, readContext, readDraft, result, screenshot, slideHtml, writeDraft } from './lib.js';
10
10
 
11
11
  try {
@@ -41,12 +41,17 @@ try {
41
41
  if (reel) copyFileSync(join(folder, draft.reel!), reel);
42
42
 
43
43
  // FIELD_FEED_NOTIFY=0 — 시험·리허설에서 텔레그램으로 나가지 않게.
44
- const sent = process.env.FIELD_FEED_NOTIFY === '0' ? false : sendOutbound([
44
+ // 경로(daemon=/v1/outbound 수락 · direct=텔레그램 직접 · false=실패)를 그대로 남긴다 — 10-01 운영 실측에서
45
+ // «notified:true» 만 있고 그래프 자식은 debug.log 가 꺼져 있어 도착 근거가 0 이었다. 사람이 «최종 게시»를 누른
46
+ // 직후라 야간 무음 보류는 걸지 않는다(sendOutbound 의 보류 대신 바로 보낸다).
47
+ const notify = process.env.FIELD_FEED_NOTIFY === '0' ? 'off' : sendReport([
45
48
  `✅ 승인됨 · 게시 준비 완료 — ${draft.event.title} (${draft.event.date})`,
46
49
  `캐러셀 ${included.length + 1}장${reel ? ' ⊕ 릴스 1편' : ''} · 초안 r${draft.revision}${draft.updatedBy === 'human' ? '(사람이 고침)' : ''}`,
47
50
  '', text, '', `묶음: ${ready}`, '(인스타그램 게시는 아직 사람이 한다 — 자동 게시는 다음 판)',
48
51
  ].join('\n'), 'report');
49
- result({ ready, slides: included.length + 1, reel: Boolean(reel), redrawn, revision: draft.revision, notified: sent });
52
+ const delivered = { at: new Date().toISOString(), path: notify === false ? 'failed' : notify, revision: draft.revision };
53
+ writeDraft(folder, { ...(readDraft(folder) ?? draft), delivered } as typeof draft);
54
+ result({ ready, slides: included.length + 1, reel: Boolean(reel), redrawn, revision: draft.revision, notified: notify === 'daemon' || notify === 'direct', notifyPath: delivered.path });
50
55
  } catch (error) {
51
56
  console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
52
57
  }
@@ -18,6 +18,8 @@ export type FeedDraft = {
18
18
  hashtags: string[];
19
19
  location: string | null;
20
20
  reel: string | null;
21
+ /** deliver 가 남긴다 — 알림 경로(daemon·direct·failed·off)와 시각. 도착의 근거는 아니고 «어디로 보냈나»다. */
22
+ delivered?: { at: string; path: 'daemon' | 'direct' | 'failed' | 'off'; revision: number };
21
23
  };
22
24
 
23
25
  export const ENGINE = resolve(import.meta.dir, '../../skills/explainer-video/engine');
@@ -177,7 +177,7 @@ for command in "${REQUIRED_COMMANDS[@]}"; do
177
177
  fi
178
178
  done
179
179
  for command in "${HARNESS_COMMANDS[@]}"; do
180
- command -v "$command" >/dev/null 2>&1 || echo "⚠️ harness command missing: $command" >&2
180
+ command -v "$command" >/dev/null 2>&1 || echo "$(t "⚠️ harness command missing: $command (see Next below)" "⚠️ 하니스가 쓰는 명령이 없습니다: $command (아래 «다음» 참고)")" >&2
181
181
  done
182
182
 
183
183
  # bun 의 «실제 실행 파일» — PATH 의 이름이 아니라 bun 이 스스로 말하는 경로(링크를 끝까지 푼 것).
@@ -444,7 +444,7 @@ STEP=1
444
444
  # INST1 order: new shell → elanous (first-time setup, which also signs in) → missing tools → harness say.
445
445
  case ":$PATH:" in
446
446
  *":$PREFIX/bin:"*) ;;
447
- *) if [ "$MODIFY_PATH" -eq 1 ]; then echo " $STEP) $(t "open a new shell (or: source $STARTUP)" "새 셸을 여세요(또는: source $STARTUP)")"; else echo " $STEP) $(t "add $PREFIX/bin to PATH" "$PREFIX/bin 을 PATH 에 넣으세요")"; fi; STEP=$((STEP + 1)) ;;
447
+ *) if [ "$START_SETUP" -eq 1 ]; then :; elif [ "$MODIFY_PATH" -eq 1 ]; then echo " $STEP) $(t "open a new shell (or: source $STARTUP)" "새 셸을 여세요(또는: source $STARTUP)")"; STEP=$((STEP + 1)); else echo " $STEP) $(t "add $PREFIX/bin to PATH" "$PREFIX/bin 을 PATH 에 넣으세요")"; STEP=$((STEP + 1)); fi ;;
448
448
  esac
449
449
  if [ "$START_SETUP" -eq 0 ]; then
450
450
  echo " $STEP) $(t "Run: $PREFIX/bin/elanous # first-time setup (in a new shell: elanous)" "실행: $PREFIX/bin/elanous # 첫 설정(새 셸에서는: elanous)")"; STEP=$((STEP + 1))
@@ -473,7 +473,7 @@ if ! command -v make >/dev/null 2>&1 || ! command -v c++ >/dev/null 2>&1; then
473
473
  if [ -n "$BUILD_HINT" ]; then
474
474
  echo " $STEP) $BUILD_HINT"; STEP=$((STEP + 1))
475
475
  fi
476
- echo " $STEP) elanous doctor --fix --yes # rebuild node-pty after installing build tools"; STEP=$((STEP + 1))
476
+ echo " $STEP) elanous doctor --fix --yes # $(t 'rebuild node-pty after installing build tools' '빌드 도구를 깐 뒤 node-pty 다시 빌드')"; STEP=$((STEP + 1))
477
477
  fi
478
478
  if ! command -v rg >/dev/null 2>&1; then
479
479
  if [ -z "${OS_ID:-}" ] && [ -r "${ELANOUS_INSTALL_OS_RELEASE_FILE:-/etc/os-release}" ]; then
@@ -487,22 +487,22 @@ if ! command -v rg >/dev/null 2>&1; then
487
487
  Linux) case " ${OS_ID:-} ${OS_LIKE:-} " in
488
488
  *' debian '*|*' ubuntu '*) RG_HINT='sudo apt-get install -y ripgrep' ;;
489
489
  *' fedora '*) RG_HINT='sudo dnf install -y ripgrep' ;;
490
- *) RG_HINT='install ripgrep (rg) with your package manager' ;;
490
+ *) RG_HINT="$(t 'install ripgrep (rg) with your package manager' '패키지 관리자로 ripgrep(rg)을 설치하세요')" ;;
491
491
  esac ;;
492
- *) RG_HINT='install ripgrep (rg) with your package manager' ;;
492
+ *) RG_HINT="$(t 'install ripgrep (rg) with your package manager' '패키지 관리자로 ripgrep(rg)을 설치하세요')" ;;
493
493
  esac
494
494
  echo " $STEP) $RG_HINT"; STEP=$((STEP + 1))
495
495
  fi
496
496
  if ! command -v codex >/dev/null 2>&1; then
497
497
  if ! command -v node >/dev/null 2>&1; then
498
- echo " $STEP) install Node.js 20+ (the Codex CLI runs on node) # e.g. your package manager or https://nodejs.org"; STEP=$((STEP + 1))
498
+ echo " $STEP) $(t 'install Node.js 20+ (the Codex CLI runs on node) # e.g. your package manager or https://nodejs.org' 'Node.js 20 이상을 설치하세요(Codex CLI 가 node 로 돕니다) # 예: 패키지 관리자 또는 https://nodejs.org')"; STEP=$((STEP + 1))
499
499
  fi
500
- echo " $STEP) npm install -g @openai/codex # the harness drives the Codex CLI (not yet measured on a fresh machine)"; STEP=$((STEP + 1))
500
+ echo " $STEP) npm install -g @openai/codex # $(t 'the harness drives the Codex CLI' '하니스가 Codex CLI 를 씁니다')"; STEP=$((STEP + 1))
501
501
  fi
502
502
  if ! command -v gh >/dev/null 2>&1; then
503
- echo " $STEP) install gh, then: gh auth login # the harness opens pull requests with it"; STEP=$((STEP + 1))
503
+ echo " $STEP) $(t 'install gh, then: gh auth login # the harness opens pull requests with it' 'gh 를 설치한 뒤: gh auth login # 하니스가 PR 을 열 때 씁니다')"; STEP=$((STEP + 1))
504
504
  elif ! gh auth status >/dev/null 2>&1; then
505
- echo " $STEP) gh auth login # the harness opens pull requests with it"; STEP=$((STEP + 1))
505
+ echo " $STEP) gh auth login # $(t 'the harness opens pull requests with it' '하니스가 PR 을 열 때 씁니다')"; STEP=$((STEP + 1))
506
506
  fi
507
507
  if [ "$ELN_AVAILABLE" -eq 1 ]; then
508
508
  echo " $STEP) $(t 'eln harness say "<one line of what you want>" (or: elanous harness say)' 'eln harness say "<하고 싶은 일 한 줄>" (또는: elanous harness say)')"
@@ -519,8 +519,17 @@ if [ "$START_SETUP" -eq 1 ]; then
519
519
  *":$PREFIX/bin:"*) ;;
520
520
  *) echo " $(t "(new shells will find elanous on PATH; this window uses $PREFIX/bin/elanous)" "(새 셸에서는 PATH 로 elanous 를 찾습니다 · 이 창은 $PREFIX/bin/elanous 를 씁니다)")" ;;
521
521
  esac
522
+ # INST3 (10-01 · node-c tmux): a wizard whose stdin is a fresh open of /dev/tty never saw a key on macOS — its event
523
+ # loop cannot watch that fd (the same binary started from a shell, which hands over the real tty, worked). So give it
524
+ # the real terminal device; fall back to /dev/tty only when there is none.
525
+ SETUP_TTY=/dev/tty
526
+ REAL_TTY="$(ps -o tty= -p $$ 2>/dev/null | tr -d ' ')"
527
+ case "$REAL_TTY" in
528
+ ''|'?'|'??') ;;
529
+ *) if [ -r "/dev/$REAL_TTY" ] && [ -w "/dev/$REAL_TTY" ]; then SETUP_TTY="/dev/$REAL_TTY"; fi ;;
530
+ esac
522
531
  if [ -n "${ELANOUS_INSTALL_SETUP_EXEC:-}" ]; then
523
- exec "$ELANOUS_INSTALL_SETUP_EXEC" "$PREFIX/bin/elanous"
532
+ ELANOUS_INSTALL_SETUP_TTY="$SETUP_TTY" exec "$ELANOUS_INSTALL_SETUP_EXEC" "$PREFIX/bin/elanous"
524
533
  fi
525
- exec "$PREFIX/bin/elanous" </dev/tty
534
+ exec "$PREFIX/bin/elanous" <"$SETUP_TTY"
526
535
  fi
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bun
2
+ // 페르소나 프리셋 문면 검사(PS1 · 2026-10-02) — persona-presets/<id>.yaml
3
+ // bun scripts/persona-presets/lint.ts 오류가 있으면 rc 1 · 경고만이면 rc 0
4
+ // 지키는 것: 도구는 «지금 설치할 수 있는 것»만(공개 묶음 표 · 저장소 플러그인 · core) · 용어(«캐릭터» 금지) ·
5
+ // 자리(OP·MK·TC·UX)와 이름이 겹치지 않음 · 직함은 선택 속성 · 과장 금지어.
6
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
7
+ import { join, resolve } from 'node:path';
8
+ import { parse } from 'yaml';
9
+
10
+ const ROOT = resolve(import.meta.dir, '../..');
11
+ const DIR = join(ROOT, 'persona-presets');
12
+
13
+ export type Preset = {
14
+ personaId: string; displayName: string; names: string[]; role: string; title?: string | null; oneLine: string;
15
+ voice: { rule: string; examples: string[] }; tasks: Array<{ when: string; what: string }>;
16
+ tools: Array<{ name: string; from: string; optional?: boolean }>; firstQuestions: string[]; askBefore?: string[]; doesNot: string[]; forWhom: string;
17
+ };
18
+
19
+ const CORE = new Set(['harness']);
20
+ const TITLES = new Set(['COO', 'CMO', 'CTO', 'CXO']);
21
+ const SEATS = /^(OP|MK|TC|UX)$/i;
22
+ const BANNED = ['캐릭터', '완전 자율', '무엇이든', '유일', '알아서 다'];
23
+
24
+ /** Official packs and the skills each one lists, read from the public plugin table (the same page users read). */
25
+ export function officialPacks(root = ROOT): Map<string, string> {
26
+ const table = readFileSync(join(root, 'release/public/docs/plugins.md'), 'utf8');
27
+ const packs = new Map<string, string>();
28
+ for (const row of table.split('\n')) {
29
+ const m = row.match(/^\| `([a-z0-9-]+)` \| (.*) \| (.*) \|$/);
30
+ if (m && !/not published/i.test(m[3]!)) packs.set(m[1]!, m[2]!);
31
+ }
32
+ return packs;
33
+ }
34
+
35
+ export function lintPreset(p: Preset, file: string, packs: Map<string, string>, root = ROOT): { errors: string[]; warnings: string[] } {
36
+ const errors: string[] = []; const warnings: string[] = [];
37
+ const need = (ok: unknown, msg: string) => { if (!ok) errors.push(msg); };
38
+ need(/^[a-z][a-z0-9-]{1,30}$/.test(p.personaId ?? '') && file.endsWith(`/${p.personaId}.yaml`), 'personaId = 파일 이름(소문자)');
39
+ need(p.displayName && p.role && p.oneLine && p.forWhom, 'displayName · role · oneLine · forWhom 필요');
40
+ need(Array.isArray(p.names) && p.names.length >= 2, 'names 후보 둘 이상');
41
+ need(p.voice?.rule && p.voice.examples?.length === 2, 'voice = rule ⊕ 예시 두 줄');
42
+ need(p.tasks?.length === 3 && p.tasks.every((t) => t.when && t.what), 'tasks = 할 일 셋(when · what)');
43
+ need(p.firstQuestions?.length >= 2 && p.firstQuestions.length <= 3, 'firstQuestions = 처음 묻는 칸 둘~셋');
44
+ need(p.doesNot?.length >= 2, 'doesNot = 하지 않는 것 둘 이상');
45
+ if (p.title != null && !TITLES.has(p.title)) errors.push(`title 은 없음 또는 ${[...TITLES].join('·')}`);
46
+ for (const name of p.names ?? []) if (SEATS.test(name)) errors.push(`이름 «${name}» 이 자리(OP·MK·TC·UX)와 겹친다`);
47
+ const text = JSON.stringify(p);
48
+ for (const word of BANNED) if (text.includes(word)) errors.push(`금지어 «${word}»`);
49
+ for (const tool of p.tools ?? []) {
50
+ if (tool.from === 'core') { if (!CORE.has(tool.name)) errors.push(`core 도구 «${tool.name}» 는 없다`); continue; }
51
+ const row = packs.get(tool.from);
52
+ if (row !== undefined) {
53
+ if (tool.from !== tool.name && !row.includes(`\`${tool.name}\``)) errors.push(`«${tool.name}» 이 묶음 «${tool.from}» 의 목록에 없다`);
54
+ continue;
55
+ }
56
+ // 공식 묶음에 없는 도구 = 저장소 플러그인일 때만 · 반드시 optional(공개 판 사용자는 따로 설치해야 쓴다)
57
+ if (tool.from !== tool.name || !existsSync(join(root, 'plugins', tool.name, 'plugin.json'))) errors.push(`«${tool.name}» 은 공식 묶음에도 plugins/${tool.name}/plugin.json 에도 없다`);
58
+ else if (tool.optional !== true) errors.push(`«${tool.name}» 은 공식 묶음에 없다 — optional: true 로 둔다`);
59
+ else warnings.push(`«${tool.name}» 은 선택 도구(공식 묶음 게재 전)`);
60
+ }
61
+ return { errors, warnings };
62
+ }
63
+
64
+ if (import.meta.main) {
65
+ const packs = officialPacks();
66
+ const index = parse(readFileSync(join(DIR, '_index.yaml'), 'utf8')) as { presets: string[] };
67
+ const files = readdirSync(DIR).filter((f) => f.endsWith('.yaml') && !f.startsWith('_'));
68
+ let bad = 0; const names = new Map<string, string>();
69
+ for (const f of files) {
70
+ const p = parse(readFileSync(join(DIR, f), 'utf8')) as Preset;
71
+ const { errors, warnings } = lintPreset(p, `persona-presets/${f}`, packs);
72
+ if (!index.presets.includes(p.personaId)) errors.push('_index.yaml presets 에 없다');
73
+ for (const n of p.names ?? []) { if (names.has(n)) errors.push(`이름 «${n}» 이 ${names.get(n)} 와 겹친다`); names.set(n, p.personaId); }
74
+ for (const e of errors) console.log(`✗ ${f}: ${e}`);
75
+ for (const w of warnings) console.log(`⚠ ${f}: ${w}`);
76
+ if (errors.length) bad++; else console.log(`✓ ${f} · ${p.displayName} · 도구 ${p.tools.length}`);
77
+ }
78
+ process.exit(bad ? 1 : 0);
79
+ }
@@ -7,14 +7,14 @@ export function runChecklistGate(context: GraphContext = readGraphContext()) {
7
7
  const data = listChecklist(version);
8
8
  const gate = checklistGate(version);
9
9
  const counts = summarizeChecklist(data);
10
- const summary = `확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}`;
10
+ const summary = `확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}${gate.parity?.length ? ` · ⚠ 짝 경고 ${gate.parity.length}(${gate.parity.map((p) => p.id).join(', ')})` : ''}`;
11
11
  if (gate.ok && gate.moved.length) {
12
12
  const [major, minor, patch] = version.split('.').map(Number);
13
13
  const next = `${major}.${minor}.${patch! + 1}`;
14
14
  for (const id of gate.moved) {
15
15
  const item = data.items.find((candidate) => candidate.id === id)!;
16
16
  if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
17
- try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}) }); }
17
+ try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }); }
18
18
  catch (error) {
19
19
  if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
20
20
  }
@@ -4,13 +4,14 @@ import { mkdirSync, realpathSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { debug } from '../../src/debug/log.js';
6
6
  import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
7
- import { buildReleaseManifest, type ReleaseLanding } from '../../src/release-loop/manifest.js';
7
+ import { buildReleaseManifest, parseNextMdNotes, type NextMdNote, type ReleaseLanding } from '../../src/release-loop/manifest.js';
8
8
  import { readReleaseNotes, releaseNotesDir } from '../../src/release-loop/release-note.js';
9
9
  import { queryRunningRuns } from '../../src/self-implement/running-runs.js';
10
10
  import { loadFederatedRunLedger } from '../../src/self-implement/run-ledger.js';
11
11
 
12
12
  const SEED = 'c1cc37478';
13
13
  const CLI_DOC = 'release/public/docs/cli.md';
14
+ const NEXT_MD = 'release/next.md';
14
15
 
15
16
  function git(args: string[], optional = false): string | null {
16
17
  const result = spawnSync('git', args, { cwd: process.cwd(), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
@@ -61,6 +62,22 @@ function landingAt(sha: string, title: string): ReleaseLanding {
61
62
  };
62
63
  }
63
64
 
65
+ /** next.md lines at the cutoff, each tied to the last landing in range whose diff added that exact line. */
66
+ function nextMdNotes(cutoff: string, landings: ReleaseLanding[]): NextMdNote[] {
67
+ const text = git(['show', `${cutoff}:${NEXT_MD}`], true);
68
+ if (!text) return [];
69
+ const addedBy = new Map<string, string>();
70
+ for (const landing of landings) {
71
+ if (!landing.changedFiles.includes(NEXT_MD)) continue;
72
+ const diff = requiredGit(['show', '-m', '--first-parent', '--format=', '-U0', landing.sha, '--', NEXT_MD]);
73
+ for (const line of diff.split('\n')) if (line.startsWith('+') && !line.startsWith('+++')) addedBy.set(line.slice(1).trim(), landing.sha);
74
+ }
75
+ return parseNextMdNotes(text).map((note) => {
76
+ const sha = note.raw ? addedBy.get(note.raw) : undefined;
77
+ return { kind: note.kind, line: note.line, ...(sha ? { sha } : {}) };
78
+ });
79
+ }
80
+
64
81
  function runningGoalPaths(): string[] {
65
82
  const repo = process.cwd();
66
83
  const paths: string[] = [];
@@ -119,10 +136,11 @@ export function main(args: string[] = process.argv.slice(2)): void {
119
136
  const notes = readReleaseNotes(releaseNotesDir(instanceRoot));
120
137
  debug.log('release.notes', 'read', { fragments: notes.size, problems: notes.problems });
121
138
  const manifest = buildReleaseManifest({
122
- version, baseline: { ref: baseline.ref, sha: baselineSha }, cutoff: { sha: cutoff }, landings, notes,
139
+ version, baseline: { ref: baseline.ref, sha: baselineSha }, cutoff: { sha: cutoff }, landings, notes, nextMd: nextMdNotes(cutoff, landings),
123
140
  runningGoalPaths: runningGoalPaths(), publicCommandsBefore: commandsAt(baselineSha), publicCommandsAfter: commandsAt(cutoff),
124
141
  });
125
142
  const output = { ...manifest, baselineSource: baseline.source };
143
+ debug.log('release-loop.notes', 'fragments', { version, ...manifest.fragments });
126
144
  const directory = join(instanceRoot, 'release', version);
127
145
  mkdirSync(directory, { recursive: true });
128
146
  writeFileSync(join(directory, 'manifest.json'), JSON.stringify(output, null, 2) + '\n');
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { finishNode, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
6
+
7
+ export interface ExportCheckResult extends Record<string, unknown> {
8
+ outcome: 'ok' | 'fail';
9
+ verdict: 'pass' | 'fail';
10
+ summary: string;
11
+ step: 'export' | 'build';
12
+ tail: string;
13
+ }
14
+
15
+ function failed(step: ExportCheckResult['step'], message: string, result?: CommandResult): ExportCheckResult {
16
+ const tail = (result ? result.stderr || result.stdout : message).trim().slice(-300);
17
+ return { outcome: 'fail', verdict: 'fail', summary: `${step} failed: ${message}${result ? ` (rc=${result.status})` : ''}`, step, tail };
18
+ }
19
+
20
+ /** Check the same public export and PWA build as release prepare, without cloning or writing to a public remote. */
21
+ export function runExportCheck(run: CommandRunner = runCommand, repoRoot = process.cwd()): ExportCheckResult {
22
+ const context = readGraphContext();
23
+ const commit = nodeOutput(context, 'version-release', 'commit');
24
+ if (context.outputs.gate?.outcome !== 'ok') return failed('export', 'gate did not pass');
25
+ const root = resolve(repoRoot);
26
+ const out = mkdtempSync(join(tmpdir(), 'elanous-export-check-'));
27
+ const sourceDir = join(out, 'source');
28
+ let added = false;
29
+ let step: ExportCheckResult['step'] = 'export';
30
+ let result: ExportCheckResult = failed(step, 'check incomplete');
31
+ try {
32
+ const modulesRoot = dirname(realpathSync(join(root, 'node_modules')));
33
+ const add = run('git', ['worktree', 'add', '-q', '--detach', sourceDir, commit], root);
34
+ if (add.status !== 0) result = failed('export', 'git worktree add', add);
35
+ else {
36
+ added = true;
37
+ const head = run('git', ['rev-parse', 'HEAD'], sourceDir);
38
+ if (head.status !== 0 || head.stdout.trim() !== commit) result = failed('export', 'cut commit mismatch', head);
39
+ else {
40
+ const version = (JSON.parse(readFileSync(join(sourceDir, 'package.json'), 'utf8')) as { version?: string }).version;
41
+ if (version !== context.input.version) result = failed('export', `package.json version ${version} != ${context.input.version}`);
42
+ else {
43
+ symlinkSync(join(modulesRoot, 'node_modules'), join(sourceDir, 'node_modules'), 'dir');
44
+ const exported = run('bun', ['scripts/public-export.ts', '--out', join(out, 'export')], sourceDir);
45
+ if (exported.status !== 0) result = failed('export', 'public export', exported);
46
+ else {
47
+ step = 'build';
48
+ const publicDir = join(out, 'export');
49
+ symlinkSync(join(modulesRoot, 'node_modules'), join(publicDir, 'node_modules'), 'dir');
50
+ const pwaModules = join(modulesRoot, 'apps', 'pwa', 'node_modules');
51
+ if (existsSync(pwaModules) && existsSync(join(publicDir, 'apps', 'pwa'))) symlinkSync(pwaModules, join(publicDir, 'apps', 'pwa', 'node_modules'), 'dir');
52
+ const built = run('bun', ['bin/elanous.mjs', 'nexus', 'build'], publicDir);
53
+ result = built.status === 0 && existsSync(join(publicDir, 'apps', 'pwa', 'out', 'index.html'))
54
+ ? { outcome: 'ok', verdict: 'pass', summary: `public export and PWA build ${commit.slice(0, 12)}`, step: 'build', tail: '' }
55
+ : failed('build', 'PWA build or out/index.html missing', built);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ } catch (error) {
61
+ result = failed(step, error instanceof Error ? error.message : String(error));
62
+ } finally {
63
+ try {
64
+ if (added) {
65
+ const removed = run('git', ['worktree', 'remove', '--force', sourceDir], root);
66
+ if (removed.status !== 0) result = failed(step, 'git worktree remove', removed);
67
+ }
68
+ } catch (error) {
69
+ result = failed(step, `git worktree remove: ${error instanceof Error ? error.message : String(error)}`);
70
+ } finally {
71
+ rmSync(out, { recursive: true, force: true });
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+
77
+ if (import.meta.main) {
78
+ let version = '';
79
+ try {
80
+ version = readGraphContext().input.version;
81
+ process.exitCode = finishNode('export-check', version, runExportCheck());
82
+ } catch (error) {
83
+ process.exitCode = finishNode('export-check', version, failed('export', error instanceof Error ? error.message : String(error)));
84
+ }
85
+ }
@@ -10,7 +10,7 @@ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
10
10
  import { diffFailures, junitFailures, parseFailures } from './gate-diff';
11
11
  import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
12
12
  import { baselineFromCutLogs } from './baseline-from-logs';
13
- import { planShards, readFileDurations } from './shard-plan';
13
+ import { planShards, readFileDurations, readFileMemory } from './shard-plan';
14
14
  import { deriveCdpTestPatterns } from '../test-deterministic';
15
15
  import { emitNodeResult, readGraphContext } from './node-verdict.js';
16
16
  import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
@@ -112,11 +112,27 @@ const summaryCount = (output: string, label: string) => Number(lastMatch(output,
112
112
  /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
113
113
  const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
114
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';
115
120
  /** Integration tests that the Pod sweep does not run — each is judged by a real run elsewhere at the cut.
116
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)
117
122
  * sat in a shard of its own past the 1200 s deadline with no output and stopped the whole gate. Judged on the
118
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
+
119
127
  export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
128
+ /** Whole-repository audits run alone in the nightly test-diet graph, not in release-gate shards. */
129
+ export const GATE_NIGHTLY_AUDITS: readonly string[] = [
130
+ 'test/f12-sweep.test.ts', // Sweeps the whole repository; 7.8 GB even after TD1a.
131
+ 'scripts/unwired-exports.test.ts', // Compiles and scans all exports; 9.9 GB.
132
+ 'test/guardian/dispatch-surface-contract.test.ts', // Compiles repository-wide dispatch contracts; 8.3 GB.
133
+ 'test/pwa-build-typecheck.test.ts', // Builds and typechecks the whole PWA; 4.2 GB.
134
+ 'test/user-config-mcp.test.ts', // 2.1 GB peak; keep the gate below the 2 GB audit budget.
135
+ ];
120
136
  const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
121
137
  const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
122
138
  let last: string | undefined;
@@ -203,12 +219,21 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
203
219
  // CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
204
220
  // bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
205
221
  const assignable = files.filter((file) => !cdpPatterns.includes(file) && !POD_SWEEP_INTEGRATION_ONLY.includes(file)
206
- && !file.split('/').some((part) => part.startsWith('.')));
222
+ && !GATE_NIGHTLY_AUDITS.includes(file) && !file.split('/').some((part) => part.startsWith('.')));
207
223
  const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
208
224
  if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
225
+ const nightlyAudits = files.filter((file) => GATE_NIGHTLY_AUDITS.includes(file));
226
+ if (nightlyAudits.length) debug.log('release-loop.gate', 'pod-sweep-nightly-audit', { files: nightlyAudits });
209
227
  if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
210
228
  const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
211
- const shards = planShards(assignable, durations, shardCount);
229
+ const memory = readFileMemory(join(tree, POD_MEMORY_SOURCE));
230
+ const heavy = new Set(assignable.filter((file) => (memory.get(file) ?? 0) >= POD_HEAVY_FILE_MB));
231
+ const light = assignable.filter((file) => !heavy.has(file));
232
+ const shards = [
233
+ ...(light.length ? planShards(light, durations, shardCount) : []),
234
+ ...[...heavy].map((file) => ({ files: [file], plannedSeconds: durations.get(file) ?? 0 })),
235
+ ];
236
+ if (heavy.size) debug.log('release-loop.gate', 'pod-heavy-alone', { files: [...heavy], thresholdMb: POD_HEAVY_FILE_MB, memoryLimit: POD_ISOLATION_MEMORY_LIMIT });
212
237
  const known = assignable.filter((file) => durations.has(file)).length;
213
238
  debug.log('release-loop.gate', 'pod-shard-plan', {
214
239
  shards: shards.length, known, unknown: assignable.length - known,
@@ -259,7 +284,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
259
284
  pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
260
285
  ...(bunCache ? { bunCache } : {}),
261
286
  // 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
262
- ...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
287
+ ...(paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
263
288
  name: `gate-${randomUUID()}`,
264
289
  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"`],
265
290
  });
@@ -350,6 +375,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
350
375
  ...(lastFile ? { lastFile } : {}),
351
376
  ...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
352
377
  }] };
378
+ // GT1 — a big shard that failed at the root goes straight to cap-sized chunks instead of halving twice: memory
379
+ // piles up when one bun process runs many files (0.2.7: only bundles of 54+ OOMed), and each halving round costs
380
+ // a Pod start, an install and a rerun. A failing chunk then isolates its files (depth 2), as before.
381
+ if (depth === 0 && paths.length > POD_SHARD_FILE_CAP) {
382
+ 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));
383
+ debug.log('release-loop.gate', 'pod-shard-cap-split', { shard, files: paths.length, chunks: chunks.length, cap: POD_SHARD_FILE_CAP, reason });
384
+ const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`)));
385
+ return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
386
+ }
353
387
  debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
354
388
  const middle = Math.ceil(paths.length / 2);
355
389
  const children = await Promise.all([
@@ -403,9 +437,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
403
437
  const outputs: string[] = [];
404
438
  let total = { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 };
405
439
  for (const group of groups) {
406
- if (!group.paths.length || !files.some((f) => group.paths.some((p) => f === p || f.startsWith(`${p}/`)))) continue;
440
+ if (!group.paths.length || !files.some((f) => !GATE_NIGHTLY_AUDITS.includes(f) && group.paths.some((p) => f === p || f.startsWith(`${p}/`)))) continue;
407
441
  const start = Date.now();
408
- const groupIgnores = cdpPatterns.filter((pattern) => group.paths.some((p) => pattern === p || pattern.startsWith(`${p}/`)))
442
+ const groupIgnores = [...new Set([...cdpPatterns, ...GATE_NIGHTLY_AUDITS])]
443
+ .filter((pattern) => files.includes(pattern) && group.paths.some((p) => pattern === p || pattern.startsWith(`${p}/`)))
409
444
  .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
410
445
  const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths.map(asPath)], tree);
411
446
  if (logDir) {
@@ -584,7 +619,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
584
619
  };
585
620
  let baseline: SweepFailures | undefined = trusted ? { failures: trusted.failures, errors: trusted.errors ?? [] } : undefined;
586
621
  if (!baseline) {
587
- const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'));
622
+ // K9b — with no reusable ledger the baseline is swept on the same Pod pool as the cut, not on this host (0.2.6: an
623
+ // hour of local baseline after a 23-minute Pod cut).
624
+ const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'),
625
+ opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
588
626
  baseline = failuresOf(baseRun, 'baseline sweep');
589
627
  }
590
628
  for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
@@ -57,6 +57,11 @@ export function errorResult(error: unknown): { outcome: 'error'; verdict: 'fail'
57
57
  return { outcome: 'error', verdict: 'fail', summary: error instanceof Error ? error.message : String(error) };
58
58
  }
59
59
 
60
+ /** A cut whose notes would show users nothing fails the cutoff instead of shipping a «Plus N internal changes.» page. */
61
+ export function cutoffNotesEmpty(data: Record<string, unknown> | undefined): boolean {
62
+ return Array.isArray(data?.escalate) && (data.escalate as Array<{ kind?: string }>).some((item) => item?.kind === 'notes-empty');
63
+ }
64
+
60
65
  // Existing cutoff/PWA/TUI programs have different output conventions; normalize them at the graph boundary.
61
66
  if (import.meta.main) {
62
67
  const node = process.argv[2];
@@ -78,7 +83,9 @@ if (import.meta.main) {
78
83
  const baseline = data?.baseline as { sha?: string } | undefined;
79
84
  const outcome: NodeOutcome = node === 'cutoff' ? run.status === 0 && data?.version === version && (data?.cutoff as { sha?: string } | undefined)?.sha === commit && typeof baseline?.sha === 'string' ? 'ok' : 'error'
80
85
  : run.status === 0 && (verdict === 'pass' || verdict === 'flaky') ? 'ok' : run.status === 1 ? 'fail' : 'error';
81
- result = { ...data, outcome, verdict: outcome === 'ok' ? verdict === 'flaky' ? 'flaky' : 'pass' : 'fail',
86
+ if (node === 'cutoff' && outcome === 'ok' && cutoffNotesEmpty(data)) {
87
+ result = { ...data, outcome: 'fail', verdict: 'fail', summary: 'cutoff notes-empty — 사용자에게 보일 변경 줄이 0이다(release/next.md 와 PR 노트 조각을 확인하라)' };
88
+ } else result = { ...data, outcome, verdict: outcome === 'ok' ? verdict === 'flaky' ? 'flaky' : 'pass' : 'fail',
82
89
  summary: outcome === 'ok' ? `${node} ${verdict === 'flaky' ? 'flaky' : 'pass'}` : `${node} ${data?.error || run.stderr.trim() || data?.verdict || 'failed'} (rc=${run.status})` };
83
90
  } catch (error) { result = errorResult(error); }
84
91
  process.exitCode = finishNode(node ?? 'unknown', version, result);
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawnSync } from 'node:child_process';
3
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { debug } from '../../src/debug/log.js';
7
7
  import { sendOutbound } from '../../src/domains/outbound-alert.js';
8
- import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
8
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
9
9
  import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
10
10
 
11
11
  type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
@@ -16,6 +16,8 @@ export interface NpmPublishDeps {
16
16
  registry?: Registry;
17
17
  sleep?: (ms: number) => Promise<void>;
18
18
  stateDir?: string;
19
+ /** Test seam — the production root whose secrets/npm-token is the last fallback. */
20
+ productionRoot?: string;
19
21
  alert?: (text: string, kind: string) => boolean;
20
22
  log?: (version: string, waitedMinutes: number) => void;
21
23
  }
@@ -48,7 +50,10 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
48
50
  try {
49
51
  if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
50
52
  const stateDir = deps.stateDir ?? effectiveInstanceRoot();
51
- const candidate = join(stateDir, 'release', version, 'prepared', 'dist', 'elanous.tgz');
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');
52
57
  const run = deps.exec ?? exec;
53
58
  const readRegistry = deps.registry ?? registry;
54
59
  const wait = deps.sleep ?? sleep;
@@ -71,7 +76,8 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
71
76
  const before = await inspect();
72
77
  if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with latest tag` };
73
78
  if (!before.visible) {
74
- const tokenFile = context.input.npmTokenFile ?? join(stateDir, 'secrets', 'npm-token');
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');
75
81
  if (typeof tokenFile !== 'string' || !tokenFile.trim()) throw new Error('input.npmTokenFile must be a path');
76
82
  token = readFileSync(tokenFile, 'utf8').trim();
77
83
  if (!token) throw new Error('npm token file empty');