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
@@ -2,7 +2,7 @@
2
2
  import { homedir } from 'node:os';
3
3
  import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
4
4
 
5
- interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string }
5
+ interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string }
6
6
 
7
7
  function firstLine(text: string): string { return text.trim().split(/\r?\n/)[0]?.trim() ?? ''; }
8
8
 
@@ -44,7 +44,8 @@ export function runOpsUpgrade(run: CommandRunner = runCommand) {
44
44
  }
45
45
  const command = (args: string[]): CommandResult => host === 'local'
46
46
  ? run('elanous', args)
47
- : run('ssh', [host, `PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"; export PATH; elanous ${args.map(quote).join(' ')}`]);
47
+ // The installer's own bin dir comes first — a --no-modify-path install (the bot VM) has elanous only there.
48
+ : run('ssh', [host, `PATH="$HOME/.local/share/elanous/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH"; export PATH; elanous ${args.map(quote).join(' ')}`]);
48
49
  let before = '';
49
50
  let after = '';
50
51
  try {
@@ -68,7 +69,14 @@ export function runOpsUpgrade(run: CommandRunner = runCommand) {
68
69
  results.push({ host, ok: false, before, after, error: `올렸는데 판이 그대로 (기대 ${version}, 실제 ${after || '없음'})` });
69
70
  continue;
70
71
  }
71
- results.push({ host, ok: true, before, after });
72
+ // The webhook receiver (`elanous hooks serve`, a user systemd unit on the bot VM) keeps the old code until it
73
+ // restarts. try-restart touches it only where it runs; elsewhere the unit is absent and nothing happens.
74
+ let hooks: string | undefined;
75
+ if (host !== 'local' && context.input.opsRestart === true) {
76
+ const restarted = run('ssh', [host, 'systemctl --user try-restart elanous-hooks.service 2>/dev/null && systemctl --user is-active elanous-hooks.service 2>/dev/null || echo absent']);
77
+ hooks = firstLine(restarted.stdout) || 'absent';
78
+ }
79
+ results.push({ host, ok: true, before, after, ...(hooks ? { hooks } : {}) });
72
80
  } catch (error) {
73
81
  results.push({ host, ok: false, before, after, error: firstLine(String(error)) });
74
82
  }
@@ -17,6 +17,22 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
17
17
  });
18
18
  }
19
19
 
20
+ /** Polls each release asset until it answers 200 (following redirects). Returns the names still missing. */
21
+ export function waitForAssets(run: CommandRunner, repo: string, tag: string, names: readonly string[], waitSeconds: unknown = 600,
22
+ sleep: (ms: number) => void = (ms) => Bun.sleepSync(ms)): string[] {
23
+ if (!repo || names.length === 0) return [];
24
+ const limit = typeof waitSeconds === 'number' && Number.isFinite(waitSeconds) && waitSeconds >= 0 ? Math.min(waitSeconds, 1800) : 600;
25
+ let pending = [...names];
26
+ for (let waited = 0; ; waited += 15) {
27
+ pending = pending.filter((name) => {
28
+ const probe = run('curl', ['-sIL', '-o', '/dev/null', '-w', '%{http_code}', '-m', '20', `https://github.com/${repo}/releases/download/${tag}/${name}`]);
29
+ return probe.stdout.trim() !== '200';
30
+ });
31
+ if (pending.length === 0 || waited >= limit) return pending;
32
+ sleep(15_000);
33
+ }
34
+ }
35
+
20
36
  export function runPublish(run: CommandRunner = runCommand) {
21
37
  const context = readGraphContext();
22
38
  const version = context.input.version;
@@ -50,7 +66,13 @@ export function runPublish(run: CommandRunner = runCommand) {
50
66
  const result = lastResult(published);
51
67
  if (published.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `release publish failed: ${result?.error ?? published.stderr.trim()}` };
52
68
  if (published.status !== 0 || result?.ok !== true || result.published !== true || result.tag !== `v${version}`) throw new Error(`release publish incomplete or tag mismatch: ${result?.error ?? published.stderr.trim()}`);
53
- return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `published v${version}`, tag: `v${version}` };
69
+ // REL3 — «published» only once every asset downloads. 10-01: the run went on to verify before GitHub served the
70
+ // assets (release created 13:16Z, assets 13:20Z, run ended 13:15Z) and verify failed on a missing installer.
71
+ const repo = typeof result.publicRepo === 'string' ? result.publicRepo : '';
72
+ const names = Array.isArray(result.assets) ? (result.assets as unknown[]).filter((a): a is string => typeof a === 'string').map((a) => a.split('/').at(-1)!) : [];
73
+ const pending = waitForAssets(run, repo, `v${version}`, names, context.input.assetWaitSeconds);
74
+ if (pending.length) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `published v${version} but assets not downloadable yet: ${pending.join(', ')}`, tag: `v${version}` };
75
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `published v${version} · ${names.length} assets downloadable`, tag: `v${version}` };
54
76
  } finally { rmSync(dir, { recursive: true, force: true }); }
55
77
  }
56
78
 
@@ -26,6 +26,23 @@ export function readFileDurations(dir: string): Map<string, number> {
26
26
  return durations;
27
27
  }
28
28
 
29
+ /** Per-file peak memory (MB) from a TSV with `file` and `rss_mb` columns (TD1 whole-gate measurement); missing → empty. */
30
+ export function readFileMemory(path: string): Map<string, number> {
31
+ const memory = new Map<string, number>();
32
+ let lines: string[];
33
+ try { lines = readFileSync(path, 'utf8').split(/\r?\n/); } catch { return memory; }
34
+ const header = lines[0]?.split('\t') ?? [];
35
+ const fileAt = header.indexOf('file');
36
+ const rssAt = header.indexOf('rss_mb');
37
+ if (fileAt < 0 || rssAt < 0) return memory;
38
+ for (const line of lines.slice(1)) {
39
+ const cells = line.split('\t');
40
+ const mb = Number(cells[rssAt]);
41
+ if (cells[fileAt] && cells[rssAt] !== '' && Number.isFinite(mb) && mb >= 0) memory.set(cells[fileAt]!, mb);
42
+ }
43
+ return memory;
44
+ }
45
+
29
46
  /** Longest processing time first; tie-breaks use lexical paths, then the least populated lightest shard. */
30
47
  export function planShards(files: string[], durations: Map<string, number>, count: number): Array<{ files: string[]; plannedSeconds: number }> {
31
48
  if (!Number.isSafeInteger(count) || count < 1) throw new Error('invalid shard count');
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bun
2
+ // 스토리보드 정본(storyboards/<kind>/<id>.yaml) — 검사 ⊕ MD 뽑기 (대표 10-01 23:0x · OP 형식 결정 = YAML 정본 ⊕ 스키마 ⊕ 생성 MD).
3
+ // bun scripts/storyboard/storyboard.ts lint [파일…] 스키마 필수 칸 · 시간 겹침 · 화면 문구 출처 · 승인본 규칙
4
+ // bun scripts/storyboard/storyboard.ts render [파일…] 내부 문서 `STORYBOARD-<id>-v<version>` 를 다시 쓴다(손으로 안 고친다)
5
+ // 인자가 없으면 storyboards/ 아래 전부. lint 는 오류가 있으면 rc 1, 경고만이면 rc 0.
6
+ import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
7
+ import { join, relative, resolve } from 'node:path';
8
+ import { parse } from 'yaml';
9
+
10
+ const ROOT = resolve(import.meta.dir, '../..');
11
+ const DIR = join(ROOT, 'storyboards');
12
+
13
+ type Text = { text: string; source: string; ref?: string };
14
+ type Shot = {
15
+ id: string; t: [number, number]; stage: string; layer?: string; persona?: string; purpose: string; subject?: string; action?: string;
16
+ camera?: { move?: string; lens_mm?: number; angle?: string; framing?: string }; lighting?: string; color?: string;
17
+ transition_in?: { type: string; intent: string }; on_screen_text?: Text[]; audio?: { vo?: string; sfx?: string; bgm?: string };
18
+ generation?: { tool: string; model?: string; prompt?: string; asset?: string }; real_or_staged: string; state: string; notes?: string;
19
+ };
20
+ /** kind: site — a site's page structure, menu and copy draft, planned like a storyboard (SITE2 · 10-02). */
21
+ type Section = { id: string; title: string; purpose: string; change: string; copy?: Text[]; notes?: string };
22
+ type Site = { domain: string; role: string; preview?: string; branch?: string; nav: Array<{ label: string; href: string }>; pages: Array<{ path: string; sections: Section[] }> };
23
+ export type Storyboard = {
24
+ id: string; kind: string; title: string; version: number; status: string; approved?: { by: string; at: string; note?: string } | null;
25
+ supersedes?: string; owner: string; directive?: string; format: { aspects: string[]; duration_s: number; fps?: number; loop?: boolean; surface?: string };
26
+ scqa?: { s?: string; c?: string; q?: string; a?: string }; principles?: string[]; layers?: Record<string, string>; shots: Shot[]; sites?: Site[];
27
+ references?: string[]; open_questions?: string[];
28
+ };
29
+
30
+ const KINDS = ['teaser', 'site-hero', 'event-demo', 'persona-scene', 'explainer', 'ad', 'site'];
31
+ const CHANGES = ['keep', 'move', 'new', 'replace', 'remove'];
32
+ const STAGES = ['hook', 'buildup', 'climax', 'transition', 'end', 'loop'];
33
+ const SOURCES = ['real-ui', 'real-cli', 'real-output', 'copy', 'example'];
34
+
35
+ export function lint(sb: Storyboard, file = ''): { errors: string[]; warnings: string[] } {
36
+ const errors: string[] = [], warnings: string[] = [];
37
+ const need = (cond: unknown, msg: string) => { if (!cond) errors.push(msg); };
38
+ need(/^[a-z0-9][a-z0-9-]{2,80}$/.test(sb.id ?? ''), 'id: 소문자·숫자·하이픈 3~80자');
39
+ need(KINDS.includes(sb.kind), `kind: ${KINDS.join('|')}`);
40
+ need(Number.isInteger(sb.version) && sb.version >= 1, 'version: 1 이상 정수');
41
+ need(['draft', 'approved', 'superseded'].includes(sb.status), 'status: draft|approved|superseded');
42
+ need(sb.owner, 'owner 필요');
43
+ if (sb.status === 'approved') need(sb.approved?.by && sb.approved?.at, '승인본은 approved.by·at 필요');
44
+ if (file && sb.kind && !file.includes(`/${sb.kind}/`)) errors.push(`파일 위치가 kind(${sb.kind}) 폴더가 아니다`);
45
+ if (sb.kind === 'site') return lintSite(sb, errors, warnings);
46
+ need(sb.format?.aspects?.length && sb.format.duration_s > 0, 'format.aspects ⊕ duration_s 필요');
47
+ if (!Array.isArray(sb.shots) || !sb.shots.length) { errors.push('shots 가 비었다'); return { errors, warnings }; }
48
+ const ids = new Set<string>();
49
+ sb.shots.forEach((s, i) => {
50
+ const at = `shots[${i}](${s.id ?? '?'})`;
51
+ need(s.id && !ids.has(s.id), `${at}: id 없음·중복`); ids.add(s.id);
52
+ need(Array.isArray(s.t) && s.t.length === 2 && s.t[0] < s.t[1], `${at}: t = [시작, 끝] 이고 시작 < 끝`);
53
+ need(STAGES.includes(s.stage), `${at}: stage ${STAGES.join('|')}`);
54
+ need(s.purpose, `${at}: purpose(이 비트의 한 가지) 필요`);
55
+ need(['real', 'staged', 'composite'].includes(s.real_or_staged), `${at}: real_or_staged 필요`);
56
+ need(['planned', 'shot', 'approved'].includes(s.state), `${at}: state planned|shot|approved`);
57
+ if (s.transition_in && !s.transition_in.intent) errors.push(`${at}: 전환에는 의도 한 줄(«그냥 컷»은 설계가 아니다)`);
58
+ for (const txt of s.on_screen_text ?? []) {
59
+ need(SOURCES.includes(txt.source), `${at}: 화면 문구 «${txt.text}» 의 source 가 ${SOURCES.join('|')} 가 아니다`);
60
+ if (txt.source?.startsWith('real-') && !txt.ref) warnings.push(`${at}: 실제 문면 «${txt.text}» 에 ref(출처) 가 없다`);
61
+ if (/\d/.test(txt.text) && txt.source === 'copy') warnings.push(`${at}: 마케팅 문구에 숫자 «${txt.text}» — 실측 근거나 «example» 로`);
62
+ }
63
+ if (s.generation?.tool === 'higgsfield' && !s.generation.prompt) warnings.push(`${at}: 힉스필드 샷에 prompt 가 없다`);
64
+ if (s.real_or_staged !== 'real' && !/연출/.test(JSON.stringify(sb))) warnings.push(`${at}: 연출 샷이 있는데 문서 어디에도 «연출 장면» 표기가 없다`);
65
+ });
66
+ const end = Math.max(...sb.shots.map((s) => s.t?.[1] ?? 0));
67
+ if (sb.format?.duration_s && Math.abs(end - sb.format.duration_s) > 0.5) warnings.push(`마지막 샷 끝(${end}s) ≠ format.duration_s(${sb.format.duration_s}s)`);
68
+ return { errors, warnings };
69
+ }
70
+
71
+ /** A site plan has pages instead of shots: every section says why it is there and what changes; copy names its source. */
72
+ function lintSite(sb: Storyboard, errors: string[], warnings: string[]): { errors: string[]; warnings: string[] } {
73
+ if (!Array.isArray(sb.sites) || !sb.sites.length) { errors.push('site 종류는 sites 가 필요하다'); return { errors, warnings }; }
74
+ for (const site of sb.sites) {
75
+ const at = `sites(${site.domain ?? '?'})`;
76
+ if (!site.domain || !site.role) errors.push(`${at}: domain ⊕ role(이 사이트가 맡는 것) 필요`);
77
+ if (!site.nav?.length) errors.push(`${at}: nav(메뉴) 필요`);
78
+ if (!site.pages?.length) errors.push(`${at}: pages 필요`);
79
+ if (!site.preview) warnings.push(`${at}: preview(미리보기 주소) 없음`);
80
+ const ids = new Set<string>();
81
+ for (const page of site.pages ?? []) for (const sec of page.sections ?? []) {
82
+ const where = `${at}${page.path} #${sec.id ?? '?'}`;
83
+ if (!sec.id || ids.has(`${page.path}#${sec.id}`)) errors.push(`${where}: id 없음·중복`);
84
+ ids.add(`${page.path}#${sec.id}`);
85
+ if (!sec.purpose) errors.push(`${where}: purpose(이 칸이 하는 한 가지) 필요`);
86
+ if (!CHANGES.includes(sec.change)) errors.push(`${where}: change ${CHANGES.join('|')}`);
87
+ for (const txt of sec.copy ?? []) {
88
+ if (!SOURCES.includes(txt.source)) errors.push(`${where}: 문구 «${txt.text}» 의 source 가 ${SOURCES.join('|')} 가 아니다`);
89
+ if (/\d/.test(txt.text) && txt.source === 'copy' && !txt.ref) warnings.push(`${where}: 마케팅 문구에 숫자 «${txt.text}» — ref(실측 근거) 를 단다`);
90
+ }
91
+ }
92
+ }
93
+ return { errors, warnings };
94
+ }
95
+
96
+ function renderSites(sb: Storyboard): string[] {
97
+ return (sb.sites ?? []).flatMap((site) => [
98
+ `## ${site.domain} — ${site.role}`, '',
99
+ ...(site.preview ? [`- 미리보기: ${site.preview}${site.branch ? ` (가지 \`${site.branch}\`)` : ''}`] : []),
100
+ `- 메뉴: ${site.nav.map((n) => `${n.label}(${n.href})`).join(' · ')}`, '',
101
+ ...site.pages.flatMap((page) => [
102
+ `### ${page.path}`, '', '| # | 칸 | 제목 | 목적 | 바뀜 | 문구(출처) | 메모 |', '|---|---|---|---|---|---|---|',
103
+ ...page.sections.map((sec, i) => `| ${i + 1} | ${md(sec.id)} | ${md(sec.title)} | ${md(sec.purpose)} | ${sec.change} | ${md((sec.copy ?? []).map((x) => `«${x.text}»(${x.source}${x.ref ? ` · ${x.ref}` : ''})`).join(' / '))} | ${md(sec.notes)} |`),
104
+ '',
105
+ ]),
106
+ ]);
107
+ }
108
+
109
+ const md = (v: unknown) => String(v ?? '').replace(/\|/g, '\\|').replace(/\n+/g, ' ');
110
+ export function render(sb: Storyboard, file: string): string {
111
+ const head = [
112
+ `# STORYBOARD — ${sb.title} (v${sb.version} · ${sb.status}${sb.approved ? ` · 승인 ${sb.approved.by} ${sb.approved.at}` : ''})`,
113
+ '',
114
+ `> ⛔ 자동 생성 — 손으로 고치지 않는다. 정본 = \`${file}\` · 다시 뽑기 = \`bun scripts/storyboard/storyboard.ts render ${file}\`.`,
115
+ `> 종류 ${sb.kind} · 소유 ${sb.owner}${sb.format ? ` · ${sb.format.aspects.join(' · ')} · ${sb.format.duration_s}초${sb.format.loop ? ' · 반복' : ''}${sb.format.surface ? ` · ${sb.format.surface}` : ''}` : ''}${sb.supersedes ? ` · 앞 판 ${sb.supersedes}` : ''}`,
116
+ ...(sb.directive ? [`> 지시: ${sb.directive}`] : []),
117
+ '',
118
+ ];
119
+ const scqa = sb.scqa ? ['## SCQA', ...(['s', 'c', 'q', 'a'] as const).filter((k) => sb.scqa?.[k]).map((k) => `- **${k.toUpperCase()}** — ${sb.scqa![k]}`), ''] : [];
120
+ const pr = sb.principles?.length ? ['## 원칙', ...sb.principles.map((p, i) => `${i + 1}. ${p}`), ''] : [];
121
+ const layers = sb.layers ? ['## 층', ...Object.entries(sb.layers).map(([k, v]) => `- **${k}** — ${v}`), ''] : [];
122
+ const table = sb.kind === 'site' ? renderSites(sb) : [
123
+ '## 샷 구조표',
124
+ '| # | 초 | 단계 | 층·인물 | 목적 | 화면(피사체·행동) | 카메라 | 빛·색 | 전환(의도) | 화면 문구(출처) | 소리 | 만들기 | 실물/연출 · 상태 |',
125
+ '|---|---|---|---|---|---|---|---|---|---|---|---|---|',
126
+ ...sb.shots.map((s) => `| ${md(s.id)} | ${s.t[0]}–${s.t[1]} | ${s.stage} | ${md([s.layer, s.persona].filter(Boolean).join(' · '))} | ${md(s.purpose)} | ${md([s.subject, s.action].filter(Boolean).join(' — '))} | ${md(s.camera ? [s.camera.move, s.camera.lens_mm ? `${s.camera.lens_mm}mm` : '', s.camera.angle, s.camera.framing].filter(Boolean).join(' · ') : '')} | ${md([s.lighting, s.color].filter(Boolean).join(' · '))} | ${md(s.transition_in ? `${s.transition_in.type} — ${s.transition_in.intent}` : '')} | ${md((s.on_screen_text ?? []).map((x) => `«${x.text}»(${x.source}${x.ref ? ` · ${x.ref}` : ''})`).join(' / '))} | ${md(s.audio ? [s.audio.vo && `VO ${s.audio.vo}`, s.audio.sfx && `SFX ${s.audio.sfx}`, s.audio.bgm && `BGM ${s.audio.bgm}`].filter(Boolean).join(' · ') : '')} | ${md(s.generation ? [s.generation.tool, s.generation.model].filter(Boolean).join(' · ') : '')} | ${s.real_or_staged} · ${s.state} |`),
127
+ '',
128
+ ];
129
+ const shots = sb.shots ?? [];
130
+ const prompts = shots.filter((s) => s.generation?.prompt);
131
+ const pblock = prompts.length ? ['## 생성 프롬프트', ...prompts.flatMap((s) => [`### ${s.id} · ${s.generation!.tool}${s.generation!.model ? ` · ${s.generation!.model}` : ''}`, '```', s.generation!.prompt!.trim(), '```', ...(s.generation!.asset ? [`결과: \`${s.generation!.asset}\``] : []), ''])] : [];
132
+ const notes = shots.filter((s) => s.notes).map((s) => `- ${s.id}: ${s.notes}`);
133
+ const tail = [
134
+ ...(notes.length ? ['## 샷 메모', ...notes, ''] : []),
135
+ ...(sb.open_questions?.length ? ['## 열린 물음', ...sb.open_questions.map((q) => `- ${q}`), ''] : []),
136
+ ...(sb.references?.length ? ['## 참고', ...sb.references.map((r) => `- ${r}`), ''] : []),
137
+ ];
138
+ return [...head, ...scqa, ...pr, ...layers, ...table, ...pblock, ...tail].join('\n');
139
+ }
140
+
141
+ function files(args: string[]): string[] {
142
+ if (args.length) return args.map((a) => resolve(a));
143
+ const out: string[] = [];
144
+ const walk = (d: string) => { for (const f of readdirSync(d)) { const p = join(d, f); if (statSync(p).isDirectory()) walk(p); else if (/\.ya?ml$/.test(f) && !f.startsWith('_')) out.push(p); } };
145
+ walk(DIR);
146
+ return out.sort();
147
+ }
148
+
149
+ if (import.meta.main) {
150
+ const [cmd, ...rest] = process.argv.slice(2);
151
+ if (cmd !== 'lint' && cmd !== 'render') { console.error('usage: storyboard.ts lint|render [files…]'); process.exit(2); }
152
+ let bad = 0;
153
+ for (const f of files(rest)) {
154
+ const rel = relative(ROOT, f);
155
+ const sb = parse(readFileSync(f, 'utf8')) as Storyboard;
156
+ const { errors, warnings } = lint(sb, rel);
157
+ for (const e of errors) console.log(`✗ ${rel}: ${e}`);
158
+ for (const w of warnings) console.log(`⚠ ${rel}: ${w}`);
159
+ if (errors.length) { bad++; continue; }
160
+ if (cmd === 'render') {
161
+ const out = join(ROOT, 'docs', 'marketing', `STORYBOARD-${sb.id}-v${sb.version}.md`);
162
+ writeFileSync(out, render(sb, rel) + '\n');
163
+ console.log(`✓ ${rel} → ${relative(ROOT, out)}`);
164
+ } else console.log(`✓ ${rel} · v${sb.version} · ${sb.status} · ${sb.kind === 'site' ? `사이트 ${sb.sites?.length ?? 0}` : `샷 ${sb.shots.length}`}`);
165
+ }
166
+ process.exit(bad ? 1 : 0);
167
+ }
@@ -0,0 +1,89 @@
1
+ // TD2 — a daily look at the test suite, one cost-weighted slice at a time (shadow: it measures and drafts, it
2
+ // never deletes or moves a test). Design: 내부 문서 `TD1-v2-gate-content-review-2026-10-01`.
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ export const DEFAULT_COST_SECS = 10;
7
+ export const SLOW_SECS = 60;
8
+ export const HEAVY_MB = 2048;
9
+
10
+ export interface Measurement { file: string; secs: number; rssMb: number | null; rc: number | null; pass: number | null; fail: number | null; reason?: string }
11
+ export type Verdict = 'keep' | 'failing' | 'review';
12
+ export interface Judged extends Measurement { caught90: number; flags: string[]; verdict: Verdict }
13
+ export interface LedgerLine { at: string; range: string; start: number; end: number; next: number; total: number; commit: string; budgetSecs: number; results: Judged[] }
14
+
15
+ /** `file\tsecs\t…` (TD1 whole-gate TSV) → seconds per file. */
16
+ export function costTable(tsv: string): Map<string, number> {
17
+ const costs = new Map<string, number>();
18
+ for (const line of tsv.split('\n').slice(1)) {
19
+ const [file, secs] = line.split('\t');
20
+ const n = Number(secs);
21
+ if (file && Number.isFinite(n) && n > 0) costs.set(file, n);
22
+ }
23
+ return costs;
24
+ }
25
+
26
+ /** Next slice from the cursor until the known cost reaches the budget (at least one file; wraps at the end). */
27
+ export function pickRange(files: readonly string[], start: number, costs: ReadonlyMap<string, number>, budgetSecs: number): { start: number; end: number; next: number; files: string[]; estimatedSecs: number } {
28
+ if (files.length === 0) return { start: 0, end: -1, next: 0, files: [], estimatedSecs: 0 };
29
+ const from = ((start % files.length) + files.length) % files.length;
30
+ const picked: string[] = [];
31
+ let spent = 0;
32
+ let i = from;
33
+ while (picked.length < files.length) {
34
+ const file = files[i]!;
35
+ const cost = costs.get(file) ?? DEFAULT_COST_SECS;
36
+ if (picked.length > 0 && spent + cost > budgetSecs) break;
37
+ picked.push(file);
38
+ spent += cost;
39
+ i = (i + 1) % files.length;
40
+ }
41
+ return { start: from, end: (from + picked.length - 1) % files.length, next: i, files: picked, estimatedSecs: spent };
42
+ }
43
+
44
+ /** Mechanical verdict: failing, or slow/heavy with nothing caught in 90 days → review (a draft card, never an action). */
45
+ export function judge(m: Measurement, caught90: number): Judged {
46
+ const flags: string[] = [];
47
+ if (m.secs >= SLOW_SECS) flags.push('slow');
48
+ if (m.rssMb !== null && m.rssMb >= HEAVY_MB) flags.push('heavy');
49
+ const verdict: Verdict = m.rc !== 0 ? 'failing' : flags.length > 0 && caught90 === 0 ? 'review' : 'keep';
50
+ return { ...m, caught90, flags, verdict };
51
+ }
52
+
53
+ export function ledgerPath(root: string): string { return join(root, 'test-diet', 'ledger.jsonl'); }
54
+ export function nightlyAuditLedgerPath(root: string): string { return join(root, 'test-diet', 'nightly-audit.jsonl'); }
55
+
56
+ export function lastLedgerLine(root: string): LedgerLine | null {
57
+ const path = ledgerPath(root);
58
+ if (!existsSync(path)) return null;
59
+ const lines = readFileSync(path, 'utf8').trim().split('\n').filter(Boolean);
60
+ try { return lines.length ? JSON.parse(lines.at(-1)!) as LedgerLine : null; } catch { return null; }
61
+ }
62
+
63
+ export function appendLedger(root: string, line: LedgerLine): void {
64
+ mkdirSync(join(root, 'test-diet'), { recursive: true });
65
+ appendFileSync(ledgerPath(root), `${JSON.stringify(line)}\n`);
66
+ }
67
+
68
+ /** Draft approval card — written as a file for a person to read; nothing is acted on. */
69
+ export function writeCardDraft(root: string, line: LedgerLine): string | null {
70
+ const items = line.results.filter((r) => r.verdict !== 'keep');
71
+ if (items.length === 0) return null;
72
+ const dir = join(root, 'test-diet', 'cards');
73
+ mkdirSync(dir, { recursive: true });
74
+ const path = join(dir, `${line.at.slice(0, 10)}-${line.range.replace(/[^0-9a-z~#-]/gi, '')}.md`);
75
+ const rows = items.map((r) => `| \`${r.file}\` | ${r.verdict} | ${r.secs}s | ${r.rssMb ?? '?'} MB | ${r.caught90} | ${r.flags.join(', ') || '—'} | ${(r.reason ?? '').replace(/\|/g, '/')} |`);
76
+ writeFileSync(path, [
77
+ `# 시험 리뷰 초안 — ${line.range} (${line.at.slice(0, 10)})`,
78
+ '',
79
+ '> 초안이다. 아무것도 지우거나 옮기지 않았다. 사람이 읽고 정한다.',
80
+ '',
81
+ '| 파일 | 판정 | 단독 시간 | 피크 메모리 | 90일 fix 커밋 | 표지 | 실패 사유(첫 줄) |',
82
+ '|---|---|---|---|---|---|---|',
83
+ ...rows,
84
+ '',
85
+ `원장: ${line.results.length}개 중 ${items.length}개 · 측정 판 ${line.commit}`,
86
+ '',
87
+ ].join('\n'));
88
+ return path;
89
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bun
2
+ // TD2 graph nodes: pick → measure → record. Shadow only — measures alone on a remote host and drafts a card.
3
+ // bun scripts/test-diet/node.ts pick|measure|audit|record (test-diet or nightly-audit graph)
4
+ import { spawnSync } from 'node:child_process';
5
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { debug } from '../../src/debug/log.js';
8
+ import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
9
+ import { GATE_NIGHTLY_AUDITS } from '../release-loop/gate-node.js';
10
+ import { appendLedger, costTable, judge, lastLedgerLine, nightlyAuditLedgerPath, pickRange, writeCardDraft, type Measurement } from './lib.js';
11
+
12
+ type Context = { graphId?: string; input: Record<string, unknown>; outputs: Record<string, Record<string, unknown> | null> };
13
+ function context(): Context {
14
+ const at = process.env.ELANOUS_GRAPH_CONTEXT;
15
+ if (!at) throw new Error('ELANOUS_GRAPH_CONTEXT required');
16
+ const raw = JSON.parse(at.trimStart().startsWith('{') ? at : readFileSync(at, 'utf8')) as Partial<Context>;
17
+ return { graphId: raw.graphId, input: raw.input ?? {}, outputs: raw.outputs ?? {} };
18
+ }
19
+ function emit(result: Record<string, unknown> & { outcome: 'ok' | 'fail' | 'error'; summary: string }): number {
20
+ console.log(JSON.stringify({ verdict: result.outcome === 'ok' ? 'pass' : 'fail', ...result }));
21
+ return result.outcome === 'ok' ? 0 : result.outcome === 'fail' ? 1 : 2;
22
+ }
23
+ const git = (args: string[]) => spawnSync('git', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).stdout ?? '';
24
+
25
+ function pick(ctx: Context): number {
26
+ const budgetSecs = typeof ctx.input.budgetSecs === 'number' && ctx.input.budgetSecs > 0 ? ctx.input.budgetSecs : 1800;
27
+ const files = git(['ls-files', '*.test.ts']).split('\n').filter(Boolean).sort();
28
+ const costs = costTable(readFileSync(join(process.cwd(), 'docs/measurements/td1-whole-gate-mechanical-2026-10-01.tsv'), 'utf8'));
29
+ const last = lastLedgerLine(effectiveInstanceRoot());
30
+ for (const r of last?.results ?? []) costs.set(r.file, r.secs);
31
+ // `start` re-looks a slice on purpose; otherwise continue from the ledger cursor.
32
+ const startAt = typeof ctx.input.start === 'number' && ctx.input.start >= 0 ? ctx.input.start : last?.next ?? 0;
33
+ const range = pickRange(files, startAt, costs, budgetSecs);
34
+ return emit({ outcome: 'ok', summary: `test-diet pick #${range.start}~#${range.end} of ${files.length} (≈${Math.round(range.estimatedSecs)}s)`, ...range, total: files.length, budgetSecs });
35
+ }
36
+
37
+ /** One remote run: shallow clone of the mirror, one install, each file alone with its peak memory. */
38
+ function measure(ctx: Context, audit = false): number {
39
+ const picked = audit ? [...GATE_NIGHTLY_AUDITS] : ctx.outputs.pick?.files;
40
+ if (!Array.isArray(picked) || picked.length === 0) return emit({ outcome: 'fail', summary: 'nothing picked' });
41
+ const host = typeof ctx.input.remote === 'string' ? ctx.input.remote : 'node-b';
42
+ const mirror = typeof ctx.input.mirror === 'string' ? ctx.input.mirror : '~/mirror/elanous-agent.git';
43
+ const script = [
44
+ 'set -u', 'export PATH="$HOME/.bun/bin:$PATH"',
45
+ 'T=$(mktemp -d)', 'trap \'rm -rf "$T"\' EXIT',
46
+ `git clone -q --depth 1 "file://$(eval echo ${mirror})" "$T/repo" || exit 3`,
47
+ // Same installs as the gate: root and apps/pwa (PWA tests import its own dependencies).
48
+ 'cd "$T/repo" && bun install --frozen-lockfile >/dev/null 2>&1 || exit 4',
49
+ '(cd apps/pwa && bun install --frozen-lockfile >/dev/null 2>&1) || echo "WARN pwa-install-failed"',
50
+ 'echo "COMMIT $(git rev-parse HEAD)"',
51
+ 'TO=$(command -v timeout || command -v gtimeout || true)',
52
+ 'while IFS= read -r f; do',
53
+ ' [ -n "$f" ] || continue',
54
+ ' s=$(date +%s)',
55
+ ' if [ -n "$TO" ]; then /usr/bin/time -l "$TO" -k 10 300 bun run scripts/test-deterministic.ts "./$f" > "$T/out" 2> "$T/err"; else /usr/bin/time -l bun run scripts/test-deterministic.ts "./$f" > "$T/out" 2> "$T/err"; fi',
56
+ ' rc=$?; e=$(( $(date +%s) - s ))',
57
+ ' rss=$(awk \'/maximum resident set size/ {printf "%d", $1/1048576}\' "$T/err")',
58
+ // bun test prints its summary on stderr — read both streams.
59
+ ' pass=$(cat "$T/out" "$T/err" | grep -E -o "^ *[0-9]+ pass" | grep -E -o "[0-9]+" | tail -1); fail=$(cat "$T/out" "$T/err" | grep -E -o "^ *[0-9]+ fail" | grep -E -o "[0-9]+" | tail -1)',
60
+ // A failing file names its first error, so an environment gap (e.g. no Java on the host) is not read as a test defect.
61
+ ' why=""; [ "$rc" -ne 0 ] && why=$(grep -v -E "^\\s*$|^bun test|^ *[0-9]+ \\||^error: *$" "$T/err" | grep -m1 -E "Unable to|Cannot find|not found|ENOENT|EACCES|rror" | tr "\\t" " " | cut -c1-160)',
62
+ ' printf "M\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n" "$f" "$e" "${rss:-}" "$rc" "${pass:-}" "${fail:-}" "$why"',
63
+ "done <<'TEST_DIET_FILES'", ...picked, 'TEST_DIET_FILES',
64
+ ].join('\n');
65
+ const run = spawnSync('ssh', ['-o', 'BatchMode=yes', host, 'bash', '-s'], { input: `${script}\n`, encoding: 'utf8', timeout: 4 * 3600_000, maxBuffer: 16 * 1024 * 1024 });
66
+ const remoteError = run.status !== 0 ? `remote measure failed (rc=${run.status ?? 'no exit'}): ${String(run.error ?? run.stderr ?? '').trim().slice(0, 200)}` : undefined;
67
+ if (!audit && remoteError && !String(run.stdout).includes('\nM\t')) return emit({ outcome: 'error', summary: remoteError });
68
+ const commit = /^COMMIT (\w+)/m.exec(String(run.stdout))?.[1] ?? '?';
69
+ const measurements: Measurement[] = String(run.stdout).split('\n').filter((l) => l.startsWith('M\t')).map((l) => {
70
+ const [, file, secs, rss, rc, pass, fail, why] = l.split('\t');
71
+ const num = (v: string | undefined) => (v === undefined || v === '' ? null : Number(v));
72
+ return { file: file!, secs: Number(secs), rssMb: num(rss), rc: num(rc), pass: num(pass), fail: num(fail), ...(why ? { reason: why } : {}) };
73
+ });
74
+ const complete = measurements.length === picked.length && picked.every((file) => measurements.some((m) => m.file === file));
75
+ const summary = remoteError ?? `measured ${measurements.length}/${picked.length} on ${host} at ${commit.slice(0, 9)}`;
76
+ return emit({ outcome: audit ? complete && !remoteError ? 'ok' : 'error' : measurements.length ? 'ok' : 'error',
77
+ summary, host, commit, measurements, ...(remoteError ? { remoteError } : {}) });
78
+ }
79
+
80
+ function recordNightlyAudit(ctx: Context): number {
81
+ const m = ctx.outputs.audit;
82
+ const raw = Array.isArray(m?.measurements) ? m.measurements : [];
83
+ const measured = new Map<string, Measurement>();
84
+ for (const item of raw) {
85
+ if (!item || typeof item !== 'object' || !('file' in item) || typeof item.file !== 'string' || !GATE_NIGHTLY_AUDITS.includes(item.file)) continue;
86
+ const x = item as Partial<Measurement>;
87
+ if (measured.has(item.file) || typeof x.secs !== 'number' || !Number.isFinite(x.secs)
88
+ || (x.rssMb !== null && (typeof x.rssMb !== 'number' || !Number.isFinite(x.rssMb)))
89
+ || (x.rc !== null && (typeof x.rc !== 'number' || !Number.isInteger(x.rc)))
90
+ || (x.pass !== null && (typeof x.pass !== 'number' || !Number.isInteger(x.pass)))
91
+ || (x.fail !== null && (typeof x.fail !== 'number' || !Number.isInteger(x.fail)))) continue;
92
+ measured.set(item.file, x as Measurement);
93
+ }
94
+ const error = typeof m?.remoteError === 'string' ? m.remoteError
95
+ : m?.outcome === 'error' || m?.outcome === 'fail' || !m
96
+ ? typeof m?.summary === 'string' ? m.summary : 'audit execution produced no result' : undefined;
97
+ const results = GATE_NIGHTLY_AUDITS.map((file, index) => {
98
+ const found = measured.get(file);
99
+ const measurement: Measurement = found ?? { file, secs: 0, rssMb: null, rc: null, pass: null, fail: null,
100
+ reason: `not measured: ${typeof m?.summary === 'string' ? m.summary : error ?? 'audit result missing'}` };
101
+ // Even a complete set of file outputs cannot prove a clean audit when the remote process failed afterward.
102
+ return judge(error && measured.size === GATE_NIGHTLY_AUDITS.length && index === 0 && found
103
+ ? { ...found, rc: found.rc === 0 ? null : found.rc, reason: `${error}${found.reason ? `; ${found.reason}` : ''}` } : measurement, 1);
104
+ });
105
+ const line = { at: new Date().toISOString(), range: 'nightly-audit', start: 0, end: results.length - 1,
106
+ next: 0, total: results.length, commit: String(m?.commit ?? '?'), budgetSecs: 0, ...(error ? { error } : {}), results };
107
+ const root = effectiveInstanceRoot();
108
+ const path = nightlyAuditLedgerPath(root);
109
+ mkdirSync(join(root, 'test-diet'), { recursive: true });
110
+ appendFileSync(path, `${JSON.stringify(line)}\n`);
111
+ const card = writeCardDraft(root, line);
112
+ const failing = results.filter((r) => r.verdict === 'failing').length;
113
+ debug.log('test-diet', 'nightly-audit-recorded', { files: results.length, failing, card: card !== null });
114
+ return emit({ outcome: failing ? 'fail' : 'ok', summary: `nightly audit ${results.length} · failing ${failing}${card ? ' · card draft' : ''}`, failing, ledger: path, ...(card ? { card } : {}) });
115
+ }
116
+
117
+ function record(ctx: Context): number {
118
+ const p = ctx.outputs.pick;
119
+ const m = ctx.outputs.measure;
120
+ const measurements = (m?.measurements ?? []) as Measurement[];
121
+ if (!p || measurements.length === 0) return emit({ outcome: 'fail', summary: 'nothing measured' });
122
+ const results = measurements.map((x) => judge(x, Number(git(['log', '--since=90.days', '--format=%s', '--', x.file]).split('\n').filter((s) => /^fix\b/i.test(s)).length)));
123
+ const line = {
124
+ at: new Date().toISOString(), range: `#${p.start}~#${p.end}`, start: Number(p.start), end: Number(p.end), next: Number(p.next), total: Number(p.total),
125
+ commit: String(m?.commit ?? '?'), budgetSecs: Number(p.budgetSecs), results,
126
+ };
127
+ const root = effectiveInstanceRoot();
128
+ appendLedger(root, line);
129
+ const card = writeCardDraft(root, line);
130
+ const counts = { keep: results.filter((r) => r.verdict === 'keep').length, review: results.filter((r) => r.verdict === 'review').length, failing: results.filter((r) => r.verdict === 'failing').length };
131
+ debug.log('test-diet', 'recorded', { range: line.range, ...counts, card: card !== null });
132
+ return emit({ outcome: 'ok', summary: `test-diet ${line.range} · keep ${counts.keep} · review ${counts.review} · failing ${counts.failing}${card ? ' · card draft' : ''}`, range: line.range, ...counts, ...(card ? { card } : {}) });
133
+ }
134
+
135
+ if (import.meta.main) {
136
+ const step = process.argv[2];
137
+ try {
138
+ const ctx = context();
139
+ process.exitCode = step === 'pick' ? pick(ctx) : step === 'measure' ? measure(ctx) : step === 'audit' ? measure(ctx, true)
140
+ : step === 'record' ? (ctx.graphId === 'nightly-audit' || Object.hasOwn(ctx.outputs, 'audit') ? recordNightlyAudit(ctx) : record(ctx)) : emit({ outcome: 'error', summary: `unknown step: ${step}` });
141
+ } catch (error) {
142
+ process.exitCode = emit({ outcome: 'error', summary: error instanceof Error ? error.message : String(error) });
143
+ }
144
+ }
@@ -0,0 +1,30 @@
1
+ // EN10a — ACP auth methods: a client that runs terminals gets a `terminal` method (it re-launches `elanous --acp-server --login`);
2
+ // an older client gets an agent method carrying Zed's `terminal-auth` meta, so «sign-in needed → run elanous login in a terminal» shows either way.
3
+ import type { AuthMethod, ClientCapabilities } from '@agentclientprotocol/sdk';
4
+
5
+ export const ACP_LOGIN_METHOD_ID = 'elanous-login';
6
+ /** Appended to the configured agent invocation (`elanous --acp-server`) by a terminal-auth client. */
7
+ export const ACP_LOGIN_ARG = '--login';
8
+ /** What the login step runs (the ChatGPT/Codex subscription sign-in). */
9
+ export const ACP_LOGIN_SUBCOMMAND = ['login', 'openai-codex'] as const;
10
+
11
+ export interface AcpLaunch { command: string; args: string[] }
12
+
13
+ /** How this process was started (bun + script, or a compiled binary) so a client can start the same thing. */
14
+ export function currentLaunch(argv: readonly string[] = process.argv, execPath = process.execPath): AcpLaunch {
15
+ const script = argv[1];
16
+ return script && /\.(?:m?js|ts)$/.test(script) ? { command: execPath, args: [script] } : { command: execPath, args: [] };
17
+ }
18
+
19
+ export function acpAuthMethods(client: ClientCapabilities | null | undefined, launch: AcpLaunch = currentLaunch()): AuthMethod[] {
20
+ const name = 'Sign in to elanous';
21
+ if (client?.auth?.terminal === true) {
22
+ return [{ type: 'terminal', id: ACP_LOGIN_METHOD_ID, name, description: 'Opens a terminal to sign in with your ChatGPT (Codex) subscription', args: [ACP_LOGIN_ARG] }];
23
+ }
24
+ return [{
25
+ id: ACP_LOGIN_METHOD_ID,
26
+ name,
27
+ description: `Run \`elanous ${ACP_LOGIN_SUBCOMMAND.join(' ')}\` in a terminal, then try again`,
28
+ _meta: { 'terminal-auth': { command: launch.command, args: [...launch.args, ...ACP_LOGIN_SUBCOMMAND], label: 'elanous login' } },
29
+ }];
30
+ }
package/src/acp/server.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  //
27
27
  // Related: src/acp/client.ts (elanous-as-client path — pre-MT5 work).
28
28
 
29
+ import { ACP_LOGIN_METHOD_ID, acpAuthMethods } from './auth-methods.js';
29
30
  import { AgentSideConnection, ndJsonStream, RequestError } from '@agentclientprotocol/sdk';
30
31
  import type {
31
32
  InitializeRequest, InitializeResponse,
@@ -1415,14 +1416,13 @@ function wireAcpConnection(
1415
1416
  agentCapabilities: opts.agentBrand !== undefined && opts.agentModel !== undefined
1416
1417
  ? buildAgentDeclaration({ brand: opts.agentBrand, model: opts.agentModel })
1417
1418
  : buildAgentDeclaration(),
1418
- authMethods: [],
1419
+ authMethods: acpAuthMethods(req.clientCapabilities),
1419
1420
  };
1420
1421
  },
1421
1422
 
1422
- async authenticate(_req: AuthenticateRequest): Promise<AuthenticateResponse> {
1423
- // No auth flow — we advertise authMethods: [] so the client
1424
- // shouldn't call this, but the Agent interface requires the
1425
- // method to be present.
1423
+ async authenticate(req: AuthenticateRequest): Promise<AuthenticateResponse> {
1424
+ // EN10a — sign-in happens in a terminal (`--acp-server --login`); this only acknowledges the method so the client retries.
1425
+ debug.log('acp.session', 'authenticate', { methodId: req.methodId === ACP_LOGIN_METHOD_ID ? req.methodId : 'other' });
1426
1426
  return {};
1427
1427
  },
1428
1428
 
@@ -0,0 +1,19 @@
1
+ import { openSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { dlopen, FFIType } from 'bun:ffi';
4
+ import { claimSetupLinkToken } from './setup-link-tokens.js';
5
+
6
+ const [mode, dir, token, now] = process.argv.slice(2);
7
+ if (mode === 'claim') {
8
+ process.stdout.write(JSON.stringify(claimSetupLinkToken(token, { dir, now: Number(now) })));
9
+ } else if (mode === 'hold') {
10
+ const fd = openSync(join(dir, 'setup-link-tokens.json.lock'), 'r+');
11
+ const libc = dlopen(process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : 'libc.so.6', {
12
+ flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
13
+ });
14
+ if (libc.symbols.flock(fd, 2) !== 0) throw new Error('failed to acquire lock');
15
+ process.stdout.write('locked\n');
16
+ await new Promise<void>(() => { setInterval(() => {}, 1000); });
17
+ } else {
18
+ throw new Error(`unknown fixture mode: ${mode}`);
19
+ }