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
@@ -0,0 +1,115 @@
1
+ import { realpathSync, statSync } from 'node:fs';
2
+ import { isAbsolute, extname, relative, resolve } from 'node:path';
3
+ import { debug } from '../debug/log.js';
4
+ import { notifyMissionDocument, notifyMissionOrigin } from '../autopilot/mission-notify.js';
5
+ import type { MissionOrigin } from '../autopilot/mission-origin.js';
6
+ import { redactSecrets } from '../task-cards/card-store.js';
7
+ import { loadDirectiveOrigins } from './directive.js';
8
+ import { saveLaunchLedger, type LaunchEntry, type LaunchLedger } from './launch.js';
9
+
10
+ export interface OutcomeNotifyDeps {
11
+ root: string;
12
+ ledger: LaunchLedger;
13
+ shadow?: boolean;
14
+ loadOrigin?: (root: string, issue: string) => MissionOrigin | null;
15
+ loadOrigins?: typeof loadDirectiveOrigins;
16
+ document?: typeof notifyMissionDocument;
17
+ message?: typeof notifyMissionOrigin;
18
+ worktreeCommand?: (args: string[]) => { exitCode: number; stdout: string };
19
+ }
20
+
21
+ const EXTENSIONS = new Set(['.md', '.pdf', '.png', '.jpg', '.csv', '.txt', '.html', '.json']);
22
+ const MAX_BYTES = 20 * 1024 * 1024;
23
+
24
+ function localArtifact(outcome: LaunchEntry, deps: OutcomeNotifyDeps): string | null {
25
+ const ref = outcome.artifactRef;
26
+ const branch = outcome.artifactBranch;
27
+ if (!ref || !branch || isAbsolute(ref) || !EXTENSIONS.has(extname(ref).toLowerCase())) return null;
28
+ const command = deps.worktreeCommand ?? ((args: string[]) => {
29
+ const result = Bun.spawnSync(args, { cwd: resolve(import.meta.dir, '../..'), stdout: 'pipe', stderr: 'pipe' });
30
+ return { exitCode: result.exitCode, stdout: result.stdout.toString() };
31
+ });
32
+ try {
33
+ const result = command(['git', 'worktree', 'list', '--porcelain']);
34
+ if (result.exitCode !== 0) return null;
35
+ for (const block of result.stdout.split(/\n\s*\n/)) {
36
+ const path = /^worktree (.+)$/m.exec(block)?.[1];
37
+ if (!path || !block.split('\n').includes(`branch refs/heads/${branch}`) ||
38
+ (outcome.artifactWorktree && !path.endsWith(outcome.artifactWorktree))) continue;
39
+ try {
40
+ const base = realpathSync(path);
41
+ const file = realpathSync(resolve(base, ref));
42
+ const inside = relative(base, file);
43
+ if (!inside || inside === '..' || inside.startsWith('../') || isAbsolute(inside)) continue;
44
+ const stat = statSync(file);
45
+ return stat.isFile() && stat.size <= MAX_BYTES ? file : null;
46
+ } catch { /* Worktree or file is gone; try another matching worktree. */ }
47
+ }
48
+ } catch { /* No accessible worktree or artifact: use PR link instead. */ }
49
+ return null;
50
+ }
51
+
52
+ /** Persist each conversation's delivery independently; only failed recipients retry (up to three times). */
53
+ export function notifyOutcome(outcome: LaunchEntry, deps: OutcomeNotifyDeps): LaunchEntry['notified'] {
54
+ if (outcome.notified === 'shadow') return 'shadow';
55
+ let origins: MissionOrigin[] = [];
56
+ try {
57
+ origins = deps.loadOrigin
58
+ ? [deps.loadOrigin(deps.root, outcome.issue)].filter((item): item is MissionOrigin => item !== null)
59
+ : (deps.loadOrigins ?? loadDirectiveOrigins)(deps.root, outcome.issue);
60
+ } catch { /* Damaged or unavailable origin cannot route a result. */ }
61
+ const recipients = new Map<string, MissionOrigin>();
62
+ for (const origin of origins) {
63
+ if (origin.channel !== 'telegram' || !Number.isSafeInteger(origin.chatId)) continue;
64
+ const key = JSON.stringify([origin.chatId, origin.botId ?? null, origin.threadId ?? null]);
65
+ recipients.set(key, origin);
66
+ }
67
+ if (outcome.notified === 'skipped-no-origin' && recipients.size === 0 && !deps.shadow) return outcome.notified;
68
+ if (outcome.notified === 'sent' && !outcome.notifyRecipients) return 'sent';
69
+ const file = outcome.status !== 'failed' && (recipients.size > 0 || deps.shadow) ? localArtifact(outcome, deps) : null;
70
+ const kind = outcome.status === 'failed' ? 'failure' : file ? 'file' : 'link';
71
+ let sent = false;
72
+ if (deps.shadow) {
73
+ outcome.notified = 'shadow';
74
+ outcome.notifyPreview = { hasChatId: recipients.size > 0, kind: file ? 'file' : 'link' };
75
+ } else if (recipients.size === 0) {
76
+ outcome.notified = 'skipped-no-origin';
77
+ } else {
78
+ const title = redactSecrets(outcome.title).replace(/\s+/g, ' ').slice(0, 240);
79
+ // Say what happened in plain words: an open PR is not a merge, and a run that left nothing is not a failure.
80
+ const status = outcome.awaitingMerge ? 'PR 열림(병합 대기)' : outcome.status === 'merged' ? '병합됨'
81
+ : outcome.status === 'completed' ? '완료(병합 없음)' : outcome.status;
82
+ const pr = outcome.prNumber;
83
+ const link = outcome.prUrl && /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/.test(outcome.prUrl) &&
84
+ (pr === undefined || outcome.prUrl.endsWith(`/pull/${pr}`)) ? outcome.prUrl : undefined;
85
+ const noOutput = outcome.status === 'completed' && !outcome.artifactRef && pr === undefined && !outcome.prUrl;
86
+ const undeliverable = outcome.status !== 'failed' && !noOutput && !file && !link;
87
+ const text = outcome.status === 'failed'
88
+ ? `${title} · 실패 — ${redactSecrets(outcome.reason ?? '런 실패').split(/\r?\n/)[0]?.slice(0, 240) ?? '런 실패'}`
89
+ : noOutput
90
+ ? `${title} · ${status} — 남긴 파일·PR 없음`
91
+ : undeliverable
92
+ ? `${title} · ${status} · 산출물 전달 불가 — ${outcome.artifactRef ? '파일을 확인할 수 없습니다' : '검증된 PR 링크가 없습니다'}`
93
+ : `${title} · ${status} · PR ${pr ?? '없음'} · ${link}`;
94
+ const legacyAttempts = !outcome.notifyRecipients && outcome.notified === 'failed' ? outcome.notifyAttempts ?? 0 : 0;
95
+ const progress = outcome.notifyRecipients ??= {};
96
+ for (const [key, origin] of recipients) {
97
+ const previous = progress[key] ?? (legacyAttempts ? { notified: 'failed' as const, attempts: legacyAttempts } : undefined);
98
+ if (previous?.notified === 'sent' || (previous?.attempts ?? 0) >= 3) continue;
99
+ let delivered = false;
100
+ try {
101
+ if (!undeliverable || !previous) {
102
+ delivered = file ? (deps.document ?? notifyMissionDocument)(origin, file, redactSecrets(`${title} · ${status}`))
103
+ : (deps.message ?? notifyMissionOrigin)(origin, redactSecrets(text));
104
+ }
105
+ } catch { /* A failed recipient remains eligible on a later tick. */ }
106
+ progress[key] = { notified: delivered && !undeliverable ? 'sent' : 'failed', attempts: (previous?.attempts ?? 0) + 1 };
107
+ sent ||= delivered && !undeliverable;
108
+ }
109
+ outcome.notifyAttempts = Math.max(0, ...Object.values(progress).map(item => item.attempts));
110
+ outcome.notified = [...recipients.keys()].every(key => progress[key]?.notified === 'sent') ? 'sent' : 'failed';
111
+ }
112
+ saveLaunchLedger(deps.root, deps.ledger);
113
+ debug.log('steward.outcome', 'notify', { cardId: `linear:${outcome.issue}`, issue: outcome.issue, kind, sent });
114
+ return outcome.notified;
115
+ }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { CardStore, redactSecrets, type AppendSectionInput, type CreateCardInput, type TaskCard } from '../task-cards/card-store.js';
3
3
  import type { ScheduledDecision, TriageIssue } from './triage.js';
4
+ import type { HitlEntry } from './launch.js';
4
5
 
5
6
  export interface StewardCardStore {
6
7
  createCard(input: CreateCardInput): TaskCard;
@@ -33,7 +34,7 @@ export function recordTriageOnCards(decisions: ScheduledDecision[], issues: Tria
33
34
  });
34
35
  }
35
36
  const { rung, dependsOn, priority, disposition, hitlReason, why, owner } = decision;
36
- const judgment = { rung, dependsOn, priority, disposition: rung === 'hitl' || hitlReason ? 'hitl' : disposition, hitlReason: hitlReason ?? null, why: redactSecrets(why), ...(owner ? { owner } : {}) };
37
+ const judgment = { rung, dependsOn, priority, disposition: rung === 'hitl' || hitlReason ? 'hitl' : disposition, hitlReason: hitlReason ?? null, why: redactSecrets(why), ...(owner ? { owner } : {}), ...(decision.capability ? { capability: decision.capability } : {}) };
37
38
  const key = `triage:${createHash('sha256').update(JSON.stringify(judgment)).digest('hex')}`;
38
39
  if (!card.sections.some(section => section.key === key)) {
39
40
  store.appendSection(card.id, { key, owner: 'steward', content: JSON.stringify(judgment) });
@@ -44,7 +45,7 @@ export function recordTriageOnCards(decisions: ScheduledDecision[], issues: Tria
44
45
  }
45
46
  }
46
47
 
47
- function recordOnCard(section: 'launch' | 'outcome', issue: Pick<TriageIssue, 'identifier' | 'title'>, value: object, deps: StewardCardDeps): void {
48
+ function recordOnCard(section: 'launch' | 'outcome' | 'hitl', issue: Pick<TriageIssue, 'identifier' | 'title'>, value: object, deps: StewardCardDeps): void {
48
49
  const store = deps.store ?? new CardStore(deps.root);
49
50
  try {
50
51
  const content = redactSecrets(JSON.stringify(value));
@@ -67,3 +68,8 @@ export function recordLaunchOnCard(issue: Pick<TriageIssue, 'identifier' | 'titl
67
68
  export function recordOutcomeOnCard(issue: Pick<TriageIssue, 'identifier' | 'title'>, outcome: object, deps: StewardCardDeps = {}): void {
68
69
  recordOnCard('outcome', issue, outcome, deps);
69
70
  }
71
+
72
+ /** Append a HITL handoff revision (pending → raised/unavailable) to the issue card. */
73
+ export function recordHitlOnCard(issue: Pick<TriageIssue, 'identifier' | 'title'>, entry: HitlEntry, deps: StewardCardDeps = {}): void {
74
+ recordOnCard('hitl', issue, entry, deps);
75
+ }
@@ -1,8 +1,33 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
1
4
  import { resolveRoleModel } from '../user-config.js';
2
5
  import type { HitlReason, Rung, TriageIssue } from './triage.js';
3
6
 
4
7
  export type StewardAsk = (prompt: string, role: 'classify' | 'planning') => Promise<unknown>;
5
- export interface ClassifiedIssue { issue: string; rung: Rung; why: string; hitlReason?: HitlReason; duplicateOf?: string }
8
+ export interface ClassifiedIssue { issue: string; rung: Rung; why: string; hitlReason?: HitlReason; duplicateOf?: string; capability?: 'new-capability' | 'existing-capability' }
9
+ export interface CapabilityInventory { graphs: readonly string[]; plugins: readonly string[]; commands: readonly string[] }
10
+
11
+ /** Read installed names, not promises in documentation. The caller can supply a test-instance root. */
12
+ export function installedCapabilities(root: string = resolve(import.meta.dir, '../..')): CapabilityInventory {
13
+ const files = (dir: string): string[] => {
14
+ try { return readdirSync(dir, { withFileTypes: true }).flatMap(entry => entry.isDirectory()
15
+ ? files(join(dir, entry.name)) : entry.isFile() && /\.ya?ml$/.test(entry.name) ? [join(dir, entry.name)] : []); }
16
+ catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; }
17
+ };
18
+ const graphs = files(join(root, 'graphs')).flatMap(file => {
19
+ const match = /^graph_id:\s*['"]?([^\s'"#]+)/m.exec(readFileSync(file, 'utf8'));
20
+ return match ? [match[1]!] : [];
21
+ });
22
+ let plugins: string[];
23
+ try { plugins = readdirSync(join(root, 'plugins'), { withFileTypes: true }).filter(entry => entry.isDirectory() && entry.name !== 'examples').map(entry => entry.name); }
24
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; plugins = []; }
25
+ const help = spawnSync(process.execPath, [join(root, 'bin/elanous.mjs'), '--test', '--help'], { cwd: root, encoding: 'utf8', timeout: 15_000 });
26
+ // An unconfigured test instance cannot start the CLI. Unknown commands must not be treated as installed.
27
+ const commands = help.status === 0
28
+ ? [...help.stdout.matchAll(/^\s{2}(?!-)([\w:-]+)(?:\s|$)/gm)].map(match => match[1]!) : [];
29
+ return { graphs, plugins, commands };
30
+ }
6
31
  export interface IssuePlan { issue: string; priority: number; dependsOn: string[]; owner?: string }
7
32
 
8
33
  const unsafe: Array<[HitlReason, RegExp]> = [
@@ -42,14 +67,17 @@ export async function askSteward(prompt: string, role: 'classify' | 'planning'):
42
67
  return parsed(result);
43
68
  }
44
69
 
45
- export async function classifyIssue(issue: TriageIssue, ask: StewardAsk = askSteward): Promise<ClassifiedIssue> {
70
+ export async function classifyIssue(issue: TriageIssue, ask: StewardAsk = askSteward, inventory: CapabilityInventory = installedCapabilities()): Promise<ClassifiedIssue> {
46
71
  const candidate = unsafeReason(issue);
47
- const response = parsed(await ask(`스튜어드 분류. JSON 객체만: {"rung":0|1|2|3|4|5|"hitl","hitlReason":null,"why":"한 문장"}. 0=이미됨, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl; 그 외 사람 확인은 other. 확인되지 않은 완료를 추측하지 마라. ${candidate ? `안전 후보 ${candidate}: 이유를 확인하라.` : ''}\n이슈: ${JSON.stringify(issue)}`, 'classify'));
72
+ const response = parsed(await ask(`스튜어드 분류. JSON 객체만: {"rung":0|1|2|3|4|5|"hitl","hitlReason":null,"why":"한 문장","capability":"new-capability"|"existing-capability"}. 0=이미됨, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl; 그 외 사람 확인은 other. 새 능력인지 기존 그래프·플러그인·명령으로 되는 일인지 판정하라. 존재만으로 그 기능이 소원을 충족한다고 추측하지 마라. 확인되지 않은 완료를 추측하지 마라. ${candidate ? `안전 후보 ${candidate}: 이유를 확인하라.` : ''}\n설치된 능력: ${JSON.stringify(inventory)}\n이슈: ${JSON.stringify(issue)}`, 'classify'));
48
73
  if (![0, 1, 2, 3, 4, 5, 'hitl'].includes(response.rung as Rung) || typeof response.why !== 'string' || !response.why.trim()) throw new Error(`Invalid steward classification: ${issue.identifier}`);
49
74
  const reason = candidate ?? (unsafe.some(([name]) => name === response.hitlReason) ? response.hitlReason as HitlReason : undefined)
50
75
  ?? (response.rung === 'hitl' ? 'other' : undefined);
76
+ // A missing or unknown verdict does not block triage; working-backwards drafting is simply skipped for that issue.
77
+ const capability = response.capability === 'new-capability' || response.capability === 'existing-capability' ? response.capability : undefined;
51
78
  return { issue: issue.identifier, rung: reason ? 'hitl' : response.rung as Rung,
52
- ...(reason ? { hitlReason: reason } : {}), why: response.why.trim().replace(/\s+/g, ' ') };
79
+ ...(reason ? { hitlReason: reason } : {}), why: response.why.trim().replace(/\s+/g, ' '),
80
+ ...(capability ? { capability } : {}) };
53
81
  }
54
82
 
55
83
  /** One request for the current batch, never one request per issue. */
@@ -8,9 +8,13 @@ import { debug, redactSecretText } from '../debug/log.js';
8
8
  import { effectiveInstanceRoot } from '../instance/resolve.js';
9
9
  import { redactSecrets } from '../task-cards/card-store.js';
10
10
  import { directiveHash } from './directive.js';
11
- import { recordTriageOnCards, type StewardCardDeps } from './steward-cards.js';
11
+ import { notifyOutcome, type OutcomeNotifyDeps } from './outcome-notify.js';
12
+ import { recordTriageOnCards, recordLaunchOnCard, recordOutcomeOnCard, recordHitlOnCard, type StewardCardDeps } from './steward-cards.js';
13
+ import { planLaunches, launch, collectOutcomes, raiseHitl, readLaunchLedger, saveLaunchLedger, type LaunchDeps } from './launch.js';
14
+ import { InvalidWorkingBackwardsDraft, recordWorkingBackwardsOnCards } from './working-backwards.js';
15
+ import { CardStore } from '../task-cards/card-store.js';
12
16
  import { triageWithinBudget } from './triage-budget.js';
13
- import { askSteward, classifyIssue, planIssues, ruleJudgment, unsafeReason, type ClassifiedIssue, type StewardAsk } from './triage-plan.js';
17
+ import { askSteward, classifyIssue, installedCapabilities, planIssues, ruleJudgment, unsafeReason, type ClassifiedIssue, type StewardAsk } from './triage-plan.js';
14
18
  import { failureAlertText, markStageStart, markStageEnd, readFailureStreak, shouldAlert, writeFailureStreak, type FailureStreak, type StewardStage } from './failure-streak.js';
15
19
 
16
20
  export type Rung = 0 | 1 | 2 | 3 | 4 | 5 | 'hitl';
@@ -24,6 +28,7 @@ export interface TriageDecision {
24
28
  owner?: string;
25
29
  /** Carried from a previous triage without a current judgment; scheduling must not route it. */
26
30
  deferred?: true;
31
+ capability?: ClassifiedIssue['capability'];
27
32
  }
28
33
  export interface ScheduledDecision extends TriageDecision { disposition: 'now' | 'wait' | 'hitl' }
29
34
  export interface StewardSettings {
@@ -32,6 +37,9 @@ export interface StewardSettings {
32
37
  budget?: number;
33
38
  tracks?: Record<string, string>;
34
39
  alertAfterFailures?: number;
40
+ launch?: 'off' | 'shadow' | 'live';
41
+ maxParallel?: number;
42
+ podPool?: string;
35
43
  }
36
44
  export interface StewardDeps {
37
45
  fetch?: typeof fetch;
@@ -44,6 +52,10 @@ export interface StewardDeps {
44
52
  cardStore?: StewardCardDeps['store'];
45
53
  warn?: (message: string) => void;
46
54
  ask?: StewardAsk;
55
+ launchSettings?: Pick<StewardSettings, 'launch' | 'maxParallel' | 'podPool'>;
56
+ launchCommand?: LaunchDeps['command'];
57
+ spawnLaunch?: LaunchDeps['spawn'];
58
+ outcomeNotify?: Pick<OutcomeNotifyDeps, 'document' | 'message' | 'worktreeCommand' | 'loadOrigin' | 'loadOrigins'>;
47
59
  /** Internal command boundary: the current sync has already claimed this pid. */
48
60
  stageOwnedByCommand?: boolean;
49
61
  }
@@ -89,6 +101,8 @@ export async function triageIssues(issues: TriageIssue[], judge?: NonNullable<St
89
101
  const classified: ClassifiedIssue[] = [];
90
102
  const classifiedIds = new Set<string>();
91
103
  const counts = { rule: 0, classify: 0, planning: 0 };
104
+ // Inventory is shared by every classification in this batch; do not spawn --help per issue.
105
+ let inventory: ReturnType<typeof installedCapabilities> | undefined;
92
106
  const signal = (result: TriageDecision): void => {
93
107
  decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: result.issue, reason: result.why,
94
108
  purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: result.issue } });
@@ -99,7 +113,7 @@ export async function triageIssues(issues: TriageIssue[], judge?: NonNullable<St
99
113
  const byRule = ruleJudgment(issue, observedIssues ?? issues);
100
114
  if (byRule) { counts.rule++; return { ...byRule, dependsOn: [], priority: 0 }; }
101
115
  counts.classify++;
102
- const row = await classifyIssue(issue, ask);
116
+ const row = await classifyIssue(issue, ask, inventory ??= installedCapabilities());
103
117
  return { ...row, dependsOn: [], priority: 0 };
104
118
  } catch (error) {
105
119
  try { debug.log('steward.stage', 'judgment-invalid', { issue: issue.identifier, reason: redactSecrets(String(error)) }); } catch { /* fail-soft */ }
@@ -111,6 +125,7 @@ export async function triageIssues(issues: TriageIssue[], judge?: NonNullable<St
111
125
  result.why !== 'triage judgment timed out — human review' &&
112
126
  result.why !== 'triage deferred — deadline') {
113
127
  classified.push({ issue: result.issue, rung: result.rung, why: result.why,
128
+ ...(result.capability ? { capability: result.capability } : {}),
114
129
  ...(result.hitlReason ? { hitlReason: result.hitlReason } : {}) });
115
130
  classifiedIds.add(result.issue);
116
131
  } else signal(result);
@@ -232,9 +247,9 @@ function readState(path: string): { issues: Record<string, string>; digestDay?:
232
247
  catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { issues: {} }; throw error; }
233
248
  }
234
249
 
235
- /** Each command node invokes one stage. No stage can spawn, launch, merge or approve. */
250
+ /** Each command node invokes one stage. Only the launch setting permits spawn; merge and approval remain with the harness and human. */
236
251
  export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'report', deps: StewardDeps = {}): Promise<void> {
237
- const settings = stewardSettings();
252
+ const settings = { ...stewardSettings(), ...deps.launchSettings };
238
253
  if ((settings.mode ?? 'observe') !== 'observe') throw new Error('steward act mode is out of scope');
239
254
  const root = deps.root ?? effectiveInstanceRoot();
240
255
  if (stage === 'sync' && !deps.stageOwnedByCommand) {
@@ -286,7 +301,8 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
286
301
  let cursor: string | null = null;
287
302
  do {
288
303
  const data: { issues: { nodes: Array<{ identifier: string; state?: { type: string } }>; pageInfo: { hasNextPage: boolean; endCursor: string | null } } } = await linear(key,
289
- 'query($ids:[String!]!,$after:String){issues(filter:{identifier:{in:$ids}},first:250,after:$after){nodes{identifier state{type}} pageInfo{hasNextPage endCursor}}}',
304
+ // IssueFilter has no `identifier` field (HTTP 400 GRAPHQL_VALIDATION_FAILED · 10-02 measured); `id` accepts identifiers like ELA-1.
305
+ 'query($ids:[ID!]!,$after:String){issues(filter:{id:{in:$ids}},first:250,after:$after){nodes{identifier state{type}} pageInfo{hasNextPage endCursor}}}',
290
306
  { ids: batch, after: cursor }, fetchFn);
291
307
  if (!Array.isArray(data.issues?.nodes) || !data.issues.pageInfo ||
292
308
  (data.issues.pageInfo.hasNextPage && (!data.issues.pageInfo.endCursor || data.issues.pageInfo.endCursor === cursor))) {
@@ -297,7 +313,46 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
297
313
  } while (cursor);
298
314
  }
299
315
  }
300
- writeFileSync(schedule, JSON.stringify(scheduleTriage(decisions, settings, {}, completed)));
316
+ let rows = scheduleTriage(decisions, settings, {}, completed);
317
+ const issues = existsSync(snapshot) ? JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[] : [];
318
+ // L2 — a new-capability wish gets its PR/FAQ and manual drafts on the card before any launch is recorded.
319
+ // Only new-capability rows: triage sections are written first so the card reads intake → triage → prfaq → manual → launch;
320
+ // the report stage repeats both calls idempotently. A wish whose draft is invalid goes to hitl on its own —
321
+ // the other rows (and the shadow ledger) still proceed this tick.
322
+ {
323
+ const present = new Set(issues.map(issue => issue.identifier));
324
+ const wishes = rows.filter(row => present.has(row.issue) && row.capability === 'new-capability' && row.rung !== 'hitl');
325
+ const store = wishes.length ? deps.cardStore ?? new CardStore(root) : undefined;
326
+ const invalid = new Set<string>();
327
+ if (store) try {
328
+ recordTriageOnCards(wishes, issues, { root, store, now: deps.now });
329
+ for (const wish of wishes) {
330
+ try { await recordWorkingBackwardsOnCards([wish], issues, store, deps.ask ?? askSteward); }
331
+ catch (error) {
332
+ if (!(error instanceof InvalidWorkingBackwardsDraft)) throw error;
333
+ invalid.add(wish.issue);
334
+ debug.log('steward.stage', 'draft-invalid', { issue: wish.issue });
335
+ }
336
+ }
337
+ } finally { if (!deps.cardStore) store.close(); }
338
+ if (invalid.size) rows = rows.map(row => invalid.has(row.issue)
339
+ ? { ...row, rung: 'hitl' as const, disposition: 'hitl' as const, hitlReason: 'other' as const, why: 'working-backwards draft invalid' }
340
+ : row);
341
+ }
342
+ writeFileSync(schedule, JSON.stringify(rows));
343
+ const ledger = readLaunchLedger(root);
344
+ const launchDeps: LaunchDeps = { root, settings, ledger, command: deps.launchCommand, spawn: deps.spawnLaunch };
345
+ const cardDeps = { root, store: deps.cardStore, now: deps.now };
346
+ collectOutcomes(ledger, launchDeps);
347
+ const plan = planLaunches(rows, issues, ledger, settings);
348
+ for (const { issue, row } of plan.hitl) {
349
+ const entry = raiseHitl(issue, row, launchDeps);
350
+ recordHitlOnCard(issue, entry, cardDeps);
351
+ }
352
+ for (const item of plan.launches) {
353
+ const entry = launch(item, launchDeps);
354
+ recordLaunchOnCard(item.issue, entry, cardDeps);
355
+ }
301
356
  } else {
302
357
  const rows = JSON.parse(readFileSync(schedule, 'utf8')) as ScheduledDecision[];
303
358
  const path = reportPath(root);
@@ -305,15 +360,23 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
305
360
  const at = (deps.now ?? (() => new Date()))().toISOString();
306
361
  const issues = JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[];
307
362
  try {
308
- recordTriageOnCards(rows, issues, { root, store: deps.cardStore, now: deps.now });
363
+ const present = new Set(issues.map(issue => issue.identifier));
364
+ const known = rows.filter(row => present.has(row.issue));
365
+ const store = deps.cardStore ?? new CardStore(root);
366
+ try {
367
+ recordTriageOnCards(known, issues, { root, store, now: deps.now });
368
+ // Rows without a capability verdict (legacy, injected or unjudged) proceed; only drafting is skipped for them.
369
+ await recordWorkingBackwardsOnCards(known, issues, store, deps.ask ?? askSteward);
370
+ } finally { if (!deps.cardStore) store.close(); }
309
371
  } catch (error) {
310
372
  const message = `steward report: card write failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`;
311
373
  debug.log('steward.cards', 'write-failed', { error: message });
312
374
  (deps.warn ?? console.warn)(message);
375
+ if (error instanceof InvalidWorkingBackwardsDraft) throw error;
313
376
  }
314
377
  for (const row of rows) {
315
378
  const issue = issues.find(item => item.identifier === row.issue);
316
- if (!issue) throw new Error(`Missing issue ${row.issue}`);
379
+ if (!issue) continue;
317
380
  const body = `스튜어드 observe · ${row.disposition} · rung ${row.rung} · 우선순위 ${row.priority} · 의존 ${row.dependsOn.join(', ') || '없음'}${row.duplicateOf ? ` · 중복 ${row.duplicateOf}` : ''} · ${row.why}${row.hitlReason ? ` · HITL ${row.hitlReason}` : ''}`;
318
381
  const hash = directiveHash(body);
319
382
  if (state.issues[row.issue] === hash) continue;
@@ -322,6 +385,27 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
322
385
  state.issues[row.issue] = hash;
323
386
  writeFileSync(path, JSON.stringify(state));
324
387
  }
388
+ const ledger = readLaunchLedger(root);
389
+ const outcomes = collectOutcomes(ledger, { root, settings, ledger, command: deps.launchCommand, spawn: deps.spawnLaunch });
390
+ for (const outcome of outcomes) {
391
+ const issue = issues.find(item => item.identifier === outcome.issue) ??
392
+ { identifier: outcome.issue, title: outcome.title, ref: '', body: '' };
393
+ recordOutcomeOnCard(issue, outcome, { root, store: deps.cardStore, now: deps.now });
394
+ notifyOutcome(outcome, { root, ledger, shadow: settings.launch !== 'live', ...deps.outcomeNotify });
395
+ const prefix = ['telegram', 'pwa', 'tui', 'cli'].includes(outcome.source) ? `${outcome.source} · ` : '';
396
+ await (deps.sendDigest ?? sendStewardDigest)(`${prefix}${issue.title} · PR ${outcome.prNumber ?? '없음'} · ${outcome.status}`);
397
+ outcome.reported = true;
398
+ saveLaunchLedger(root, ledger);
399
+ }
400
+ const justCollected = new Set(outcomes.map(outcome => outcome.issue));
401
+ for (const outcome of Object.values(ledger.launches)) {
402
+ if ((settings.launch !== 'live' && outcome.status === 'shadow' && !outcome.notified) ||
403
+ (settings.launch === 'live' && !justCollected.has(outcome.issue) && outcome.reported && outcome.notified !== 'shadow') ||
404
+ // A finished run whose PR waits for merge still owes the wish's chat its PR link (once; failures retry).
405
+ (settings.launch === 'live' && outcome.awaitingMerge === true && !outcome.reported && outcome.notified !== 'sent')) {
406
+ notifyOutcome(outcome, { root, ledger, shadow: settings.launch !== 'live', ...deps.outcomeNotify });
407
+ }
408
+ }
325
409
  const day = at.slice(0, 10);
326
410
  if (state.digestDay !== day) {
327
411
  const send = deps.sendDigest ?? sendStewardDigest;
@@ -0,0 +1,64 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { debug } from '../debug/log.js';
3
+ import { redactSecrets } from '../task-cards/card-store.js';
4
+ import type { ScheduledDecision, TriageIssue } from './triage.js';
5
+ import type { StewardAsk } from './triage-plan.js';
6
+ import type { StewardCardStore } from './steward-cards.js';
7
+
8
+ export interface WorkingBackwardsDraft {
9
+ prfaq: string;
10
+ manual: string;
11
+ }
12
+
13
+ export class InvalidWorkingBackwardsDraft extends Error {
14
+ constructor() { super('Invalid working-backwards draft'); }
15
+ }
16
+
17
+ /** Draft promises before implementation; no document, site, or channel is written here. */
18
+ export async function draftWorkingBackwards(issue: TriageIssue, ask: StewardAsk): Promise<WorkingBackwardsDraft> {
19
+ const raw = await ask(`새 능력 소원의 구현 전 워킹 백워드 초안을 JSON 객체 {"prfaq":"...","manual":"...","signals":["판정 신호: 조건 = ...; 관측 = ...; 기대 = ..."]} 로만 써라. PR/FAQ 는 docs/marketing/TEMPLATE-prfaq-2026-09-29.md 모양의 ① 한 줄, 부제(누가), ② 문제(왜), ③ 해결(무엇), ④ 대표의 말(없는 인용은 확인 대기), ⑤ 시작하기, ⑥ 고객 FAQ(질문·답 셋 이상), ⑦ 내부 FAQ, ⑧ 판정 기준을 포함한다. 매뉴얼은 docs/marketing/TEMPLATE-manual-2026-10-02.md 모양으로 사용자가 칠 명령과 볼 화면을 제안하되 구현 전 가설이라고 명시한다. 아직 구현된 명령·화면이라고 주장하지 마라. 판정 신호 한두 문장은 PR/FAQ ⑧ 에 그대로 포함시켜라.\n소원: ${JSON.stringify(issue)}`, 'planning');
20
+ let value: unknown = raw;
21
+ if (typeof raw === 'string') {
22
+ // A draft that is not JSON is as invalid as one with missing sections — the report must stop on it too.
23
+ try { value = JSON.parse(raw.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()); }
24
+ catch { throw new InvalidWorkingBackwardsDraft(); }
25
+ }
26
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new InvalidWorkingBackwardsDraft();
27
+ const row = value as Record<string, unknown>;
28
+ const prfaq = row.prfaq;
29
+ const manual = row.manual;
30
+ const faq = typeof prfaq === 'string' ? prfaq.split('⑥ 고객 FAQ')[1]?.split('⑦ 내부 FAQ')[0] ?? '' : '';
31
+ const criteria = typeof prfaq === 'string' ? prfaq.split('⑧ 판정 기준')[1] ?? '' : '';
32
+ const answered = faq.split('\n').filter(line => /^\|\s*[^|?]+\?\s*\|\s*[^|\s][^|]*\|/.test(line));
33
+ if (typeof prfaq !== 'string' || typeof manual !== 'string' ||
34
+ !Array.isArray(row.signals) || row.signals.length < 1 || row.signals.length > 2 ||
35
+ !row.signals.every(signal => typeof signal === 'string' && /^판정 신호: 조건 = .+; 관측 = .+; 기대 = .+\.$/.test(signal) && criteria.includes(signal)) ||
36
+ !['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧'].every(section => prfaq.includes(section)) ||
37
+ answered.length < 3 ||
38
+ !/elanous\s+\S+/.test(manual) || !/화면/.test(manual)) throw new InvalidWorkingBackwardsDraft();
39
+ return { prfaq: redactSecrets(prfaq), manual: redactSecrets(manual) };
40
+ }
41
+
42
+ /** Called during the steward report, after triage and before a launch can be recorded. */
43
+ export async function recordWorkingBackwardsOnCards(
44
+ decisions: ScheduledDecision[], issues: TriageIssue[], store: StewardCardStore, ask: StewardAsk,
45
+ ): Promise<void> {
46
+ const byId = new Map(issues.map(issue => [issue.identifier, issue]));
47
+ for (const decision of decisions) {
48
+ const issue = byId.get(decision.issue);
49
+ if (!issue) throw new Error(`Missing issue ${decision.issue}`);
50
+ if (decision.capability !== 'new-capability' || decision.deferred || decision.duplicateOf || decision.rung === 'hitl') {
51
+ debug.log('steward.working-backwards', 'skipped-existing', { issueId: issue.identifier, verdict: decision.capability ?? 'unknown' });
52
+ continue;
53
+ }
54
+ const card = store.createCard({ goalId: `linear:${issue.identifier}`, title: redactSecrets(issue.title) });
55
+ if (card.sections.some(section => section.key.startsWith('prfaq:')) && card.sections.some(section => section.key.startsWith('manual:'))) continue;
56
+ if (card.sections.some(section => section.key.startsWith('launch:'))) throw new Error(`Working-backwards draft follows launch: ${issue.identifier}`);
57
+ const draft = await draftWorkingBackwards(issue, ask);
58
+ for (const [section, content] of [['prfaq', draft.prfaq], ['manual', draft.manual]] as const) {
59
+ const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;
60
+ if (!card.sections.some(item => item.key === key)) store.appendSection(card.id, { key, owner: 'steward', content });
61
+ }
62
+ debug.log('steward.working-backwards', 'drafted', { issueId: issue.identifier, verdict: decision.capability });
63
+ }
64
+ }
@@ -0,0 +1,141 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ export const STORYBOARD_ROOT = resolve(import.meta.dir, '../..');
5
+ const DIR = join(STORYBOARD_ROOT, 'storyboards');
6
+
7
+ type Text = { text: string; source: string; ref?: string };
8
+ type Shot = {
9
+ id: string; t: [number, number]; stage: string; layer?: string; persona?: string; purpose: string; subject?: string; action?: string;
10
+ camera?: { move?: string; lens_mm?: number; angle?: string; framing?: string }; lighting?: string; color?: string;
11
+ transition_in?: { type: string; intent: string }; on_screen_text?: Text[]; audio?: { vo?: string; sfx?: string; bgm?: string };
12
+ generation?: { tool: string; model?: string; prompt?: string; asset?: string }; real_or_staged: string; state: string; notes?: string;
13
+ };
14
+ /** kind: site — a site's page structure, menu and copy draft, planned like a storyboard (SITE2 · 10-02). */
15
+ type Section = { id: string; title: string; purpose: string; change: string; copy?: Text[]; notes?: string };
16
+ type Site = { domain: string; role: string; preview?: string; branch?: string; nav: Array<{ label: string; href: string }>; pages: Array<{ path: string; sections: Section[] }> };
17
+ export type Storyboard = {
18
+ id: string; kind: string; title: string; version: number; status: string; approved?: { by: string; at: string; note?: string } | null;
19
+ supersedes?: string; owner: string; directive?: string; format: { aspects: string[]; duration_s: number; fps?: number; loop?: boolean; surface?: string };
20
+ scqa?: { s?: string; c?: string; q?: string; a?: string }; principles?: string[]; layers?: Record<string, string>; shots: Shot[]; sites?: Site[];
21
+ references?: string[]; open_questions?: string[];
22
+ };
23
+
24
+ const KINDS = ['teaser', 'site-hero', 'event-demo', 'persona-scene', 'explainer', 'ad', 'site'];
25
+ const CHANGES = ['keep', 'move', 'new', 'replace', 'remove'];
26
+ const STAGES = ['hook', 'buildup', 'climax', 'transition', 'end', 'loop'];
27
+ const SOURCES = ['real-ui', 'real-cli', 'real-output', 'copy', 'example'];
28
+
29
+ export function lint(sb: Storyboard, file = ''): { errors: string[]; warnings: string[] } {
30
+ const errors: string[] = [], warnings: string[] = [];
31
+ const need = (cond: unknown, msg: string) => { if (!cond) errors.push(msg); };
32
+ need(/^[a-z0-9][a-z0-9-]{2,80}$/.test(sb.id ?? ''), 'id: 소문자·숫자·하이픈 3~80자');
33
+ need(KINDS.includes(sb.kind), `kind: ${KINDS.join('|')}`);
34
+ need(Number.isInteger(sb.version) && sb.version >= 1, 'version: 1 이상 정수');
35
+ need(['draft', 'approved', 'superseded'].includes(sb.status), 'status: draft|approved|superseded');
36
+ need(sb.owner, 'owner 필요');
37
+ if (sb.status === 'approved') need(sb.approved?.by && sb.approved?.at, '승인본은 approved.by·at 필요');
38
+ if (file && sb.kind && !file.includes(`/${sb.kind}/`)) errors.push(`파일 위치가 kind(${sb.kind}) 폴더가 아니다`);
39
+ if (sb.kind === 'site') return lintSite(sb, errors, warnings);
40
+ need(sb.format?.aspects?.length && sb.format.duration_s > 0, 'format.aspects ⊕ duration_s 필요');
41
+ if (!Array.isArray(sb.shots) || !sb.shots.length) { errors.push('shots 가 비었다'); return { errors, warnings }; }
42
+ const ids = new Set<string>();
43
+ sb.shots.forEach((s, i) => {
44
+ const at = `shots[${i}](${s.id ?? '?'})`;
45
+ need(s.id && !ids.has(s.id), `${at}: id 없음·중복`); ids.add(s.id);
46
+ need(Array.isArray(s.t) && s.t.length === 2 && s.t[0] < s.t[1], `${at}: t = [시작, 끝] 이고 시작 < 끝`);
47
+ need(STAGES.includes(s.stage), `${at}: stage ${STAGES.join('|')}`);
48
+ need(s.purpose, `${at}: purpose(이 비트의 한 가지) 필요`);
49
+ need(['real', 'staged', 'composite'].includes(s.real_or_staged), `${at}: real_or_staged 필요`);
50
+ need(['planned', 'shot', 'approved'].includes(s.state), `${at}: state planned|shot|approved`);
51
+ if (s.transition_in && !s.transition_in.intent) errors.push(`${at}: 전환에는 의도 한 줄(«그냥 컷»은 설계가 아니다)`);
52
+ for (const txt of s.on_screen_text ?? []) {
53
+ need(SOURCES.includes(txt.source), `${at}: 화면 문구 «${txt.text}» 의 source 가 ${SOURCES.join('|')} 가 아니다`);
54
+ if (txt.source?.startsWith('real-') && !txt.ref) warnings.push(`${at}: 실제 문면 «${txt.text}» 에 ref(출처) 가 없다`);
55
+ if (/\d/.test(txt.text) && txt.source === 'copy') warnings.push(`${at}: 마케팅 문구에 숫자 «${txt.text}» — 실측 근거나 «example» 로`);
56
+ }
57
+ if (s.generation?.tool === 'higgsfield' && !s.generation.prompt) warnings.push(`${at}: 힉스필드 샷에 prompt 가 없다`);
58
+ if (s.real_or_staged !== 'real' && !/연출/.test(JSON.stringify(sb))) warnings.push(`${at}: 연출 샷이 있는데 문서 어디에도 «연출 장면» 표기가 없다`);
59
+ });
60
+ const end = Math.max(...sb.shots.map((s) => s.t?.[1] ?? 0));
61
+ if (sb.format?.duration_s && Math.abs(end - sb.format.duration_s) > 0.5) warnings.push(`마지막 샷 끝(${end}s) ≠ format.duration_s(${sb.format.duration_s}s)`);
62
+ return { errors, warnings };
63
+ }
64
+
65
+ /** A site plan has pages instead of shots: every section says why it is there and what changes; copy names its source. */
66
+ function lintSite(sb: Storyboard, errors: string[], warnings: string[]): { errors: string[]; warnings: string[] } {
67
+ if (!Array.isArray(sb.sites) || !sb.sites.length) { errors.push('site 종류는 sites 가 필요하다'); return { errors, warnings }; }
68
+ for (const site of sb.sites) {
69
+ const at = `sites(${site.domain ?? '?'})`;
70
+ if (!site.domain || !site.role) errors.push(`${at}: domain ⊕ role(이 사이트가 맡는 것) 필요`);
71
+ if (!site.nav?.length) errors.push(`${at}: nav(메뉴) 필요`);
72
+ if (!site.pages?.length) errors.push(`${at}: pages 필요`);
73
+ if (!site.preview) warnings.push(`${at}: preview(미리보기 주소) 없음`);
74
+ const ids = new Set<string>();
75
+ for (const page of site.pages ?? []) for (const sec of page.sections ?? []) {
76
+ const where = `${at}${page.path} #${sec.id ?? '?'}`;
77
+ if (!sec.id || ids.has(`${page.path}#${sec.id}`)) errors.push(`${where}: id 없음·중복`);
78
+ ids.add(`${page.path}#${sec.id}`);
79
+ if (!sec.purpose) errors.push(`${where}: purpose(이 칸이 하는 한 가지) 필요`);
80
+ if (!CHANGES.includes(sec.change)) errors.push(`${where}: change ${CHANGES.join('|')}`);
81
+ for (const txt of sec.copy ?? []) {
82
+ if (!SOURCES.includes(txt.source)) errors.push(`${where}: 문구 «${txt.text}» 의 source 가 ${SOURCES.join('|')} 가 아니다`);
83
+ if (/\d/.test(txt.text) && txt.source === 'copy' && !txt.ref) warnings.push(`${where}: 마케팅 문구에 숫자 «${txt.text}» — ref(실측 근거) 를 단다`);
84
+ }
85
+ }
86
+ }
87
+ return { errors, warnings };
88
+ }
89
+
90
+ function renderSites(sb: Storyboard): string[] {
91
+ return (sb.sites ?? []).flatMap((site) => [
92
+ `## ${site.domain} — ${site.role}`, '',
93
+ ...(site.preview ? [`- 미리보기: ${site.preview}${site.branch ? ` (가지 \`${site.branch}\`)` : ''}`] : []),
94
+ `- 메뉴: ${site.nav.map((n) => `${n.label}(${n.href})`).join(' · ')}`, '',
95
+ ...site.pages.flatMap((page) => [
96
+ `### ${page.path}`, '', '| # | 칸 | 제목 | 목적 | 바뀜 | 문구(출처) | 메모 |', '|---|---|---|---|---|---|---|',
97
+ ...page.sections.map((sec, i) => `| ${i + 1} | ${md(sec.id)} | ${md(sec.title)} | ${md(sec.purpose)} | ${sec.change} | ${md((sec.copy ?? []).map((x) => `«${x.text}»(${x.source}${x.ref ? ` · ${x.ref}` : ''})`).join(' / '))} | ${md(sec.notes)} |`),
98
+ '',
99
+ ]),
100
+ ]);
101
+ }
102
+
103
+ const md = (v: unknown) => String(v ?? '').replace(/\|/g, '\\|').replace(/\n+/g, ' ');
104
+ export function render(sb: Storyboard, file: string): string {
105
+ const head = [
106
+ `# STORYBOARD — ${sb.title} (v${sb.version} · ${sb.status}${sb.approved ? ` · 승인 ${sb.approved.by} ${sb.approved.at}` : ''})`,
107
+ '',
108
+ `> ⛔ 자동 생성 — 손으로 고치지 않는다. 정본 = \`${file}\` · 다시 뽑기 = \`bun scripts/storyboard/storyboard.ts render ${file}\`.`,
109
+ `> 종류 ${sb.kind} · 소유 ${sb.owner}${sb.format ? ` · ${sb.format.aspects.join(' · ')} · ${sb.format.duration_s}초${sb.format.loop ? ' · 반복' : ''}${sb.format.surface ? ` · ${sb.format.surface}` : ''}` : ''}${sb.supersedes ? ` · 앞 판 ${sb.supersedes}` : ''}`,
110
+ ...(sb.directive ? [`> 지시: ${sb.directive}`] : []),
111
+ '',
112
+ ];
113
+ const scqa = sb.scqa ? ['## SCQA', ...(['s', 'c', 'q', 'a'] as const).filter((k) => sb.scqa?.[k]).map((k) => `- **${k.toUpperCase()}** — ${sb.scqa![k]}`), ''] : [];
114
+ const pr = sb.principles?.length ? ['## 원칙', ...sb.principles.map((p, i) => `${i + 1}. ${p}`), ''] : [];
115
+ const layers = sb.layers ? ['## 층', ...Object.entries(sb.layers).map(([k, v]) => `- **${k}** — ${v}`), ''] : [];
116
+ const table = sb.kind === 'site' ? renderSites(sb) : [
117
+ '## 샷 구조표',
118
+ '| # | 초 | 단계 | 층·인물 | 목적 | 화면(피사체·행동) | 카메라 | 빛·색 | 전환(의도) | 화면 문구(출처) | 소리 | 만들기 | 실물/연출 · 상태 |',
119
+ '|---|---|---|---|---|---|---|---|---|---|---|---|---|',
120
+ ...sb.shots.map((s) => `| ${md(s.id)} | ${s.t[0]}–${s.t[1]} | ${s.stage} | ${md([s.layer, s.persona].filter(Boolean).join(' · '))} | ${md(s.purpose)} | ${md([s.subject, s.action].filter(Boolean).join(' — '))} | ${md(s.camera ? [s.camera.move, s.camera.lens_mm ? `${s.camera.lens_mm}mm` : '', s.camera.angle, s.camera.framing].filter(Boolean).join(' · ') : '')} | ${md([s.lighting, s.color].filter(Boolean).join(' · '))} | ${md(s.transition_in ? `${s.transition_in.type} — ${s.transition_in.intent}` : '')} | ${md((s.on_screen_text ?? []).map((x) => `«${x.text}»(${x.source}${x.ref ? ` · ${x.ref}` : ''})`).join(' / '))} | ${md(s.audio ? [s.audio.vo && `VO ${s.audio.vo}`, s.audio.sfx && `SFX ${s.audio.sfx}`, s.audio.bgm && `BGM ${s.audio.bgm}`].filter(Boolean).join(' · ') : '')} | ${md(s.generation ? [s.generation.tool, s.generation.model].filter(Boolean).join(' · ') : '')} | ${s.real_or_staged} · ${s.state} |`),
121
+ '',
122
+ ];
123
+ const shots = sb.shots ?? [];
124
+ const prompts = shots.filter((s) => s.generation?.prompt);
125
+ const pblock = prompts.length ? ['## 생성 프롬프트', ...prompts.flatMap((s) => [`### ${s.id} · ${s.generation!.tool}${s.generation!.model ? ` · ${s.generation!.model}` : ''}`, '```', s.generation!.prompt!.trim(), '```', ...(s.generation!.asset ? [`결과: \`${s.generation!.asset}\``] : []), ''])] : [];
126
+ const notes = shots.filter((s) => s.notes).map((s) => `- ${s.id}: ${s.notes}`);
127
+ const tail = [
128
+ ...(notes.length ? ['## 샷 메모', ...notes, ''] : []),
129
+ ...(sb.open_questions?.length ? ['## 열린 물음', ...sb.open_questions.map((q) => `- ${q}`), ''] : []),
130
+ ...(sb.references?.length ? ['## 참고', ...sb.references.map((r) => `- ${r}`), ''] : []),
131
+ ];
132
+ return [...head, ...scqa, ...pr, ...layers, ...table, ...pblock, ...tail].join('\n');
133
+ }
134
+
135
+ export function files(args: string[]): string[] {
136
+ if (args.length) return args.map((a) => resolve(a));
137
+ const out: string[] = [];
138
+ const walk = (d: string) => { for (const f of readdirSync(d)) { const p = join(d, f); if (statSync(p).isDirectory()) walk(p); else if (/\.ya?ml$/.test(f) && !f.startsWith('_')) out.push(p); } };
139
+ walk(DIR);
140
+ return out.sort();
141
+ }
@@ -38,10 +38,10 @@ type CardEvent =
38
38
  * A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
39
39
  * task's life (e.g. workspace diff updates), and the same full key is written once. `incidents` is open to any loop. */
40
40
  export const SECTION_OWNERS: Readonly<Record<string, string | null>> = {
41
- intake: 'steward', triage: 'steward', launch: 'steward', outcome: 'steward',
41
+ intake: 'steward', triage: 'steward', prfaq: 'steward', manual: 'steward', launch: 'steward', outcome: 'steward', hitl: 'steward',
42
42
  gates: 'execution-loop', relations: 'execution-loop', memory: 'execution-loop',
43
43
  workspace: 'executor', run: 'executor',
44
- landing: 'landing-loop', release: 'release-loop',
44
+ landing: 'landing-loop', release: 'release-loop', 'test-diet': 'test-diet',
45
45
  incidents: null,
46
46
  };
47
47