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
@@ -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
@@ -0,0 +1,145 @@
1
+ // Pre-render field cards once; render only the photo composition on subsequent runs.
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { join, dirname, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { spawnSync } from 'node:child_process';
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const W = 1080, H = 1920, FPS = 30, CARD_VERSION = 1;
11
+ const cacheRoot = () => process.env.EXPLAINER_CACHE || join(homedir(), '.cache', 'elanous-explainer');
12
+ const valid = (path) => { try { const stat = statSync(path); return stat.isFile() && stat.size > 0; } catch { return false; } };
13
+ function run(bin, args, cwd) {
14
+ const result = spawnSync(bin, args, { cwd, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
15
+ if (result.error || result.status !== 0) throw new Error(`${bin}: ${result.error?.message ?? result.stderr?.slice(-2000) ?? `exit ${result.status}`}`);
16
+ return result.stdout;
17
+ }
18
+
19
+ async function renderCards({ title, sub, intro, end, cacheDir }) {
20
+ for (const [part, output] of [['intro', intro], ['end', end]]) {
21
+ if (valid(output)) continue;
22
+ const project = join(cacheDir, `project-${part}`);
23
+ mkdirSync(project, { recursive: true });
24
+ run('node', [join(here, 'reel.mjs'), project, '--part', part, '--title', title, '--sub', sub]);
25
+ run('npx', ['-y', `hyperframes@${process.env.HYPERFRAMES_VERSION || '0.8.95'}`, 'render', '--fps', String(FPS), '--workers', '4', '--output', output], join(project, 'reel', 'hf'));
26
+ }
27
+ }
28
+
29
+ export const normalizeCardTitle = (title) => !title || /^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(title) ? '현장 스케치' : title;
30
+
31
+ function ownerGone(lock) {
32
+ let pid;
33
+ try { pid = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8')).pid; }
34
+ catch (error) {
35
+ if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
36
+ // A new owner may still be writing its identity. Never reclaim that window.
37
+ try { return Date.now() - statSync(lock).mtimeMs > 5_000; } catch { return false; }
38
+ }
39
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
40
+ try { process.kill(pid, 0); return false; }
41
+ catch (error) { return error.code === 'ESRCH'; }
42
+ }
43
+
44
+ // A single reaper checks identity again under this claim before moving an orphan.
45
+ function recoverLock(lock) {
46
+ const recovery = `${lock}.recovery`;
47
+ try { mkdirSync(recovery); }
48
+ catch (error) {
49
+ if (error.code !== 'EEXIST') throw error;
50
+ if (ownerGone(recovery)) rmSync(recovery, { recursive: true, force: true });
51
+ return;
52
+ }
53
+ try {
54
+ writeFileSync(join(recovery, 'owner.json'), JSON.stringify({ pid: process.pid }));
55
+ if (!ownerGone(lock)) return;
56
+ const abandoned = `${lock}.abandoned-${randomUUID()}`;
57
+ try { renameSync(lock, abandoned); }
58
+ catch (error) { if (error.code !== 'ENOENT') throw error; return; }
59
+ rmSync(abandoned, { recursive: true, force: true });
60
+ } finally { rmSync(recovery, { recursive: true, force: true }); }
61
+ }
62
+
63
+ export async function ensureCards({ title, sub, cacheDir = join(cacheRoot(), 'cards'), render = renderCards }) {
64
+ title = normalizeCardTitle(title);
65
+ const key = createHash('sha256').update(JSON.stringify([title, sub, W, H, CARD_VERSION])).digest('hex').slice(0, 16);
66
+ const dir = join(cacheDir, key);
67
+ const intro = join(dir, 'intro.mp4'), end = join(dir, 'end.mp4');
68
+ const ready = () => valid(intro) && valid(end);
69
+ if (ready()) return { intro, end, cardsCache: 'hit' };
70
+ mkdirSync(cacheDir, { recursive: true });
71
+ const lock = `${dir}.lock`;
72
+ const deadline = Date.now() + 10 * 60_000;
73
+ for (;;) {
74
+ try {
75
+ mkdirSync(lock);
76
+ try { writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid })); }
77
+ catch (error) { rmSync(lock, { recursive: true, force: true }); throw error; }
78
+ break;
79
+ } catch (error) {
80
+ if (error.code !== 'EEXIST') throw error;
81
+ if (ready()) return { intro, end, cardsCache: 'hit' };
82
+ recoverLock(lock);
83
+ if (Date.now() >= deadline) throw new Error(`timed out waiting for card render: ${lock}`);
84
+ await new Promise((resolve) => setTimeout(resolve, 100));
85
+ }
86
+ }
87
+ let staging;
88
+ try {
89
+ if (ready()) return { intro, end, cardsCache: 'hit' };
90
+ staging = mkdtempSync(join(cacheDir, `.${key}-`));
91
+ await render({ title, sub, intro: join(staging, 'intro.mp4'), end: join(staging, 'end.mp4'), cacheDir: staging });
92
+ if (!valid(join(staging, 'intro.mp4')) || !valid(join(staging, 'end.mp4'))) throw new Error(`card renderer did not produce both cards: ${dir}`);
93
+ rmSync(dir, { recursive: true, force: true });
94
+ renameSync(staging, dir);
95
+ return { intro, end, cardsCache: 'miss' };
96
+ } finally {
97
+ if (staging) rmSync(staging, { recursive: true, force: true });
98
+ rmSync(lock, { recursive: true, force: true });
99
+ }
100
+ }
101
+
102
+ function duration(file) {
103
+ const seconds = Number(run('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file]).trim());
104
+ if (!Number.isFinite(seconds) || seconds <= 0) throw new Error(`invalid duration: ${file}`);
105
+ return seconds;
106
+ }
107
+
108
+ export function stitch({ intro, body, end, bgm, out }) {
109
+ const first = duration(intro), middle = duration(body), last = duration(end);
110
+ if (Math.min(first, middle, last) <= 0.3) throw new Error('xfade requires clips longer than 0.3s');
111
+ const total = first + middle + last - 0.6;
112
+ const args = ['-v', 'error', '-y', '-i', intro, '-i', body, '-i', end];
113
+ const music = bgm && existsSync(bgm);
114
+ if (music) args.push('-stream_loop', '-1', '-i', bgm);
115
+ const filters = [
116
+ ...[0, 1, 2].map((i) => `[${i}:v]fps=${FPS},scale=${W}:${H},setsar=1,format=yuv420p,settb=AVTB,setpts=PTS-STARTPTS[v${i}]`),
117
+ `[v0][v1]xfade=transition=fade:duration=0.3:offset=${(first - 0.3).toFixed(3)}[v01]`,
118
+ `[v01][v2]xfade=transition=fade:duration=0.3:offset=${(first + middle - 0.6).toFixed(3)}[v]`,
119
+ ];
120
+ if (music) filters.push(`[3:a]atrim=duration=${total.toFixed(3)},asetpts=PTS-STARTPTS,afade=t=in:d=0.4,afade=t=out:st=${Math.max(0, total - 1.6).toFixed(3)}:d=1.6,loudnorm=I=-16:TP=-1.5:LRA=9[a]`);
121
+ else filters.push(`anullsrc=r=48000:cl=stereo,atrim=duration=${total.toFixed(3)}[a]`);
122
+ mkdirSync(dirname(out), { recursive: true });
123
+ run('ffmpeg', [...args, '-filter_complex', filters.join(';'), '-map', '[v]', '-map', '[a]', '-t', total.toFixed(3), '-r', String(FPS), '-c:v', 'libx264', '-preset', 'veryfast', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-ar', '48000', '-ac', '2', '-movflags', '+faststart', out]);
124
+ return out;
125
+ }
126
+
127
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
128
+ const args = process.argv.slice(2);
129
+ const value = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
130
+ const folder = args[1] && resolve(args[1]);
131
+ if (args[0] === '--warm-cards') {
132
+ const title = value('--title'), sub = value('--sub');
133
+ if (title === undefined || sub === undefined) throw new Error('--warm-cards requires --title and --sub');
134
+ const cards = await ensureCards({ title, sub });
135
+ console.log(`reel: cards ${cards.cardsCache} · ${dirname(cards.intro)}`);
136
+ } else if (args[0] === '--instant' && folder) {
137
+ const timelineFile = join(folder, 'reel', 'timeline.json');
138
+ const timeline = JSON.parse(readFileSync(timelineFile, 'utf8'));
139
+ const cards = await ensureCards({ title: timeline.title, sub: timeline.sub });
140
+ const bgm = process.env.FIELD_REEL_BGM || join(cacheRoot(), 'bgm', 'field.wav');
141
+ stitch({ intro: cards.intro, body: join(folder, 'reel', 'body.mp4'), end: cards.end, bgm, out: join(folder, 'reel', 'reel-9x16.mp4') });
142
+ writeFileSync(timelineFile, JSON.stringify({ ...timeline, cardsCache: cards.cardsCache, music: existsSync(bgm) }, null, 2));
143
+ console.log(`reel: cards ${cards.cardsCache}`);
144
+ } else throw new Error('usage: stitch.mjs --warm-cards --title … --sub … | --instant <folder>');
145
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: pitch-deck
3
+ description: "회사나 제품의 한국어 피치덱·투자 소개 슬라이드를 사실 출처와 함께 만들 때 사용한다. 피치덱, 발표 덱, pitch deck 요청에 대응한다."
4
+ category: knowledge
5
+ sideEffects: write
6
+ triggers:
7
+ - 피치덱
8
+ - pitch deck
9
+ requires: []
10
+ ---
11
+
12
+ # Pitch deck — 근거를 지닌 짧은 발표
13
+
14
+ ## 언제 / 입력
15
+
16
+ 회사 또는 제품을 5~10분에 소개할 때 쓴다. 입력은 **회사 한 줄 정의**, 저장소의 **사실 원천 파일들**(읽은 판본과 줄 위치), **발표 길이**다. 독자·구매자·공개 가능 범위를 먼저 정한다. 확인되지 않은 값은 매출·고객·시장 실적처럼 말하지 않는다.
17
+
18
+ ## 장 구성
19
+
20
+ 기본 뼈대 **일곱**: **정의**(한 줄로 무엇을 하나) → **문제**(누가 왜 멈추나) → **시장**(TAM·SAM·SOM, 출처 없으면 가정) → **기술/BM**(해결법·경쟁력·BM·누가 지불하나) → **성과**(검증된 사례·측정 또는 «아직 검증 전») → **성장 전략**(어떤 순서로 누구에게 닿나) → **팀**(누가 실행하나). **계획**은 기본 일곱과 *별도 마지막 장*: 제목·표지에 «계획 · 순서와 시기는 바뀔 수 있습니다»를 쓰고 월 단위 약속은 쓰지 않는다. 발표 길이에 맞게 기본 장 하나를 둘로 나누거나 오프닝을 더해 최종 8~12장으로 만든다.
21
+
22
+ 첫 장은 **3초 안에 뜻이 전해지는 쉬운 한 줄**, 전문 용어 0. 다음 장에는 **실물 증거**를 둔다: 실제 화면의 자리와 시연할 행동을 명시하고, 이미지가 없으면 «화면 자리»라고 표기한다. 화면·시연은 검증 전이면 «계획»으로 구분하며 이미 된 일로 그리지 않는다. 사이트와 데모 대본의 메시지 순서(약속 → 증거 → 작동 원리 → 다음 행동)를 따른다. 설득 문장은 빠르고 쉬워도 기술적 아우라는 검증 가능한 실물로 뒷받침한다. 남의 이름·제휴 암시·«곧»·미측정 성과는 넣지 않는다.
23
+
24
+ 각 장: **한 장 한 메시지**, 제목은 명사 나열이 아니라 **주장 문장**. 본문·제목을 합쳐 **공백 기준 45낱말 이하**(출처 ID·진행표시·접근성 라벨 제외). 넘치면 장을 나누고 핵심을 고른다. 수치·날짜·비율은 읽은 파일의 **파일:줄** 또는 공개 링크를 SOURCES의 개별 주장과 짝짓는다. 출처가 없는 시장 규모·추정 TAM/SAM/SOM은 장에 **«가정»** 표시하고 SOURCES의 가정 목록에도 적는다. 근거가 없으면 수치를 만들지 말고 측정 방법을 제시한다. 고객·매출·계약·기술 성숙도를 과장하지 않는다.
25
+
26
+ ### 누구에게 파나 — ECP / ICP
27
+
28
+ ECP(early customer profile)의 **시급한 문제·사건/구매 트리거·현재 우회 비용·도입 준비**를 묻고, ICP(ideal customer profile)의 **조직/업종·규모·구매자·사용자·의사결정 권한·도입 제약**을 적는다. 시장 장에서는 ECP → ICP → 접근 가능한 구매자(SOM) 순서로 좁힌다. 기술/BM 장에는 구매자와 사용자, 값이 생기는 순간, 과금 가설을 서로 구분한다. 인터뷰 전인 칸은 «가정»이라고 쓰며 공개되지 않은 연락처·고객사 내부 수치를 옮기지 않는다.
29
+
30
+ ## 작성 · 출력
31
+
32
+ 1. 참고본은 `.eml`의 본문·첨부를 파싱해 **슬라이드 구조만**, docx/xlsx에서는 **칸 이름만** 참고한다. 참고본의 사람 이름·연락처·주소·내부 수치·원문 문구는 전재하지 않는다. 저장소의 브랜드 정본과 공개 가능한 기능 근거를 읽고 계획을 현재 성과로 바꾸지 않는다.
33
+ 2. `template.html`의 CSS·JS를 각 덱 HTML에 **인라인 복사**하고 `<section class="slide" data-claim="…">`마다 하나의 메시지를 넣는다. 파일 밖 폰트·이미지·스크립트 의존성 없이 로컬 파일로 직접 연다. 제목의 주장 ID마다 SOURCES 표에 **같은 ID**, 출처와 그 줄에서 확인할 내용, 상태(사실/가정/계획)를 기록한다. 숫자 하나라도 별도 근거가 필요하면 같은 행에서 항목별로 대응한다.
34
+ 3. 출력은 **한 파일 HTML 덱**, 키보드 방향키/스페이스로 넘김(화면 버튼도 제공), 16:9, 인쇄하면 한 쪽 한 장. 버튼·입력 요소에 초점이 있을 때는 슬라이드 단축키를 건너뛰어 기본 키보드 조작(«이전» 버튼에서 스페이스)을 보존한다. 인쇄 미리보기·첫/끝 넘김·숫자/개인 연락처를 확인한다. 디자인 시스템을 새로 만들지 않고 `내부 문서 `BRAND-CANON-elanvital-and-elanous-2026-09-30``의 색·서체 사용 기준을 따른다.
35
+
36
+ 기존 연결: `src/skills/index.ts`의 `buildSkillIndex` → `listSkillNames`/`parseSkillMd`가 이 폴더의 SKILL.md를 발견한다. 덱은 스킬이 작성해 직접 여는 **독립 문서**이며 HTML을 위한 신규 런타임 라우트는 만들지 않는다.
@@ -0,0 +1,80 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>피치덱 — 회사명</title>
7
+ <style>
8
+ :root { color-scheme: dark; font-family: Geist, "G sans", "Noto Sans KR", system-ui, sans-serif; background: #0c0c0e; color: #f2f1ee; }
9
+ * { box-sizing: border-box; }
10
+ body { margin: 0; }
11
+ main { min-height: 100svh; display: grid; place-items: center; padding: 4rem 1rem; }
12
+ .slide { display: none; width: min(100%, calc((100svh - 8rem) * 16 / 9)); aspect-ratio: 16 / 9; padding: clamp(1.4rem, 5vw, 5rem); background: #0c0c0e; border: 1px solid #f2f1ee; flex-direction: column; justify-content: space-between; overflow: hidden; }
13
+ .slide.active { display: flex; }
14
+ .eyebrow, .marker { color: #e95047; font-size: clamp(.8rem, 1.3vw, 1rem); font-weight: 650; letter-spacing: .08em; }
15
+ h1, h2 { max-width: 20ch; font-size: clamp(2rem, 4.4vw, 4.9rem); line-height: 1.15; letter-spacing: -.045em; margin: .4em 0; text-wrap: balance; }
16
+ p, li { max-width: 54ch; font-size: clamp(1rem, 1.7vw, 1.6rem); line-height: 1.55; }
17
+ ul { padding-left: 1.2em; }
18
+ .accent { color: #e95047; }
19
+ .screen-slot { border: 1px dashed #e95047; padding: .6rem 1rem; color: #f2f1ee; font-size: clamp(.9rem, 1.4vw, 1.2rem); }
20
+ .foot { display: flex; justify-content: space-between; align-items: end; gap: 1rem; color: #f2f1ee; font-size: .85rem; }
21
+ nav { position: fixed; bottom: 1rem; right: 1rem; display: flex; align-items: center; gap: .7rem; background: #0c0c0e; padding: .4rem; }
22
+ button { background: #0c0c0e; color: #f2f1ee; border: 1px solid #e95047; padding: .6rem .9rem; font: inherit; cursor: pointer; }
23
+ button:focus-visible { outline: 3px solid #e95047; outline-offset: 3px; }
24
+ button:disabled { opacity: .42; cursor: not-allowed; }
25
+ @media print {
26
+ @page { size: 16in 9in; margin: 0; }
27
+ body { width: 16in; }
28
+ :root { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
29
+ main { display: block; padding: 0; min-height: 0; }
30
+ .slide, .slide.active { display: flex; width: 16in; height: 9in; aspect-ratio: auto; border: 0; break-after: page; page-break-after: always; }
31
+ .slide:last-child { break-after: auto; page-break-after: auto; }
32
+ nav { display: none; }
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <main id="deck" aria-label="피치덱">
38
+ <section class="slide active" data-claim="T1" aria-label="정의" aria-hidden="false">
39
+ <span class="eyebrow">브랜드 · 정의</span>
40
+ <div><h1>한 줄로 무엇을 하는지 말합니다.</h1><p>검증 가능한 회사 한 줄 정의를 여기에 넣습니다.</p></div>
41
+ <div class="foot"><span>출처: SOURCES · T1</span><span class="marker">회사명</span></div>
42
+ </section>
43
+ <section class="slide" data-claim="T2" aria-label="계획" aria-hidden="true">
44
+ <span class="eyebrow">계획 · 순서와 시기는 바뀔 수 있습니다</span>
45
+ <div><h2>다음 걸음은 검증하며 정합니다.</h2><p>이 칸은 현재 제공 기능이 아니라 앞으로의 계획입니다.</p></div>
46
+ <div class="foot"><span>출처: SOURCES · T2</span><span class="marker">계획</span></div>
47
+ </section>
48
+ </main>
49
+ <nav aria-label="슬라이드 이동"><button id="prev" type="button" aria-label="이전 장">← 이전</button><output id="position" aria-live="polite"></output><button id="next" type="button" aria-label="다음 장">다음 →</button></nav>
50
+ <script>
51
+ (() => {
52
+ const slides = [...document.querySelectorAll('section.slide')];
53
+ const prev = document.getElementById('prev');
54
+ const next = document.getElementById('next');
55
+ const position = document.getElementById('position');
56
+ let current = 0;
57
+ function show(index) {
58
+ current = Math.max(0, Math.min(slides.length - 1, index));
59
+ slides.forEach((slide, i) => {
60
+ slide.classList.toggle('active', i === current);
61
+ slide.setAttribute('aria-hidden', String(i !== current));
62
+ });
63
+ prev.disabled = current === 0;
64
+ next.disabled = current === slides.length - 1;
65
+ position.textContent = `${current + 1} / ${slides.length}`;
66
+ }
67
+ prev.addEventListener('click', () => show(current - 1));
68
+ next.addEventListener('click', () => show(current + 1));
69
+ document.addEventListener('keydown', event => {
70
+ if (event.target instanceof Element && event.target.closest('button, a, input, textarea, select, [contenteditable="true"]')) return;
71
+ if (['ArrowRight', 'ArrowDown', ' ', 'PageDown'].includes(event.key)) { event.preventDefault(); show(current + 1); }
72
+ if (['ArrowLeft', 'ArrowUp', 'PageUp'].includes(event.key)) { event.preventDefault(); show(current - 1); }
73
+ if (event.key === 'Home') show(0);
74
+ if (event.key === 'End') show(slides.length - 1);
75
+ });
76
+ show(0);
77
+ })();
78
+ </script>
79
+ </body>
80
+ </html>
@@ -103,6 +103,12 @@ export interface CodexApprovalAdapter {
103
103
  onMcpToolCall(req: CodexMcpToolCallRequest): Promise<CodexApprovalDecision>;
104
104
  }
105
105
 
106
+ /** The 3-way approval question's id and labels — exported so a surface that
107
+ * answers approvals by yes/no (an ACP turn's client) can recognise it. */
108
+ export const CODEX_APPROVAL_QUESTION_ID = 'codex_approval';
109
+ export const CODEX_APPROVAL_OPT_ONCE = 'Allow once';
110
+ export const CODEX_APPROVAL_OPT_REJECT = 'Reject';
111
+
106
112
  /** Build a CodexApprovalAdapter. Intentionally minimal in Phase 3.A —
107
113
  * the handlers degrade gracefully when no approver is attached, and
108
114
  * the Phase 3.B app-server client will hook these methods up with real
@@ -131,10 +137,10 @@ export function createCodexApprovalAdapter(
131
137
  // multi-option question path is used. "Allow for session" maps to codex
132
138
  // `acceptForSession` so the sub-agent stops re-prompting for the rest of
133
139
  // the run.
134
- const OPT_ONCE = 'Allow once';
140
+ const OPT_ONCE = CODEX_APPROVAL_OPT_ONCE;
135
141
  const OPT_SESSION = 'Allow for session';
136
- const OPT_REJECT = 'Reject';
137
- const APPROVAL_QID = 'codex_approval';
142
+ const OPT_REJECT = CODEX_APPROVAL_OPT_REJECT;
143
+ const APPROVAL_QID = CODEX_APPROVAL_QUESTION_ID;
138
144
 
139
145
  /** Ask the user via the MULTI-OPTION question channel: Allow once /
140
146
  * Allow for session / Reject. Returns the scoped decision. Only called