elanous 0.2.9 → 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 (361) 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-f972fc1c00a62e88.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-f8116559c7424da7.js +1 -0
  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-54e1f966ab60a4dc.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/editor/{page-973996d584f008c1.js → page-7ed95f296f5ee8e9.js} +1 -1
  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-8b0402a46a749a3d.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-037f83e2a1edc8e2.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-0df30e0c7e50148a.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-4fddc90b23feb215.js +1 -0
  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-90670589b87abea8.js → page-26102fd7350f171e.js} +1 -1
  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-35bedde6d885f08f.js → page-139ba5c367bbf60a.js} +1 -1
  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 +14 -14
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +14 -14
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +14 -14
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +14 -14
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +14 -14
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +14 -14
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +14 -14
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +14 -14
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +14 -14
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +14 -14
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +14 -14
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +14 -14
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +14 -14
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +14 -14
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +14 -14
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +14 -14
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +14 -14
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +14 -14
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +14 -14
  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 +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  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 +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/package.json +1 -1
  146. package/plugins/card-followup/graphs/run-step.ts +48 -13
  147. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  148. package/plugins/doc-draft/graphs/run-step.ts +13 -6
  149. package/scripts/ci-pwa-build-gate.ts +160 -0
  150. package/scripts/coord-channel-watch.sh +42 -8
  151. package/scripts/coord-digest.ts +199 -0
  152. package/scripts/coord-post.sh +60 -3
  153. package/scripts/install.ps1 +68 -9
  154. package/scripts/install.sh +39 -12
  155. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  156. package/scripts/lib/tui-regress-checks.ts +146 -0
  157. package/scripts/memory-lifecycle-cycle.ts +2 -2
  158. package/scripts/ops-now-probe.ts +327 -0
  159. package/scripts/publish-internal-dist.ts +11 -1
  160. package/scripts/release-loop/cutoff.ts +19 -8
  161. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  162. package/scripts/release-loop/release-readiness.ts +9 -0
  163. package/scripts/release-loop/version-node.ts +58 -10
  164. package/scripts/repo-watch-cycle.ts +1 -1
  165. package/scripts/retro-cycle.ts +2 -2
  166. package/scripts/schedule-health-report.ts +1 -1
  167. package/scripts/storyboard/storyboard.ts +5 -161
  168. package/scripts/test-diet/lib.ts +30 -1
  169. package/scripts/test-diet/node.ts +82 -7
  170. package/scripts/test-timeout-audit.ts +114 -0
  171. package/scripts/tree-sync-alert.ts +1 -1
  172. package/scripts/tui-regress.ts +71 -0
  173. package/scripts/waitlist-daily-digest.ts +1 -1
  174. package/skills/explainer-video/SKILL.md +4 -0
  175. package/skills/explainer-video/engine/reel.mjs +18 -13
  176. package/skills/explainer-video/engine/reel.sh +24 -5
  177. package/skills/explainer-video/engine/stitch.mjs +145 -0
  178. package/skills/pitch-deck/SKILL.md +36 -0
  179. package/skills/pitch-deck/template.html +80 -0
  180. package/src/acp/codex-approval-adapter.ts +9 -3
  181. package/src/acp/core-turn-bridge.ts +117 -18
  182. package/src/acp/fact-question.ts +11 -0
  183. package/src/acp/server.ts +48 -7
  184. package/src/acp/tool-approval.ts +126 -0
  185. package/src/acp/turn-runner.ts +36 -0
  186. package/src/agent/monad-agent-turn.ts +8 -0
  187. package/src/ask-user-question/pending-questions.ts +25 -10
  188. package/src/ask-user-question/types.ts +9 -0
  189. package/src/auth/setup-link-tokens.ts +16 -5
  190. package/src/autopilot/build/integrity-gate.ts +12 -0
  191. package/src/boot/daemon-prompt-turn.ts +3 -0
  192. package/src/boot/daemon-tools/index.ts +8 -0
  193. package/src/boot/daemon-tools/types.ts +2 -0
  194. package/src/boot/daemon-tools/web-search.ts +1 -0
  195. package/src/chat/ctrl-c-in-input.ts +14 -0
  196. package/src/chat/index.ts +52 -0
  197. package/src/chat/pickers/state.ts +6 -2
  198. package/src/cli/agent-cli.ts +7 -0
  199. package/src/cli/decisions-cli.ts +25 -3
  200. package/src/cli/doctor-bundle-upload.ts +68 -0
  201. package/src/cli/doctor-cli.ts +38 -4
  202. package/src/cli/ops-cli.ts +20 -0
  203. package/src/cli/pod-cli.ts +49 -0
  204. package/src/cli/pr-cli.ts +31 -2
  205. package/src/cli/pwa-stop.ts +163 -13
  206. package/src/cli/pwa-test.ts +3 -0
  207. package/src/cli/release-cli.ts +63 -20
  208. package/src/cli/self-update.ts +14 -0
  209. package/src/cli/storyboard-cli.ts +37 -0
  210. package/src/cli/test-state-dir-flag.ts +11 -4
  211. package/src/connectors/linear.ts +146 -0
  212. package/src/context-bus/coord-events.ts +115 -0
  213. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  214. package/src/dashboard/index.ts +58 -21
  215. package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  217. package/src/decisions/decision-cards.ts +49 -1
  218. package/src/decisions/decision-ledger.ts +75 -4
  219. package/src/decisions/decision-linear-projection.ts +157 -0
  220. package/src/decisions/web-push-decision-cards.ts +50 -0
  221. package/src/directive/interpretation-card.ts +36 -0
  222. package/src/discord.ts +2 -1
  223. package/src/domains/coo-admin-tool.ts +62 -0
  224. package/src/domains/core-tools.ts +11 -3
  225. package/src/domains/elanous-card.ts +29 -0
  226. package/src/domains/ops-facts-tool.ts +76 -0
  227. package/src/domains/outbound-alert.ts +41 -13
  228. package/src/domains/release-tool.ts +168 -0
  229. package/src/domains/telegram-kind-route.ts +19 -1
  230. package/src/exec-requests/planner.ts +98 -28
  231. package/src/exec-requests/runner.ts +107 -10
  232. package/src/exec-requests/store.ts +8 -2
  233. package/src/field/field-media.ts +10 -7
  234. package/src/field/field-reel.ts +14 -4
  235. package/src/git-fs/worktree.ts +45 -6
  236. package/src/graph-runner/graph-approval-card.ts +113 -0
  237. package/src/graph-runner/graph-cli.ts +129 -4
  238. package/src/graph-runner/graph-notify.ts +25 -3
  239. package/src/graph-runner/runner.ts +142 -16
  240. package/src/harness/browser-verify.ts +25 -2
  241. package/src/harness/harness-cli-command.ts +231 -35
  242. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  243. package/src/harness/harness-incidents.ts +129 -0
  244. package/src/harness/harness-stop.ts +53 -3
  245. package/src/hitl/clarification-policy.ts +3 -1
  246. package/src/index.ts +156 -37
  247. package/src/intake-plane/author-goals.ts +37 -4
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/discord-seat-work.ts +47 -2
  250. package/src/intake-plane/seat-doc-route.ts +170 -6
  251. package/src/intake-plane/telegram-seat-work.ts +44 -2
  252. package/src/intake-plane/telegram-work.ts +1 -0
  253. package/src/maturity/cli-maturity.ts +34 -0
  254. package/src/maturity/feature-maturity.ts +98 -1
  255. package/src/maturity/tui-slash-maturity.ts +31 -0
  256. package/src/memory.ts +32 -4
  257. package/src/nexus/api/exec-requests.ts +33 -3
  258. package/src/nexus/api/http-server.ts +53 -1
  259. package/src/nexus/api/meta-api.ts +11 -0
  260. package/src/nexus/api/ops-api.ts +10 -1
  261. package/src/nexus/api/ops-seats-sources.ts +37 -7
  262. package/src/nexus/api/ops-seats.ts +16 -6
  263. package/src/nexus/api/seat-requests.ts +88 -8
  264. package/src/nexus/api/sessions-store.ts +63 -0
  265. package/src/nexus/api/setup-codex-login.ts +160 -0
  266. package/src/nexus/api/usage-api.ts +102 -0
  267. package/src/nexus/index.ts +45 -4
  268. package/src/onboarding/first-run.ts +89 -0
  269. package/src/onboarding/lan-address.ts +45 -0
  270. package/src/onboarding/web-first.ts +119 -24
  271. package/src/onboarding.ts +4 -0
  272. package/src/ops-now/ops-now-refresh.ts +100 -0
  273. package/src/project/project-store.ts +115 -0
  274. package/src/release-loop/checklist.ts +78 -4
  275. package/src/release-loop/feature-store.ts +154 -21
  276. package/src/release-loop/release-schedule.ts +53 -0
  277. package/src/repl/index.ts +7 -0
  278. package/src/seat-dispatch/ceo-commands.ts +41 -8
  279. package/src/seat-dispatch/ceo-intent.ts +38 -0
  280. package/src/seat-loop/seat-loop.ts +231 -0
  281. package/src/seat-loop/seat-report.ts +64 -0
  282. package/src/self-dev/decompose.ts +66 -11
  283. package/src/self-dev/run-store.ts +10 -1
  284. package/src/self-implement/execution-clarification.ts +115 -0
  285. package/src/self-implement/gate-cli.ts +6 -3
  286. package/src/self-implement/orchestrator.ts +37 -3
  287. package/src/self-implement/pipeline-shape.ts +1 -1
  288. package/src/self-implement/run-status-mapping.ts +5 -3
  289. package/src/session/chat.ts +2 -0
  290. package/src/session/index.ts +9 -0
  291. package/src/shell-env-bootstrap.ts +17 -0
  292. package/src/steward/directive.ts +51 -2
  293. package/src/steward/launch.ts +250 -0
  294. package/src/steward/outcome-notify.ts +115 -0
  295. package/src/steward/steward-cards.ts +8 -2
  296. package/src/steward/triage-plan.ts +32 -4
  297. package/src/steward/triage.ts +93 -9
  298. package/src/steward/working-backwards.ts +64 -0
  299. package/src/storyboard/storyboard.ts +141 -0
  300. package/src/task-cards/card-store.ts +2 -2
  301. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  302. package/src/telegram-card-followup.ts +233 -0
  303. package/src/telegram-report.ts +15 -9
  304. package/src/telegram-run.ts +7 -0
  305. package/src/telegram.ts +29 -8
  306. package/src/tool-runtime/types.ts +4 -0
  307. package/src/tui-client/dashboard-session.ts +2 -0
  308. package/src/user-config.ts +118 -11
  309. package/src/version/packed-revision.json +1 -1
  310. package/src/web-push/notify-exec-request.ts +32 -0
  311. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/morning/page-adfbd2ba9598e59a.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  359. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
  360. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  361. /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -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 };
@@ -2,12 +2,14 @@
2
2
  // TD2 graph nodes: pick → measure → record. Shadow only — measures alone on a remote host and drafts a card.
3
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 { appendFileSync, mkdirSync, 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';
10
+ import { CardStore } from '../../src/task-cards/card-store.js';
9
11
  import { GATE_NIGHTLY_AUDITS } from '../release-loop/gate-node.js';
10
- import { appendLedger, costTable, judge, lastLedgerLine, nightlyAuditLedgerPath, pickRange, writeCardDraft, type Measurement } from './lib.js';
12
+ import { appendLedger, costTable, judge, lastLedgerLine, ledgerPath, nightlyAuditLedgerPath, pickRange, propose, td1Dispositions, writeCardDraft, type LedgerLine, type Measurement, type Td1Disposition } from './lib.js';
11
13
 
12
14
  type Context = { graphId?: string; input: Record<string, unknown>; outputs: Record<string, Record<string, unknown> | null> };
13
15
  function context(): Context {
@@ -114,12 +116,13 @@ function recordNightlyAudit(ctx: Context): number {
114
116
  return emit({ outcome: failing ? 'fail' : 'ok', summary: `nightly audit ${results.length} · failing ${failing}${card ? ' · card draft' : ''}`, failing, ledger: path, ...(card ? { card } : {}) });
115
117
  }
116
118
 
117
- function record(ctx: Context): number {
119
+ export function record(ctx: Context, deps: { createStore?: (root: string) => Pick<CardStore, 'createCard' | 'appendSection' | 'close'>; td1?: ReadonlyMap<string, Td1Disposition> } = {}): number {
118
120
  const p = ctx.outputs.pick;
119
121
  const m = ctx.outputs.measure;
120
122
  const measurements = (m?.measurements ?? []) as Measurement[];
121
123
  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)));
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));
123
126
  const line = {
124
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),
125
128
  commit: String(m?.commit ?? '?'), budgetSecs: Number(p.budgetSecs), results,
@@ -127,17 +130,89 @@ function record(ctx: Context): number {
127
130
  const root = effectiveInstanceRoot();
128
131
  appendLedger(root, line);
129
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 });
130
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 };
131
160
  debug.log('test-diet', 'recorded', { range: line.range, ...counts, card: card !== null });
132
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 } : {}) });
133
162
  }
134
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
+
135
202
  if (import.meta.main) {
136
203
  const step = process.argv[2];
137
204
  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}` });
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
+ }
141
216
  } catch (error) {
142
217
  process.exitCode = emit({ outcome: 'error', summary: error instanceof Error ? error.message : String(error) });
143
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);
@@ -99,7 +99,7 @@ export async function main(): Promise<void> {
99
99
  try { return JSON.parse(readFileSync(statePath, 'utf8')); } catch { return null; }
100
100
  },
101
101
  writeState: (s) => { mkdirSync(dirname(statePath), { recursive: true }); writeFileSync(statePath, JSON.stringify(s)); },
102
- send: (text) => sendOutbound(text, 'report'),
102
+ send: (text) => sendOutbound(text, 'ops-report'),
103
103
  });
104
104
  debug.log('waitlist.digest', 'reported', result);
105
105
  console.log(`[waitlist-daily-digest] sent=${result.sent} total=${result.total} fresh=${result.fresh}`);
@@ -91,8 +91,12 @@ The reference is used only for its structure (header, chapter cards, a diagram t
91
91
 
92
92
  ```bash
93
93
  zsh $SKILL/engine/reel.sh <folder> --title "마케터의 밤" --sub "2026.10.02 · 서울"
94
+ zsh $SKILL/engine/reel.sh --warm-cards --title "마케터의 밤" --sub "2026.10.02 · 서울" # 행사 전 카드 미리 굽기
95
+ zsh $SKILL/engine/reel.sh <folder> --title "마케터의 밤" --sub "2026.10.02 · 서울" --instant # 사진 구간만 렌더
94
96
  ```
95
97
 
98
+ 즉석판 카드 캐시: `~/.cache/elanous-explainer/cards/<sha256(JSON 배열 [title, sub, 1080, 1920, 카드판번호]) 앞 16자>/{intro,end}.mp4` (`EXPLAINER_CACHE`로 루트 변경 가능).
99
+
96
100
  - **Input**: a folder of phone photos (jpg · png · heic) and short clips (mp4 · mov). The default upload folder is `~/.elanous/field/<event>/`.
97
101
  - **Order**: by capture time (`mdls` creation date), then by file name.
98
102
  - **Captions**: optional `<folder>/captions.txt`, one line per item as `파일명 | 자막`. A photo without a line gets one line from a vision model (`codex exec -i`, all photos in parallel, 30 s cap each); if that fails it shows «현장 N». `FIELD_REEL_VISION=0` turns the vision step off.
@@ -12,15 +12,18 @@ import { join, dirname, resolve, extname, basename } from "node:path";
12
12
  import { homedir } from "node:os";
13
13
  import { spawn, spawnSync } from "node:child_process";
14
14
  import { fileURLToPath } from "node:url";
15
+ import { normalizeCardTitle } from "./stitch.mjs";
15
16
 
16
17
  const here = dirname(fileURLToPath(import.meta.url));
17
18
  const argv = process.argv.slice(2);
18
19
  const opt = (k, d) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : d; };
19
20
  const folder = resolve(argv.find((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--"))) ?? ".");
20
21
  const PART = opt("part", "full");
22
+ if (!["full", "body", "intro", "end"].includes(PART)) throw new Error(`unknown reel part: ${PART}`);
23
+ const CARD = PART === "intro" || PART === "end";
21
24
  const MAX = Number(opt("max", 60)), MIN = Number(opt("min", 24));
22
25
  const W = 1080, H = 1920, PHOTO = 4.2, CLIP_MAX = 5, XF = 0.5;
23
- const INTRO = PART === "body" ? 0 : 3.0, END = PART === "body" ? 0 : 3.4;
26
+ const INTRO = PART === "body" || PART === "end" ? 0 : 3.0, END = PART === "body" || PART === "intro" ? 0 : 3.4;
24
27
  const out = join(folder, "reel"), hf = join(out, "hf"), media = join(hf, "assets", "media");
25
28
  mkdirSync(media, { recursive: true }); mkdirSync(join(hf, "assets", "fonts"), { recursive: true });
26
29
  const T0 = Date.now(), lap = (s) => console.log(`reel: ${s} · ${((Date.now() - T0) / 1000).toFixed(1)}s`);
@@ -37,18 +40,19 @@ const created = (p) => {
37
40
  return Number.isFinite(t) ? t : statSync(p).mtimeMs;
38
41
  };
39
42
  const probeDur = (p) => Number(run("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", p]).stdout.trim()) || 0;
40
- const caps = Object.fromEntries((existsSync(join(folder, "captions.txt")) ? readFileSync(join(folder, "captions.txt"), "utf8") : "")
43
+ let TITLE = opt("title", ""), SUB = opt("sub", "");
44
+ const caps = Object.fromEntries((!CARD && existsSync(join(folder, "captions.txt")) ? readFileSync(join(folder, "captions.txt"), "utf8") : "")
41
45
  .split("\n").filter((l) => l.includes("|")).map((l) => l.split("|").map((s) => s.trim())));
42
- let items = readdirSync(folder).filter((f) => [...IMG, ...VID].includes(extname(f).toLowerCase()))
46
+ let items = CARD ? [] : readdirSync(folder).filter((f) => [...IMG, ...VID].includes(extname(f).toLowerCase()))
43
47
  .map((f) => ({ f, p: join(folder, f), video: VID.includes(extname(f).toLowerCase()), t: created(join(folder, f)) }))
44
48
  .sort((a, b) => a.t - b.t || a.f.localeCompare(b.f));
45
- if (!items.length) throw new Error(`no photos or clips in ${folder}`);
49
+ if (!CARD && !items.length) throw new Error(`no photos or clips in ${folder}`);
46
50
 
47
51
  // ── durations: fit into [MIN, MAX] ─────────────────────────────────────────────
48
52
  for (const it of items) it.dur = it.video ? Math.min(CLIP_MAX, Math.max(1.5, probeDur(it.p))) : PHOTO;
49
53
  const body = () => items.reduce((a, it) => a + it.dur, 0);
50
54
  while (INTRO + body() + END > MAX && items.length > 1) items.splice(Math.floor(items.length / 2), 1); // drop from the middle, keep first & last
51
- if (PART !== "body" && INTRO + body() + END < MIN) { const photos = items.filter((i) => !i.video); const need = MIN - (INTRO + body() + END); for (const p of photos) p.dur += need / Math.max(1, photos.length); }
55
+ if (PART === "full" && INTRO + body() + END < MIN) { const photos = items.filter((i) => !i.video); const need = MIN - (INTRO + body() + END); for (const p of photos) p.dur += need / Math.max(1, photos.length); }
52
56
 
53
57
  // ── media prep: HEIC → jpg · big photos down to 2160 · a small copy for the vision model · clips re-encoded short & muted ──
54
58
  let t = INTRO;
@@ -92,18 +96,17 @@ const oneLine = (s, max) => {
92
96
  const CAP_PROMPT = "행사 현장 사진이다. 세로 영상 자막으로 쓸 한국어 한 줄을 출력하라 — 18자 이내 · 마침표 없이 · 보이는 장면만 · 사람 이름·외모 묘사·추측 금지 · 화면 속 글자(행사명·회사명)는 읽히면 써도 된다. 그 한 줄만.";
93
97
  const HEAD_PROMPT = "같은 행사에서 찍은 사진들이다. 영상 첫 화면 제목으로 쓸 한국어 행사 이름 한 줄을 출력하라 — 16자 이내 · 화면 속 글자에서 읽히는 행사·회사 이름을 우선 · 읽히지 않으면 장면을 짧게(예: «상장 기념식 현장») · 추측 금지. 그 한 줄만.";
94
98
  const slugTitle = (s) => !s || /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(s);
95
- let TITLE = opt("title", ""), SUB = opt("sub", "");
96
99
  const photos = items.filter((it) => !it.video);
97
100
  const VT0 = Date.now();
98
- const jobs = VISION ? [
101
+ const jobs = VISION && !CARD ? [
99
102
  ...photos.filter((it) => !caps[it.f]).map(async (it) => { it.vcap = oneLine(await ask(CAP_PROMPT, [join(out, `v${items.indexOf(it)}.jpg`)]), 22); }),
100
103
  ...(PART !== "body" && slugTitle(TITLE) && photos.length ? [(async () => { TITLE = oneLine(await ask(HEAD_PROMPT, photos.slice(0, 3).map((it) => join(out, `v${items.indexOf(it)}.jpg`))), 20) || TITLE; })()] : []),
101
104
  ] : [];
102
105
  await Promise.all(jobs);
103
106
  const visionMs = Date.now() - VT0;
104
107
  items.forEach((it, i) => { it.cap = caps[it.f] || it.vcap || (it.video ? "" : `현장 ${i + 1}`); });
105
- if (slugTitle(TITLE)) TITLE = "현장 스케치";
106
- if (!SUB) { const d = new Date(items[0].t + 9 * 3600e3); SUB = `${d.getUTCFullYear()}.${String(d.getUTCMonth() + 1).padStart(2, "0")}.${String(d.getUTCDate()).padStart(2, "0")}`; }
108
+ TITLE = normalizeCardTitle(TITLE);
109
+ if (!CARD && !SUB) { const d = new Date(items[0].t + 9 * 3600e3); SUB = `${d.getUTCFullYear()}.${String(d.getUTCMonth() + 1).padStart(2, "0")}.${String(d.getUTCDate()).padStart(2, "0")}`; }
107
110
  lap(`vision ${jobs.length ? `${items.filter((i) => i.vcap).length}/${photos.filter((p) => !caps[p.f]).length} captions` : "off"} · title «${TITLE}»`);
108
111
 
109
112
  // ── assets (same cache as build.mjs) ───────────────────────────────────────────
@@ -119,7 +122,7 @@ for (const [rel, url] of Object.entries(ASSETS)) {
119
122
  // ── music: cut to length · fade in/out · level to about -16 LUFS (the whole mix is music) ──
120
123
  const BGM = process.env.FIELD_REEL_BGM || join(CACHE, "bgm", "field.wav");
121
124
  let music = "";
122
- if (PART !== "body" && existsSync(BGM)) {
125
+ if (PART === "full" && existsSync(BGM)) {
123
126
  const r = run("ffmpeg", ["-v", "error", "-y", "-i", BGM, "-t", String(TOTAL), "-af", `afade=t=in:d=0.4,afade=t=out:st=${f(TOTAL - 1.6)}:d=1.6,loudnorm=I=-16:TP=-1.5:LRA=9`, "-ar", "48000", "-ac", "2", join(media, "bgm.wav")]);
124
127
  if (r.status === 0) music = "bgm.wav"; else console.error(`reel: music skipped — ${r.stderr.slice(0, 160)}`);
125
128
  }
@@ -165,16 +168,18 @@ const clips = items.map((it, i) => {
165
168
  return vis + cap;
166
169
  }).join("\n");
167
170
 
168
- const intro = PART === "body" ? "" : `<section class="clip card" data-start="0" data-duration="${INTRO}" data-track-index="6">`
171
+ const intro = PART === "body" || PART === "end" ? "" : `<section class="clip card" data-start="0" data-duration="${INTRO}" data-track-index="6">`
169
172
  + `<div class="kick ik">현장 스케치</div>${mark(200, "im")}<h1>${words(TITLE, "it")}</h1><div class="line il"></div><p class="is">${esc(SUB)}</p></section>`;
170
- const end = PART === "body" ? "" : `<section class="clip card" data-start="${f(t)}" data-duration="${END}" data-track-index="6">`
173
+ const end = PART === "body" || PART === "intro" ? "" : `<section class="clip card" data-start="${f(t)}" data-duration="${END}" data-track-index="6">`
171
174
  + `${mark(230, "em")}<h1 class="et">Elanous</h1><p class="et">사진을 올리면, 영상이 됩니다</p><p class="et red">elanous.ai</p></section>`;
172
- if (PART !== "body") {
175
+ if (PART === "full" || PART === "intro") {
173
176
  tw.push(`tl.fromTo(".ik", {opacity:0, letterSpacing:"0.6em"}, {opacity:1, letterSpacing:"0.28em", duration:0.8, ease:"power3.out"}, 0.1);`);
174
177
  tw.push(`tl.fromTo(".im", {rotation:-160, scale:0.6, opacity:0}, {rotation:0, scale:1, opacity:1, duration:1.0, ease:"power3.out"}, 0.0);`);
175
178
  tw.push(`tl.fromTo(".it", {opacity:0, y:50}, {opacity:1, y:0, duration:0.55, stagger:0.1, ease:"power3.out"}, 0.45);`);
176
179
  tw.push(`tl.fromTo(".il", {scaleX:0}, {scaleX:1, duration:0.6, ease:"power3.inOut"}, 0.9);`);
177
180
  tw.push(`tl.fromTo(".is", {opacity:0, y:16}, {opacity:1, y:0, duration:0.45, ease:"power3.out"}, 1.15);`);
181
+ }
182
+ if (PART === "full" || PART === "end") {
178
183
  tw.push(`tl.fromTo(".em", {rotation:-200, scale:0.5}, {rotation:0, scale:1, duration:1.2, ease:"power3.out"}, ${f(t)});`);
179
184
  tw.push(`tl.fromTo(".et", {opacity:0, y:20}, {opacity:1, y:0, duration:0.5, stagger:0.14, ease:"power3.out"}, ${f(t + 0.5)});`);
180
185
  }
@@ -1,9 +1,28 @@
1
1
  #!/bin/zsh
2
- # Field reel in one command: folder of phone photos/clips → <folder>/reel/<name>-9x16.mp4 · prints the elapsed seconds.
2
+ # Field reel: standard full composition or instant body with cached cards.
3
3
  # The layout/contrast check is advisory: phone photos behind captions often miss 3:1, and a failed check must not stop the reel.
4
- # Usage: zsh reel.sh <folder> [--title "…"] [--sub "…"] (captions: <folder>/captions.txt — `파일 | 자막`)
5
- E=${0:A:h}; F=${1:A}; shift
4
+ # Usage: zsh reel.sh <folder> [--title "…"] [--sub "…"] [--instant] (captions: <folder>/captions.txt — `파일 | 자막`)
5
+ E=${0:A:h}
6
+ if [[ $1 == --warm-cards ]]; then
7
+ shift
8
+ node $E/stitch.mjs --warm-cards "$@"
9
+ exit $?
10
+ fi
11
+ F=${1:A}; shift
6
12
  [[ -d $F ]] || { print -u2 "reel: no folder $F"; exit 2 }
7
13
  T0=$(date +%s)
8
- rm -rf $F/reel && node $E/reel.mjs $F "$@" && (cd $F/reel/hf && { npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} check >../render.log 2>&1 || print "reel: check reported issues (continuing — user photos vary; see above)" >>../render.log }; npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} render --fps 30 --workers 4 --output ../reel-9x16.mp4 >>../render.log 2>&1) || { print -u2 "reel: FAILED — see $F/reel/render.log"; exit 1 }
9
- print "reel: $F/reel/reel-9x16.mp4 · $(( $(date +%s) - T0 ))s"
14
+ instant=0
15
+ args=()
16
+ for arg in "$@"; do
17
+ if [[ $arg == --instant ]]; then instant=1; else args+=("$arg"); fi
18
+ done
19
+ rm -rf $F/reel
20
+ if (( instant )); then
21
+ node $E/reel.mjs $F "${args[@]}" --part body &&
22
+ (cd $F/reel/hf && { npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} check >../render.log 2>&1 || print "reel: check reported issues (continuing — user photos vary; see above)" >>../render.log }; npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} render --fps 30 --workers 4 --output ../body.mp4 >>../render.log 2>&1) &&
23
+ node $E/stitch.mjs --instant $F >>$F/reel/render.log 2>&1 || { print -u2 "reel: FAILED — see $F/reel/render.log"; exit 1 }
24
+ print "reel: $F/reel/reel-9x16.mp4 · $(( $(date +%s) - T0 ))s (instant)"
25
+ else
26
+ node $E/reel.mjs $F "${args[@]}" && (cd $F/reel/hf && { npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} check >../render.log 2>&1 || print "reel: check reported issues (continuing — user photos vary; see above)" >>../render.log }; npx -y hyperframes@${HYPERFRAMES_VERSION:-0.8.95} render --fps 30 --workers 4 --output ../reel-9x16.mp4 >>../render.log 2>&1) || { print -u2 "reel: FAILED — see $F/reel/render.log"; exit 1 }
27
+ print "reel: $F/reel/reel-9x16.mp4 · $(( $(date +%s) - T0 ))s"
28
+ fi