elanous 0.2.8 → 0.2.10

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 (385) 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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -1,167 +1,11 @@
1
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
- }
2
+ // 스토리보드 정본 검사 ⊕ MD 생성. 기존 bun scripts/storyboard/storyboard.ts lint|render 지원.
3
+ import { runStoryboard } from '../../src/cli/storyboard-cli.js';
4
+ export { lint, render } from '../../src/storyboard/storyboard.js';
5
+ export type { Storyboard } from '../../src/storyboard/storyboard.js';
148
6
 
149
7
  if (import.meta.main) {
150
8
  const [cmd, ...rest] = process.argv.slice(2);
151
9
  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);
10
+ process.exitCode = runStoryboard(cmd, rest);
167
11
  }
@@ -9,7 +9,9 @@ export const HEAVY_MB = 2048;
9
9
 
10
10
  export interface Measurement { file: string; secs: number; rssMb: number | null; rc: number | null; pass: number | null; fail: number | null; reason?: string }
11
11
  export type Verdict = 'keep' | 'failing' | 'review';
12
- export interface Judged extends Measurement { caught90: number; flags: string[]; verdict: Verdict }
12
+ export type Proposal = 'delete' | 'shrink' | 'rewrite-cheap' | 'move-out-of-gate' | 'investigate';
13
+ export interface Td1Disposition { disposition: string; alternative: string; guards: string; why_slow: string }
14
+ export interface Judged extends Measurement { caught90: number; flags: string[]; verdict: Verdict; proposal?: Proposal | null; basis?: string | null }
13
15
  export interface LedgerLine { at: string; range: string; start: number; end: number; next: number; total: number; commit: string; budgetSecs: number; results: Judged[] }
14
16
 
15
17
  /** `file\tsecs\t…` (TD1 whole-gate TSV) → seconds per file. */
@@ -23,6 +25,33 @@ export function costTable(tsv: string): Map<string, number> {
23
25
  return costs;
24
26
  }
25
27
 
28
+ /** TD1's human-reviewed dispositions, resolved by column name rather than TSV position. */
29
+ export function td1Dispositions(tsv: string): Map<string, Td1Disposition> {
30
+ const [header, ...rows] = tsv.trimEnd().split('\n');
31
+ const columns = header?.replace(/\r$/, '').split('\t') ?? [];
32
+ const index = (name: string) => columns.indexOf(name);
33
+ const result = new Map<string, Td1Disposition>();
34
+ for (const row of rows) {
35
+ const cells = row.replace(/\r$/, '').split('\t');
36
+ const get = (name: string) => cells[index(name)] ?? '';
37
+ if (get('file') && get('disposition')) result.set(get('file'), {
38
+ disposition: get('disposition'), alternative: get('alternative'), guards: get('guards'), why_slow: get('why_slow'),
39
+ });
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export function propose(judged: Judged, td1: ReadonlyMap<string, Td1Disposition>): Judged {
45
+ const reviewed = td1.get(judged.file);
46
+ if (reviewed) {
47
+ const proposal = reviewed.disposition === 'keep' ? null : reviewed.disposition as Proposal;
48
+ return { ...judged, proposal, basis: proposal ? [reviewed.alternative, reviewed.why_slow, reviewed.guards && `guards: ${reviewed.guards}`].filter(Boolean).join(' · ') : null };
49
+ }
50
+ if (judged.verdict === 'failing') return { ...judged, proposal: 'investigate', basis: `failing (rc=${judged.rc}) · caught90=${judged.caught90}` };
51
+ if (judged.verdict === 'review') return { ...judged, proposal: 'shrink', basis: `${judged.flags.join(', ')} · caught90=${judged.caught90}` };
52
+ return { ...judged, proposal: null, basis: null };
53
+ }
54
+
26
55
  /** Next slice from the cursor until the known cost reaches the budget (at least one file; wraps at the end). */
27
56
  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
57
  if (files.length === 0) return { start: 0, end: -1, next: 0, files: [], estimatedSecs: 0 };
@@ -51,6 +80,7 @@ export function judge(m: Measurement, caught90: number): Judged {
51
80
  }
52
81
 
53
82
  export function ledgerPath(root: string): string { return join(root, 'test-diet', 'ledger.jsonl'); }
83
+ export function nightlyAuditLedgerPath(root: string): string { return join(root, 'test-diet', 'nightly-audit.jsonl'); }
54
84
 
55
85
  export function lastLedgerLine(root: string): LedgerLine | null {
56
86
  const path = ledgerPath(root);
@@ -1,19 +1,22 @@
1
1
  #!/usr/bin/env bun
2
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|record (inside `elanous graph run graphs/test-diet/test-diet.yaml`)
3
+ // bun scripts/test-diet/node.ts pick|measure|audit|record (test-diet or nightly-audit graph)
4
4
  import { spawnSync } from 'node:child_process';
5
- import { readFileSync } from 'node:fs';
5
+ import { createHash } from 'node:crypto';
6
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
6
7
  import { join } from 'node:path';
7
8
  import { debug } from '../../src/debug/log.js';
8
9
  import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
9
- import { appendLedger, costTable, judge, lastLedgerLine, pickRange, writeCardDraft, type Measurement } from './lib.js';
10
+ import { CardStore } from '../../src/task-cards/card-store.js';
11
+ import { GATE_NIGHTLY_AUDITS } from '../release-loop/gate-node.js';
12
+ import { appendLedger, costTable, judge, lastLedgerLine, ledgerPath, nightlyAuditLedgerPath, pickRange, propose, td1Dispositions, writeCardDraft, type LedgerLine, type Measurement, type Td1Disposition } from './lib.js';
10
13
 
11
- type Context = { input: Record<string, unknown>; outputs: Record<string, Record<string, unknown>> };
14
+ type Context = { graphId?: string; input: Record<string, unknown>; outputs: Record<string, Record<string, unknown> | null> };
12
15
  function context(): Context {
13
16
  const at = process.env.ELANOUS_GRAPH_CONTEXT;
14
17
  if (!at) throw new Error('ELANOUS_GRAPH_CONTEXT required');
15
18
  const raw = JSON.parse(at.trimStart().startsWith('{') ? at : readFileSync(at, 'utf8')) as Partial<Context>;
16
- return { input: raw.input ?? {}, outputs: raw.outputs ?? {} };
19
+ return { graphId: raw.graphId, input: raw.input ?? {}, outputs: raw.outputs ?? {} };
17
20
  }
18
21
  function emit(result: Record<string, unknown> & { outcome: 'ok' | 'fail' | 'error'; summary: string }): number {
19
22
  console.log(JSON.stringify({ verdict: result.outcome === 'ok' ? 'pass' : 'fail', ...result }));
@@ -34,8 +37,8 @@ function pick(ctx: Context): number {
34
37
  }
35
38
 
36
39
  /** One remote run: shallow clone of the mirror, one install, each file alone with its peak memory. */
37
- function measure(ctx: Context): number {
38
- const picked = ctx.outputs.pick?.files;
40
+ function measure(ctx: Context, audit = false): number {
41
+ const picked = audit ? [...GATE_NIGHTLY_AUDITS] : ctx.outputs.pick?.files;
39
42
  if (!Array.isArray(picked) || picked.length === 0) return emit({ outcome: 'fail', summary: 'nothing picked' });
40
43
  const host = typeof ctx.input.remote === 'string' ? ctx.input.remote : 'node-b';
41
44
  const mirror = typeof ctx.input.mirror === 'string' ? ctx.input.mirror : '~/mirror/elanous-agent.git';
@@ -59,26 +62,67 @@ function measure(ctx: Context): number {
59
62
  // 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.
60
63
  ' 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)',
61
64
  ' printf "M\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n" "$f" "$e" "${rss:-}" "$rc" "${pass:-}" "${fail:-}" "$why"',
62
- 'done',
65
+ "done <<'TEST_DIET_FILES'", ...picked, 'TEST_DIET_FILES',
63
66
  ].join('\n');
64
- const run = spawnSync('ssh', ['-o', 'BatchMode=yes', host, 'bash', '-s'], { input: `${script}\n${picked.join('\n')}\n`, encoding: 'utf8', timeout: 4 * 3600_000, maxBuffer: 16 * 1024 * 1024 });
65
- // The file list follows the script on stdin; `read` in the loop consumes it.
66
- if (run.status !== 0 && !String(run.stdout).includes('\nM\t')) return emit({ outcome: 'error', summary: `remote measure failed (rc=${run.status}): ${String(run.stderr).trim().slice(0, 200)}` });
67
- const commit = /^COMMIT (\w+)/m.exec(run.stdout)?.[1] ?? '?';
67
+ const run = spawnSync('ssh', ['-o', 'BatchMode=yes', host, 'bash', '-s'], { input: `${script}\n`, encoding: 'utf8', timeout: 4 * 3600_000, maxBuffer: 16 * 1024 * 1024 });
68
+ const remoteError = run.status !== 0 ? `remote measure failed (rc=${run.status ?? 'no exit'}): ${String(run.error ?? run.stderr ?? '').trim().slice(0, 200)}` : undefined;
69
+ if (!audit && remoteError && !String(run.stdout).includes('\nM\t')) return emit({ outcome: 'error', summary: remoteError });
70
+ const commit = /^COMMIT (\w+)/m.exec(String(run.stdout))?.[1] ?? '?';
68
71
  const measurements: Measurement[] = String(run.stdout).split('\n').filter((l) => l.startsWith('M\t')).map((l) => {
69
72
  const [, file, secs, rss, rc, pass, fail, why] = l.split('\t');
70
73
  const num = (v: string | undefined) => (v === undefined || v === '' ? null : Number(v));
71
74
  return { file: file!, secs: Number(secs), rssMb: num(rss), rc: num(rc), pass: num(pass), fail: num(fail), ...(why ? { reason: why } : {}) };
72
75
  });
73
- return emit({ outcome: measurements.length ? 'ok' : 'error', summary: `measured ${measurements.length}/${picked.length} on ${host} at ${commit.slice(0, 9)}`, host, commit, measurements });
76
+ const complete = measurements.length === picked.length && picked.every((file) => measurements.some((m) => m.file === file));
77
+ const summary = remoteError ?? `measured ${measurements.length}/${picked.length} on ${host} at ${commit.slice(0, 9)}`;
78
+ return emit({ outcome: audit ? complete && !remoteError ? 'ok' : 'error' : measurements.length ? 'ok' : 'error',
79
+ summary, host, commit, measurements, ...(remoteError ? { remoteError } : {}) });
74
80
  }
75
81
 
76
- function record(ctx: Context): number {
82
+ function recordNightlyAudit(ctx: Context): number {
83
+ const m = ctx.outputs.audit;
84
+ const raw = Array.isArray(m?.measurements) ? m.measurements : [];
85
+ const measured = new Map<string, Measurement>();
86
+ for (const item of raw) {
87
+ if (!item || typeof item !== 'object' || !('file' in item) || typeof item.file !== 'string' || !GATE_NIGHTLY_AUDITS.includes(item.file)) continue;
88
+ const x = item as Partial<Measurement>;
89
+ if (measured.has(item.file) || typeof x.secs !== 'number' || !Number.isFinite(x.secs)
90
+ || (x.rssMb !== null && (typeof x.rssMb !== 'number' || !Number.isFinite(x.rssMb)))
91
+ || (x.rc !== null && (typeof x.rc !== 'number' || !Number.isInteger(x.rc)))
92
+ || (x.pass !== null && (typeof x.pass !== 'number' || !Number.isInteger(x.pass)))
93
+ || (x.fail !== null && (typeof x.fail !== 'number' || !Number.isInteger(x.fail)))) continue;
94
+ measured.set(item.file, x as Measurement);
95
+ }
96
+ const error = typeof m?.remoteError === 'string' ? m.remoteError
97
+ : m?.outcome === 'error' || m?.outcome === 'fail' || !m
98
+ ? typeof m?.summary === 'string' ? m.summary : 'audit execution produced no result' : undefined;
99
+ const results = GATE_NIGHTLY_AUDITS.map((file, index) => {
100
+ const found = measured.get(file);
101
+ const measurement: Measurement = found ?? { file, secs: 0, rssMb: null, rc: null, pass: null, fail: null,
102
+ reason: `not measured: ${typeof m?.summary === 'string' ? m.summary : error ?? 'audit result missing'}` };
103
+ // Even a complete set of file outputs cannot prove a clean audit when the remote process failed afterward.
104
+ return judge(error && measured.size === GATE_NIGHTLY_AUDITS.length && index === 0 && found
105
+ ? { ...found, rc: found.rc === 0 ? null : found.rc, reason: `${error}${found.reason ? `; ${found.reason}` : ''}` } : measurement, 1);
106
+ });
107
+ const line = { at: new Date().toISOString(), range: 'nightly-audit', start: 0, end: results.length - 1,
108
+ next: 0, total: results.length, commit: String(m?.commit ?? '?'), budgetSecs: 0, ...(error ? { error } : {}), results };
109
+ const root = effectiveInstanceRoot();
110
+ const path = nightlyAuditLedgerPath(root);
111
+ mkdirSync(join(root, 'test-diet'), { recursive: true });
112
+ appendFileSync(path, `${JSON.stringify(line)}\n`);
113
+ const card = writeCardDraft(root, line);
114
+ const failing = results.filter((r) => r.verdict === 'failing').length;
115
+ debug.log('test-diet', 'nightly-audit-recorded', { files: results.length, failing, card: card !== null });
116
+ return emit({ outcome: failing ? 'fail' : 'ok', summary: `nightly audit ${results.length} · failing ${failing}${card ? ' · card draft' : ''}`, failing, ledger: path, ...(card ? { card } : {}) });
117
+ }
118
+
119
+ export function record(ctx: Context, deps: { createStore?: (root: string) => Pick<CardStore, 'createCard' | 'appendSection' | 'close'>; td1?: ReadonlyMap<string, Td1Disposition> } = {}): number {
77
120
  const p = ctx.outputs.pick;
78
121
  const m = ctx.outputs.measure;
79
122
  const measurements = (m?.measurements ?? []) as Measurement[];
80
123
  if (!p || measurements.length === 0) return emit({ outcome: 'fail', summary: 'nothing measured' });
81
- 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)));
124
+ const td1 = deps.td1 ?? td1Dispositions(readFileSync(join(process.cwd(), 'docs/measurements/td1-top200-content-review-2026-10-01.tsv'), 'utf8'));
125
+ const results = measurements.map((x) => propose(judge(x, git(['log', '--since=90.days', '--format=%s', '--', x.file]).split('\n').filter((s) => /^fix\b/i.test(s)).length), td1));
82
126
  const line = {
83
127
  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),
84
128
  commit: String(m?.commit ?? '?'), budgetSecs: Number(p.budgetSecs), results,
@@ -86,16 +130,89 @@ function record(ctx: Context): number {
86
130
  const root = effectiveInstanceRoot();
87
131
  appendLedger(root, line);
88
132
  const card = writeCardDraft(root, line);
133
+ const proposals: Record<string, number> = {};
134
+ for (const result of results) {
135
+ if (!result.proposal) continue;
136
+ proposals[result.proposal] = (proposals[result.proposal] ?? 0) + 1;
137
+ let store: Pick<CardStore, 'createCard' | 'appendSection' | 'close'> | undefined;
138
+ try {
139
+ store = deps.createStore?.(root) ?? new CardStore(root);
140
+ const task = store.createCard({ goalId: `test-diet:${result.file}:${result.proposal}`, title: `시험 다이어트 제안 — ${result.file} · ${result.proposal}` });
141
+ const observation = { proposal: result.proposal, basis: result.basis, guards: td1.get(result.file)?.guards ?? '',
142
+ secs: result.secs, rssMb: result.rssMb, caught90: result.caught90, range: line.range };
143
+ const fingerprint = createHash('sha256').update(JSON.stringify(observation)).digest('hex');
144
+ const key = `test-diet:${line.commit}:${fingerprint}`;
145
+ if (!task.sections.some((section) => section.key === key)) store.appendSection(task.id, {
146
+ key, owner: 'test-diet', content: JSON.stringify({ ...observation, at: line.at }),
147
+ });
148
+ } catch (error) {
149
+ console.warn(`test-diet card failed: ${result.file}: ${String(error)}`);
150
+ debug.log('test-diet', 'card-failed', { file: result.file });
151
+ } finally {
152
+ try { store?.close(); } catch (error) {
153
+ console.warn(`test-diet card close failed: ${result.file}: ${String(error)}`);
154
+ debug.log('test-diet', 'card-failed', { file: result.file });
155
+ }
156
+ }
157
+ }
158
+ debug.log('test-diet', 'proposed', { range: line.range, proposals });
89
159
  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 };
90
160
  debug.log('test-diet', 'recorded', { range: line.range, ...counts, card: card !== null });
91
161
  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 } : {}) });
92
162
  }
93
163
 
164
+ function status(root: string, options: { json?: boolean; days?: number } = {}): number {
165
+ const path = ledgerPath(root);
166
+ const ledger = existsSync(path) ? readFileSync(path, 'utf8') : '';
167
+ if (!ledger.trim()) {
168
+ if (options.json) console.log(JSON.stringify({ error: '원장 없음 — 한 번도 안 돌았다' }));
169
+ else console.log('원장 없음 — 한 번도 안 돌았다');
170
+ return 1;
171
+ }
172
+ const latest = new Map<string, LedgerLine>();
173
+ for (const raw of ledger.split('\n').filter(Boolean)) {
174
+ const line = JSON.parse(raw) as LedgerLine;
175
+ const date = new Date(new Date(line.at).getTime() + 9 * 3600_000).toISOString().slice(0, 10);
176
+ if (!latest.has(date) || latest.get(date)!.at <= line.at) latest.set(date, line);
177
+ }
178
+ const days = options.days ?? 3;
179
+ const dates = [...latest.keys()].sort().reverse().slice(0, days);
180
+ let consecutive = 0;
181
+ for (const date of [...latest.keys()].sort().reverse()) {
182
+ const expected = new Date(Date.parse(`${dates[0]}T00:00:00Z`) - consecutive * 86_400_000).toISOString().slice(0, 10);
183
+ if (date !== expected) break;
184
+ consecutive++;
185
+ }
186
+ const rows = dates.map((date) => {
187
+ const line = latest.get(date)!;
188
+ return { date, range: line.range, measured: line.results.length, total: line.total,
189
+ keep: line.results.filter((r) => r.verdict === 'keep').length,
190
+ review: line.results.filter((r) => r.verdict === 'review').length,
191
+ failing: line.results.filter((r) => r.verdict === 'failing').length,
192
+ proposals: line.results.filter((r) => r.proposal != null).length };
193
+ });
194
+ if (options.json) console.log(JSON.stringify({ rows, consecutive, days }));
195
+ else {
196
+ for (const row of rows) console.log(`${row.date} · ${row.range} · ${row.measured}/${row.total} · keep ${row.keep}/review ${row.review}/failing ${row.failing} · 제안 ${row.proposals}`);
197
+ console.log(`연속 산출: ${consecutive}일 (기준 ${days})`);
198
+ }
199
+ return 0;
200
+ }
201
+
94
202
  if (import.meta.main) {
95
203
  const step = process.argv[2];
96
204
  try {
97
- const ctx = context();
98
- process.exitCode = step === 'pick' ? pick(ctx) : step === 'measure' ? measure(ctx) : step === 'record' ? record(ctx) : emit({ outcome: 'error', summary: `unknown step: ${step}` });
205
+ if (step === 'status') {
206
+ const args = process.argv.slice(3);
207
+ const daysAt = args.indexOf('--days');
208
+ const days = daysAt < 0 ? 3 : Number(args[daysAt + 1]);
209
+ if (!Number.isSafeInteger(days) || days < 1) throw new Error('--days needs a positive integer');
210
+ process.exitCode = status(effectiveInstanceRoot(), { json: args.includes('--json'), days });
211
+ } else {
212
+ const ctx = context();
213
+ process.exitCode = step === 'pick' ? pick(ctx) : step === 'measure' ? measure(ctx) : step === 'audit' ? measure(ctx, true)
214
+ : step === 'record' ? (ctx.graphId === 'nightly-audit' || Object.hasOwn(ctx.outputs, 'audit') ? recordNightlyAudit(ctx) : record(ctx)) : emit({ outcome: 'error', summary: `unknown step: ${step}` });
215
+ }
99
216
  } catch (error) {
100
217
  process.exitCode = emit({ outcome: 'error', summary: error instanceof Error ? error.message : String(error) });
101
218
  }
@@ -0,0 +1,114 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import ts from 'typescript';
5
+
6
+ export type TimeoutStatus = '파일 기본' | '시험별 일부' | '없음';
7
+ export type TimeoutFinding = { file: string; spawnLines: number; timeout: TimeoutStatus };
8
+
9
+ const target = /\bbin\/elanous\.mjs\b|\bprocess\.execPath\b|["']bun["']|\bscripts\/[^\s"']+\.(?:ts|tsx|sh)\b/;
10
+ const processCalls = new Set(['spawnSync', 'Bun.spawnSync', 'Bun.spawn', 'execFileSync', 'execSync']);
11
+
12
+ function callName(node: ts.CallExpression): string {
13
+ return node.expression.getText();
14
+ }
15
+
16
+ /** Inspect the command and argv, not unrelated options, resolving local const aliases. */
17
+ function commandText(node: ts.Expression, constants: ReadonlyMap<string, ts.Expression>, seen = new Set<string>()): string {
18
+ if (ts.isIdentifier(node) && constants.has(node.text)) {
19
+ if (seen.has(node.text)) return '';
20
+ seen.add(node.text);
21
+ return commandText(constants.get(node.text)!, constants, seen);
22
+ }
23
+ if (ts.isArrayLiteralExpression(node)) return node.elements.map((element) => commandText(element as ts.Expression, constants, new Set(seen))).join(' ');
24
+ if (ts.isObjectLiteralExpression(node)) return node.properties.map((property) =>
25
+ ts.isPropertyAssignment(property) ? commandText(property.initializer, constants, new Set(seen)) : '').join(' ');
26
+ if (ts.isStringLiteralLike(node)) return JSON.stringify(node.text);
27
+ if (ts.isPropertyAccessExpression(node) && node.getText() === 'process.execPath') return node.getText();
28
+ if (ts.isTemplateExpression(node)) return JSON.stringify(node.head.text) + node.templateSpans.map((span) => commandText(span.expression, constants, new Set(seen)) + JSON.stringify(span.literal.text)).join('');
29
+ if (ts.isParenthesizedExpression(node)) return commandText(node.expression, constants, seen);
30
+ if (ts.isCallExpression(node)) return node.arguments.map((arg) => commandText(arg, constants, new Set(seen))).join(' ');
31
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) return commandText(node.left, constants, new Set(seen)) + commandText(node.right, constants, new Set(seen));
32
+ return '';
33
+ }
34
+
35
+ function hasNumericTimeout(node: ts.Expression | undefined): boolean {
36
+ return !!node && (ts.isNumericLiteral(node) || (ts.isObjectLiteralExpression(node) && node.properties.some((property) =>
37
+ ts.isPropertyAssignment(property) && property.name.getText() === 'timeout' && ts.isNumericLiteral(property.initializer))));
38
+ }
39
+
40
+ function testTimeout(call: ts.CallExpression): boolean {
41
+ const name = callName(call);
42
+ if (name === 'test' || name === 'it') return hasNumericTimeout(call.arguments[2]);
43
+ if (name === 'describe') return call.arguments.some((arg) => hasNumericTimeout(arg));
44
+ return false;
45
+ }
46
+
47
+ function withinTimedTest(node: ts.Node): boolean {
48
+ for (let parent = node.parent; parent; parent = parent.parent) {
49
+ if (ts.isCallExpression(parent) && ['test', 'it', 'describe'].includes(callName(parent)) && testTimeout(parent)) return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ /** Classify files that actually invoke a Bun/elanous/script subprocess. */
55
+ export function classifyTimeouts(file: string, source: string): TimeoutFinding | null {
56
+ const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
57
+ const constants = new Map<string, ts.Expression>();
58
+ const collect = (node: ts.Node): void => {
59
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isVariableDeclarationList(node.parent)
60
+ && (node.parent.flags & ts.NodeFlags.Const)) constants.set(node.name.text, node.initializer);
61
+ ts.forEachChild(node, collect);
62
+ };
63
+ collect(tree);
64
+ const lines = new Set<number>();
65
+ let fileDefault = false;
66
+ let uncovered = false;
67
+ const scan = (node: ts.Node): void => {
68
+ if (ts.isCallExpression(node)) {
69
+ const name = callName(node);
70
+ if (name === 'setDefaultTimeout' && node.arguments.length > 0) fileDefault = true;
71
+ if (processCalls.has(name)) {
72
+ const command = node.arguments.slice(0, name === 'execSync' || name.startsWith('Bun.') ? 1 : 2)
73
+ .map((arg) => commandText(arg, constants)).join(' ');
74
+ const executable = node.arguments[0] ? commandText(node.arguments[0], constants) : '';
75
+ if (target.test(command) && !/^\s*"git"/.test(executable)) {
76
+ lines.add(tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1);
77
+ if (!withinTimedTest(node)) uncovered = true;
78
+ }
79
+ }
80
+ }
81
+ ts.forEachChild(node, scan);
82
+ };
83
+ scan(tree);
84
+ if (lines.size === 0) return null;
85
+ return { file, spawnLines: lines.size, timeout: fileDefault ? '파일 기본' : uncovered ? '없음' : '시험별 일부' };
86
+ }
87
+
88
+ export function auditTimeouts(root: string, paths: readonly string[]): TimeoutFinding[] {
89
+ return paths.map((file) => classifyTimeouts(file, readFileSync(join(root, file), 'utf8')))
90
+ .filter((finding): finding is TimeoutFinding => finding !== null)
91
+ .sort((a, b) => a.file.localeCompare(b.file));
92
+ }
93
+
94
+ export function renderTimeouts(findings: readonly TimeoutFinding[], json = false): string {
95
+ if (json) return JSON.stringify(findings, null, 2);
96
+ return ['파일 · 띄우는 줄 수 · 시한 상태', ...findings.map((f) => `${f.file} · ${f.spawnLines} · ${f.timeout}`)].join('\n');
97
+ }
98
+
99
+ export function main(root = process.cwd(), args = process.argv.slice(2)): number {
100
+ if (args.some((arg) => arg !== '--json')) {
101
+ console.error('Usage: bun scripts/test-timeout-audit.ts [--json]');
102
+ return 2;
103
+ }
104
+ const git = spawnSync('git', ['ls-files', '-z', '*.test.ts', '*.test.tsx'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
105
+ if (git.status !== 0 || git.error) {
106
+ console.error(git.error?.message ?? git.stderr ?? 'git ls-files failed');
107
+ return 2;
108
+ }
109
+ const findings = auditTimeouts(root, git.stdout.split('\0').filter(Boolean));
110
+ console.log(renderTimeouts(findings, args.includes('--json')));
111
+ return findings.some((f) => f.timeout === '없음') ? 1 : 0;
112
+ }
113
+
114
+ if (import.meta.main) process.exitCode = main();
@@ -20,6 +20,6 @@ export function renderTreeSyncAlert(tree: string, streak: number, decision: stri
20
20
 
21
21
  if (import.meta.main) {
22
22
  const [tree = '', streak = '0', decision = ''] = process.argv.slice(2);
23
- const ok = sendOutbound(renderTreeSyncAlert(tree, Number(streak) || 0, decision), 'alert');
23
+ const ok = sendOutbound(renderTreeSyncAlert(tree, Number(streak) || 0, decision), 'ops-alert');
24
24
  process.exit(ok ? 0 : 1);
25
25
  }
@@ -0,0 +1,71 @@
1
+ // V1 — rerun the hand-measured TUI checks (scripts/lib/tui-regress-checks.ts) on a real PTY, every release.
2
+ //
3
+ // bun scripts/tui-regress.ts [--only R2,R5] [--json]
4
+ //
5
+ // ⛔ Not under `bun test`: a real PTY of the real TUI is outside the test gate (NODE_ENV=test turns the PTY
6
+ // manifest off, and the run takes ~1 minute). Each check gets a fresh temp config/state dir and its own PTY;
7
+ // both are removed when the check ends, so the run leaves no process or folder behind.
8
+ import { spawnSync } from 'node:child_process';
9
+ import { mkdtempSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { startPty } from '../src/pty-shell/registry.js';
13
+ import { ERASE, TUI_REGRESS_CHECKS, type TuiCheck, type Verdict } from './lib/tui-regress-checks.js';
14
+
15
+ const COLS = 160;
16
+ const ROWS = 50;
17
+ const BOOT_MS = 9_000;
18
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
19
+
20
+ async function runCheck(check: TuiCheck): Promise<Verdict> {
21
+ const dir = mkdtempSync(join(tmpdir(), `tui-regress-${check.id}-`));
22
+ const cli = join(import.meta.dir, '..', 'bin', 'elanous.mjs');
23
+ if (check.role && check.role !== 'owner') {
24
+ // A config written from scratch has onboarding unfinished → bare entry would take the first-run (web-first) path,
25
+ // not the TUI. Mark onboarding done so the check sees the same TUI as an existing user.
26
+ spawnSync(process.execPath, [cli, '--config-dir', dir, 'config', 'set', 'tui.role', check.role], { stdio: 'ignore' });
27
+ spawnSync(process.execPath, [cli, '--config-dir', dir, 'config', 'set', 'onboarding.completed', 'true'], { stdio: 'ignore' });
28
+ }
29
+ const handle = startPty({
30
+ cmd: process.execPath, args: [cli, '--config-dir', dir, '--test-state-dir', dir],
31
+ cols: COLS, rows: ROWS, workdir: join(import.meta.dir, '..'),
32
+ env: { ...process.env, ELANOUS_DRIVE_TUI: '1', ELANOUS_STATE_DIR: dir },
33
+ });
34
+ const snaps: Record<string, string> = {};
35
+ try {
36
+ await sleep(BOOT_MS);
37
+ for (const step of check.steps) {
38
+ if (step.kind === 'type') handle.write(step.text);
39
+ else if (step.kind === 'key') handle.write(step.data);
40
+ else if (step.kind === 'erase') handle.write(ERASE.repeat(step.count));
41
+ else if (step.kind === 'wait') await sleep(step.ms);
42
+ else snaps[step.as] = await handle.renderScreen();
43
+ }
44
+ // TUI_REGRESS_DUMP=<id> prints that check's screens (to see why a verdict failed).
45
+ if (process.env.TUI_REGRESS_DUMP === check.id) for (const [k, v] of Object.entries(snaps)) console.log(`--- ${check.id}:${k}\n${v.split('\n').filter((l) => l.trim()).join('\n')}`);
46
+ return check.judge(snaps);
47
+ } catch (error) {
48
+ return { ok: false, reason: `실행 오류: ${String(error).slice(0, 120)}` };
49
+ } finally {
50
+ try { handle.kill('SIGKILL'); } catch { /* already gone */ }
51
+ rmSync(dir, { recursive: true, force: true });
52
+ }
53
+ }
54
+
55
+ const args = process.argv.slice(2);
56
+ const onlyArg = args.indexOf('--only');
57
+ const only = onlyArg >= 0 ? new Set((args[onlyArg + 1] ?? '').split(',').map((s) => s.trim()).filter(Boolean)) : null;
58
+ const json = args.includes('--json');
59
+ const checks = TUI_REGRESS_CHECKS.filter((check) => !only || only.has(check.id));
60
+ if (checks.length === 0) { console.error('tui-regress: 고른 칸이 없다 (--only 확인)'); process.exit(2); }
61
+
62
+ const results: { id: string; title: string; ok: boolean; reason: string }[] = [];
63
+ for (const check of checks) {
64
+ const verdict = await runCheck(check);
65
+ results.push({ id: check.id, title: check.title, ...verdict });
66
+ if (!json) console.log(`${check.id} ${verdict.ok ? '✅' : '❌'} ${check.title} — ${verdict.reason}`);
67
+ }
68
+ const fail = results.filter((r) => !r.ok).length;
69
+ if (json) console.log(JSON.stringify({ results, pass: results.length - fail, fail }));
70
+ console.log(`tui-regress: pass ${results.length - fail} · fail ${fail}`);
71
+ process.exit(fail ? 1 : 0);