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
@@ -0,0 +1,124 @@
1
+ // GET /v1/ops/seats?date=YYYY-MM-DD — one row per seat: latest channel line, PRs landed that day, red items, open decisions.
2
+ // A source that could not be read is null, never 0 («못 읽음» ≠ «없음»). Contract: 내부 문서 `DESIGN-ops3-seat-board-and-public-demo-2026-10-02` §1.
3
+ import { debug, redactSecretText } from '../../debug/log.js';
4
+ import { parseOwner, type ChecklistItem } from '../../release-loop/checklist.js';
5
+ import { canonicalSeatId } from '../../msg/msg-store.js';
6
+
7
+ export const SEATS = [
8
+ { seat: 'OP', role: 'COO' }, { seat: 'TC', role: 'CTO' }, { seat: 'MK', role: 'CMO' }, { seat: 'UX', role: 'CXO' },
9
+ ] as const;
10
+ export const SEATS_CACHE_MS = 60_000;
11
+
12
+ export interface ChannelComment { body: string; createdAt: string; url?: string }
13
+ export type ChannelSource = 'ledger' | 'github' | 'unreadable' | 'unknown';
14
+ export type ChannelResult = ChannelComment[] | {
15
+ comments: ChannelComment[];
16
+ source: 'ledger' | 'github';
17
+ seatSources?: Partial<Record<(typeof SEATS)[number]['seat'], ChannelSource>>;
18
+ } | null;
19
+ export interface MergedPr { number: number; title: string; body: string; mergedAt: string }
20
+ export interface SeatsSources {
21
+ /** Coordination-channel comments created on `date` (KST) or later; null when unreadable. */
22
+ channel(date: string): Promise<ChannelResult>;
23
+ /** PRs merged on `date` (KST); null when unreadable. */
24
+ merged(date: string): Promise<MergedPr[] | null>;
25
+ /** Checklist items of the version being worked on (counts, red) and of every version known (id → owner). */
26
+ checklist(): { current: ChecklistItem[]; all: ChecklistItem[] } | null;
27
+ /** `raisedBy.agent` of every open decision. */
28
+ openDecisionRaisers(): string[] | null;
29
+ }
30
+
31
+ export interface SeatRow {
32
+ seat: string; role: string;
33
+ channelSource: ChannelSource;
34
+ now: { text: string; at: string } | null;
35
+ landed: Array<{ pr: number; title: string; at: string; checklistId: string | null }> | null;
36
+ blocked: Array<{ id: string; title: string; status: 'red' }> | null;
37
+ pendingDecisions: number | null;
38
+ checklist: { green: number; yellow: number; red: number; done: number } | null;
39
+ }
40
+
41
+ function seatOf(value: string | undefined): string | null {
42
+ if (!value) return null;
43
+ try { return canonicalSeatId(value.includes('/') ? parseOwner(value).seat : value); } catch { return null; }
44
+ }
45
+
46
+ const clip = (text: string, max: number) => redactSecretText(text).slice(0, max);
47
+
48
+ /** First checklist id that appears as a whole word in the title, else in the body. */
49
+ function checklistIdOf(pr: MergedPr, ids: string[]): string | null {
50
+ for (const text of [pr.title, pr.body]) {
51
+ const found = ids.map((id) => ({ id, at: text.search(new RegExp(`(?<![A-Za-z0-9])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9])`)) }))
52
+ .filter((hit) => hit.at >= 0).sort((a, b) => a.at - b.at || b.id.length - a.id.length);
53
+ if (found.length) return found[0]!.id;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ export async function buildSeatsBoard(date: string, sources: SeatsSources): Promise<{ date: string; seats: SeatRow[] }> {
59
+ const [channelResult, merged] = await Promise.all([
60
+ sources.channel(date).catch(() => null), sources.merged(date).catch(() => null),
61
+ ]);
62
+ const channel = Array.isArray(channelResult) ? channelResult : channelResult?.comments ?? null;
63
+ const channelSource: ChannelSource = channelResult === null ? 'unreadable' : 'unknown';
64
+ let checklist: ReturnType<SeatsSources['checklist']> = null;
65
+ try { checklist = sources.checklist(); } catch { checklist = null; }
66
+ let raisers: string[] | null = null;
67
+ try { raisers = sources.openDecisionRaisers(); } catch { raisers = null; }
68
+
69
+ const owners = new Map<string, string>();
70
+ for (const item of checklist?.all ?? []) { const seat = seatOf(item.owner); if (seat) owners.set(item.id, seat); }
71
+ const ids = [...owners.keys()];
72
+ const landedBySeat = new Map<string, NonNullable<SeatRow['landed']>>();
73
+ for (const pr of merged ?? []) {
74
+ const id = checklistIdOf(pr, ids);
75
+ const seat = id ? owners.get(id) : undefined;
76
+ if (!seat) continue; // ⛔ not by commit author — every seat lands through the same account.
77
+ landedBySeat.set(seat, [...(landedBySeat.get(seat) ?? []), { pr: pr.number, title: clip(pr.title, 80), at: pr.mergedAt, checklistId: id }]);
78
+ }
79
+
80
+ const seats = SEATS.map(({ seat, role }): SeatRow => {
81
+ const latest = channel?.filter((c) => c.body.trimStart().startsWith(`**[${seat}]**`)).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
82
+ const mine = checklist?.current.filter((item) => seatOf(item.owner) === seat);
83
+ return {
84
+ seat, role, channelSource: channelResult && !Array.isArray(channelResult)
85
+ ? channelResult.seatSources?.[seat] ?? channelSource : channelSource,
86
+ now: latest ? { text: clip(latest.body.trimStart().split('\n')[0]!, 120), at: latest.createdAt } : null,
87
+ landed: merged === null || checklist === null ? null : (landedBySeat.get(seat) ?? []).sort((a, b) => a.at.localeCompare(b.at)),
88
+ blocked: mine ? mine.filter((item) => item.status === 'red').map((item) => ({ id: item.id, title: clip(item.title, 80), status: 'red' as const })) : null,
89
+ pendingDecisions: raisers === null ? null : raisers.filter((agent) => seatOf(agent) === seat).length,
90
+ checklist: mine ? {
91
+ green: mine.filter((i) => i.status === 'green').length, yellow: mine.filter((i) => i.status === 'yellow').length,
92
+ red: mine.filter((i) => i.status === 'red').length, done: mine.filter((i) => i.status === 'done').length,
93
+ } : null,
94
+ };
95
+ });
96
+ debug.log('ops.seats', 'built', {
97
+ date, channel: channel === null ? 'unreadable' : channel.length, merged: merged === null ? 'unreadable' : merged.length,
98
+ checklist: checklist !== null, decisions: raisers !== null,
99
+ });
100
+ return { date, seats };
101
+ }
102
+
103
+ /** KST calendar day → [start, end) in UTC ISO. */
104
+ export function kstDayRange(date: string): { start: string; end: string } {
105
+ const start = new Date(`${date}T00:00:00+09:00`);
106
+ return { start: start.toISOString(), end: new Date(start.getTime() + 86_400_000).toISOString() };
107
+ }
108
+
109
+ export function todayKst(now = new Date()): string {
110
+ return new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul' }).format(now);
111
+ }
112
+
113
+ /** One snapshot per date for 60 s; concurrent requests share one build. */
114
+ export function createSeatsCache(sources: SeatsSources, now: () => number = Date.now, ttlMs = SEATS_CACHE_MS) {
115
+ const slots = new Map<string, { at: number; value: Promise<{ date: string; seats: SeatRow[] }> }>();
116
+ return (date: string) => {
117
+ const slot = slots.get(date);
118
+ if (slot && now() - slot.at < ttlMs) return slot.value;
119
+ const value = buildSeatsBoard(date, sources);
120
+ slots.set(date, { at: now(), value });
121
+ value.catch(() => slots.delete(date));
122
+ return value;
123
+ };
124
+ }
@@ -36,6 +36,12 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
36
36
  path: '/v1/push/vapid-public-key',
37
37
  why: 'PWA fetches the VAPID public key before a bearer is pasted',
38
38
  },
39
+ {
40
+ method: 'GET',
41
+ match: 'exact',
42
+ path: '/v1/me',
43
+ why: 'operator signal only (draw the ops area or not); false unless operator.enabled, no other data',
44
+ },
39
45
  {
40
46
  method: 'GET',
41
47
  match: 'exact',
@@ -56,6 +62,13 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
56
62
  selfVerified: true,
57
63
  why: 'Pod GitHub App credential relay verifies its own run-scoped gh-credential token before minting a repository-scoped installation token',
58
64
  },
65
+ {
66
+ method: 'POST',
67
+ match: 'exact',
68
+ path: '/v1/setup/claim',
69
+ selfVerified: true,
70
+ why: 'one-use setup link is verified by the claim handler without an owner bearer',
71
+ },
59
72
  {
60
73
  method: 'GET',
61
74
  match: 'prefix',
@@ -76,7 +89,7 @@ export function isPublicRoute(
76
89
  ): boolean {
77
90
  for (const route of PUBLIC_ROUTES) {
78
91
  if (route.method !== method) continue;
79
- if (route.path.startsWith('/v1/setup/') && !ctx.setupMode) continue;
92
+ if (route.path.startsWith('/v1/setup/') && route.path !== '/v1/setup/claim' && !ctx.setupMode) continue;
80
93
  if (route.match === 'exact') {
81
94
  if (pathname === route.path) return true;
82
95
  } else if (pathname === route.path || pathname.startsWith(route.path)) {
@@ -1,7 +1,12 @@
1
1
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { dirname, join } from 'node:path';
3
+ import { basename, dirname, join } from 'node:path';
4
4
  import { debug } from '../../debug/log.js';
5
+ import { resolveAttachmentPath } from '../../boot/attachment-store.js';
6
+ import { runGh } from '../../decisions/decision-cards.js';
7
+ import { openMsgStore, canonicalSeatId } from '../../msg/msg-store.js';
8
+ import { dispatchCeoTask, type CeoCommandDeps } from '../../seat-dispatch/ceo-commands.js';
9
+ import { getUserConfig } from '../../user-config.js';
5
10
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
6
11
  import { submitIntakeWork } from '../../intake-plane/submit-intake-work.js';
7
12
  import { parseSeatAddress, resolveSeat } from '../../seat-address/seat-address.js';
@@ -16,6 +21,9 @@ interface SeatRequestItem {
16
21
  text: string;
17
22
  queuedAt: string;
18
23
  status: 'queued';
24
+ attachments?: string[];
25
+ channel?: 'posted' | 'failed' | 'unknown';
26
+ channelError?: string;
19
27
  }
20
28
 
21
29
  interface PendingRequest {
@@ -25,6 +33,7 @@ interface PendingRequest {
25
33
  text: string;
26
34
  queuedAt: string;
27
35
  ref: string;
36
+ attachments?: string[];
28
37
  }
29
38
 
30
39
  interface RejectedRequest extends Omit<PendingRequest, 'status'> {
@@ -38,10 +47,17 @@ export interface SeatRequestsDeps {
38
47
  submit?: typeof submitIntakeWork;
39
48
  now?: () => string;
40
49
  append?: (path: string, entry: RecordEntry) => void;
50
+ resolveAttachment?: typeof resolveAttachmentPath;
51
+ dispatch?: typeof dispatchCeoTask;
52
+ ceoDeps?: () => CeoCommandDeps;
41
53
  }
42
54
 
43
55
  const seats = trackData.tracks.filter((entry): entry is typeof entry & { title: string } => 'title' in entry)
44
56
  .map(({ id, title }) => ({ id, title }));
57
+
58
+ function requestSeat(address: string) {
59
+ return resolveSeat(address) ?? trackData.tracks.find((entry) => entry.id.toLowerCase() === address.replace(/^@/, '').toLowerCase());
60
+ }
45
61
  const inFlight = new Map<string, { payload: string; task: Promise<Response> }>();
46
62
 
47
63
  function unknownSeat(): Response {
@@ -140,7 +156,28 @@ function persistOutcome(path: string, entry: RecordEntry, write: typeof append):
140
156
  }
141
157
 
142
158
  function receipt(item: SeatRequestItem): Response {
143
- return jsonResponse({ receiptId: item.receiptId, seat: item.seat, queuedAt: item.queuedAt }, 202);
159
+ return jsonResponse({ receiptId: item.receiptId, seat: item.seat, queuedAt: item.queuedAt,
160
+ // Only a CEO-seat dispatch knows whether the channel line went out; anything else is reported as unknown.
161
+ attachments: item.attachments?.length ?? 0, channel: item.channel ?? 'unknown',
162
+ ...(item.channelError ? { channelError: item.channelError } : {}) }, 202);
163
+ }
164
+
165
+ function defaultCeoDeps(): CeoCommandDeps {
166
+ const cfg = getUserConfig();
167
+ const replyTarget = (cfg.raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
168
+ return { ownerId: null, replyTarget: typeof replyTarget === 'string' ? replyTarget : null, runGh,
169
+ append: (message) => { const store = openMsgStore(); try { return store.append(message); } finally { store.close(); } },
170
+ hasMessage: (seat, ref) => {
171
+ const store = openMsgStore();
172
+ try {
173
+ return store.db.query("SELECT 1 FROM msg_messages WHERE recipient = ? AND kind = 'ceo-task' AND body LIKE ? LIMIT 1")
174
+ .get(seat, `%\n요청: ${ref}`) !== null;
175
+ } finally { store.close(); }
176
+ } };
177
+ }
178
+
179
+ function canonicalSeatIdSafe(id: string): string {
180
+ try { return canonicalSeatId(id); } catch { return id; }
144
181
  }
145
182
 
146
183
  function unavailable(): Response {
@@ -153,7 +190,7 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
153
190
  if (req.method === 'GET') {
154
191
  const params = new URL(req.url).searchParams;
155
192
  const requestedSeat = params.get('seat');
156
- const seat = requestedSeat ? resolveSeat(requestedSeat) : undefined;
193
+ const seat = requestedSeat ? requestSeat(requestedSeat) : undefined;
157
194
  if (requestedSeat && !seat) return unknownSeat();
158
195
  const rawLimit = params.get('limit');
159
196
  const limit = rawLimit === null ? 20 : Number(rawLimit);
@@ -183,18 +220,30 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
183
220
  const firstAddress = parsed && input.text.startsWith(prefix)
184
221
  && (input.text.length === prefix.length || /[ \t\r\n]/.test(input.text[prefix.length]!)) ? parsed : null;
185
222
  const address = input.seat ?? (firstAddress?.seats.length === 1 ? firstAddress.seats[0] : undefined);
186
- const seat = address ? resolveSeat(address) : undefined;
223
+ const seat = address ? requestSeat(address) : undefined;
187
224
  if (!seat) return unknownSeat();
188
225
  const text = input.seat === undefined ? firstAddress!.body.trim() : input.text.trim();
189
226
  if (!text) {
190
227
  debug.log('seat-address.pwa', 'rejected', { reason: 'empty-text', seat: seat.id });
191
228
  return jsonResponse({ error: 'bad_request' }, 400);
192
229
  }
230
+ if (input.attachments !== undefined && (!Array.isArray(input.attachments) || input.attachments.length > 4
231
+ || input.attachments.some((attachment: unknown) => !attachment || typeof attachment !== 'object' || Array.isArray(attachment)
232
+ || typeof (attachment as { id?: unknown }).id !== 'string' || !(attachment as { id: string }).id.trim()))) {
233
+ debug.log('seat.dispatch', 'refused', { seat: seat.id, reason: 'invalid-attachments' });
234
+ return jsonResponse({ error: 'bad_request' }, 400);
235
+ }
236
+ const attachmentIds = (input.attachments as Array<{ id: string }> | undefined)?.map(({ id }) => id) ?? [];
237
+ // Intake (non-CEO seats) carries text only — refuse attachments there instead of accepting and dropping them.
238
+ if (attachmentIds.length && !['OP', 'TC', 'MK', 'UX'].includes(canonicalSeatIdSafe(seat.id))) {
239
+ debug.log('seat.dispatch', 'refused', { seat: seat.id, reason: 'attachments-unsupported' });
240
+ return jsonResponse({ error: 'attachments-unsupported', seat: seat.id }, 400);
241
+ }
193
242
  debug.log('seat-address.pwa', 'parsed', { seat: seat.id, textLength: text.length });
194
243
  const now = (deps.now ?? (() => new Date().toISOString()))();
195
244
  const header = req.headers.get('idempotency-key');
196
245
  if (header !== null && (!header.trim() || header.length > 200)) return jsonResponse({ error: 'bad_request' }, 400);
197
- const payload = `${seat.id}\0${text}`;
246
+ const payload = JSON.stringify([seat.id, text, attachmentIds]);
198
247
  const key = header === null ? createHash('sha256').update(randomUUID()).digest('hex')
199
248
  : createHash('sha256').update(`client\0${header}`).digest('hex');
200
249
  const lock = `${path}\0${key}`;
@@ -213,11 +262,23 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
213
262
  try { existing = records(path).get(key); }
214
263
  catch { return unavailable(); }
215
264
  if (existing) {
216
- if (existing.seat !== seat.id || existing.text !== text) return jsonResponse({ error: 'idempotency-conflict' }, 409);
265
+ if (existing.seat !== seat.id || existing.text !== text
266
+ || JSON.stringify(existing.attachments ?? []) !== JSON.stringify(attachmentIds)) return jsonResponse({ error: 'idempotency-conflict' }, 409);
217
267
  if (existing.status === 'queued') return receipt(existing);
218
268
  }
269
+ const attachments: Array<{ name: string; path: string }> = [];
270
+ for (const id of attachmentIds) {
271
+ const resolved = (deps.resolveAttachment ?? resolveAttachmentPath)(id);
272
+ if (!resolved) {
273
+ debug.log('seat.dispatch', 'refused', { seat: seat.id, reason: 'unknown-attachment' });
274
+ return jsonResponse({ error: 'unknown-attachment', id }, 400);
275
+ }
276
+ const metadata = (input.attachments as Array<{ name?: unknown }>)[attachments.length];
277
+ attachments.push({ name: typeof metadata?.name === 'string' && metadata.name.trim() ? metadata.name : basename(resolved), path: resolved });
278
+ }
219
279
  const intent: PendingRequest = existing?.status === 'pending'
220
- ? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}` };
280
+ ? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}`,
281
+ ...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
221
282
  if (existing?.status !== 'pending') {
222
283
  try {
223
284
  (deps.append ?? append)(path, intent);
@@ -227,6 +288,24 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
227
288
  }
228
289
  } catch { return unavailable(); }
229
290
  }
291
+ const ceoSeat = canonicalSeatId(seat.id);
292
+ if (['OP', 'TC', 'MK', 'UX'].includes(ceoSeat)) {
293
+ let result: Awaited<ReturnType<typeof dispatchCeoTask>>;
294
+ try {
295
+ result = await (deps.dispatch ?? dispatchCeoTask)(ceoSeat, text, (deps.ceoDeps ?? defaultCeoDeps)(), { attachments, via: 'pwa', ref: intent.ref });
296
+ } catch {
297
+ debug.log('seat.dispatch', 'refused', { seat: seat.id, reason: 'dispatch-failed' });
298
+ return unavailable();
299
+ }
300
+ const item: RecordEntry & SeatRequestItem = {
301
+ key, receiptId: intent.ref, seat: seat.id, text, queuedAt: intent.queuedAt, status: 'queued',
302
+ ...(attachmentIds.length ? { attachments: attachmentIds } : {}), channel: result.channel,
303
+ ...(result.channelError ? { channelError: result.channelError } : {}),
304
+ };
305
+ try { persistOutcome(path, item, deps.append ?? append); }
306
+ catch { return unavailable(); }
307
+ return receipt(item);
308
+ }
230
309
  let result: Awaited<ReturnType<typeof submitIntakeWork>>;
231
310
  try {
232
311
  result = await (deps.submit ?? submitIntakeWork)({
@@ -241,7 +320,8 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
241
320
  catch { return unavailable(); }
242
321
  return unavailable();
243
322
  }
244
- const item: RecordEntry & SeatRequestItem = { key, receiptId: result.acceptanceId, seat: seat.id, text, queuedAt: intent.queuedAt, status: 'queued' };
323
+ const item: RecordEntry & SeatRequestItem = { key, receiptId: result.acceptanceId, seat: seat.id, text, queuedAt: intent.queuedAt,
324
+ status: 'queued', ...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
245
325
  try { persistOutcome(path, item, deps.append ?? append); }
246
326
  catch { return unavailable(); }
247
327
  debug.log('seat-address.pwa', 'enqueued', { seat: seat.id, receiptId: item.receiptId });
@@ -18,6 +18,11 @@ import {
18
18
  isHarnessSessionOrigin,
19
19
  type SessionMeta,
20
20
  } from '../../session/index.js';
21
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { randomUUID } from 'node:crypto';
24
+ import { getProject } from '../../project/project-store.js';
25
+ import { sessionRoot } from '../../session/index.js';
21
26
  import { checkAuth, type MetaApiOpts } from './meta-api.js';
22
27
  import { jsonResponse } from './http-server.js';
23
28
 
@@ -32,6 +37,7 @@ export interface SessionStoreCard {
32
37
  * `harness` = 하니스 자식이 만든 세션. 기본 목록에서 숨기고 includeHarness=1 로 포함. */
33
38
  origin?: string;
34
39
  sourceKind?: string;
40
+ projectId?: string;
35
41
  createdAt: string;
36
42
  updatedAt: string;
37
43
  messageCount: number;
@@ -50,6 +56,7 @@ function toCard(m: SessionMeta, preview: string, nowMs: number): SessionStoreCar
50
56
  source: m.source,
51
57
  ...(m.origin ? { origin: m.origin } : {}),
52
58
  ...(m.sourceKind ? { sourceKind: m.sourceKind } : {}),
59
+ ...(m.projectId !== undefined ? { projectId: m.projectId } : {}),
53
60
  createdAt: m.createdAt,
54
61
  updatedAt: m.updatedAt,
55
62
  messageCount: m.messageCount,
@@ -82,11 +89,13 @@ export function handleSessionsStoreList(req: Request, opts: MetaApiOpts): Respon
82
89
  // 하니스 자식 세션(origin:'harness')은 기본 숨김 — includeHarness=1 로 포함.
83
90
  // 신분이 없는 옛 세션은 사람 대화로 본다(숨기지 않음). 제목 문면으로 판정하지 않는다.
84
91
  const includeHarness = url.searchParams.get('includeHarness') === '1';
92
+ const projectId = url.searchParams.get('projectId');
85
93
  const limit = Math.min(Math.max(Number(url.searchParams.get('limit')) || 100, 1), 300);
86
94
  const nowMs = Date.now();
87
95
 
88
96
  let metas = listSessions({
89
97
  limit: 600,
98
+ ...(projectId !== null ? { projectId } : {}),
90
99
  ...(includeOperational ? {} : { excludeSourceKinds: ['scheduled'] }),
91
100
  ...(includeHarness ? {} : { excludeOrigins: [HARNESS_SESSION_ORIGIN] }),
92
101
  }); // 이미 updatedAt DESC
@@ -157,6 +166,60 @@ export async function handleSessionsStoreFork(
157
166
  );
158
167
  }
159
168
 
169
+ /** PATCH /v1/sessions/store/:id — only project membership; leave transcript and other meta untouched. */
170
+ export async function handleSessionsStorePatch(req: Request, id: string, opts: MetaApiOpts): Promise<Response> {
171
+ if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
172
+ if (!id || id.includes('/') || id.includes('\\') || id.includes('..')) {
173
+ return jsonResponse({ ok: false, error: 'invalid_session_id' }, 400);
174
+ }
175
+ let body: unknown;
176
+ try { body = await req.json(); }
177
+ catch { return jsonResponse({ ok: false, error: 'invalid_json' }, 400); }
178
+ if (!body || typeof body !== 'object' || Array.isArray(body)
179
+ || !Object.hasOwn(body, 'projectId')
180
+ || Object.keys(body).some((key) => key !== 'projectId')) {
181
+ return jsonResponse({ ok: false, error: 'invalid_project_id' }, 400);
182
+ }
183
+ const projectId = (body as { projectId: unknown }).projectId;
184
+ if (projectId !== null && (typeof projectId !== 'string' || !getProject(projectId))) {
185
+ return jsonResponse({ ok: false, error: 'invalid_project_id' }, 400);
186
+ }
187
+
188
+ const root = sessionRoot();
189
+ const lock = join(root, '.index.lock');
190
+ const deadline = Date.now() + 10_000;
191
+ for (;;) {
192
+ try { mkdirSync(lock); break; }
193
+ catch {
194
+ if (!existsSync(lock)) continue;
195
+ try {
196
+ if (Date.now() - statSync(lock).mtimeMs > 30_000) { rmdirSync(lock); continue; }
197
+ } catch { continue; }
198
+ if (Date.now() >= deadline) throw new Error(`timed out acquiring lock ${lock}`);
199
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
200
+ }
201
+ }
202
+ try {
203
+ const path = join(root, 'index.json');
204
+ const entries = JSON.parse(readFileSync(path, 'utf8')) as SessionMeta[];
205
+ const pos = entries.findIndex((meta) => meta.id === id);
206
+ if (pos < 0) return jsonResponse({ ok: false, error: 'not_found' }, 404);
207
+ const meta = { ...entries[pos] };
208
+ if (projectId === null) delete meta.projectId;
209
+ else meta.projectId = projectId;
210
+ entries[pos] = meta;
211
+ const temp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
212
+ writeFileSync(temp, JSON.stringify(entries, null, 2) + '\n', 'utf8');
213
+ renameSync(temp, path);
214
+ return jsonResponse({ ok: true, meta }, 200);
215
+ } catch (error) {
216
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return jsonResponse({ ok: false, error: 'not_found' }, 404);
217
+ throw error;
218
+ } finally {
219
+ rmdirSync(lock);
220
+ }
221
+ }
222
+
160
223
  /** DELETE /v1/sessions/store/:id — on-disk 세션 삭제(index.json + <id>.jsonl 제거).
161
224
  * 파괴적·복구불가. in-flight turn abort + in-memory history forget 도 fail-soft 로 동반. */
162
225
  export function handleSessionsStoreDelete(req: Request, id: string, opts: MetaApiOpts): Response {
@@ -0,0 +1,21 @@
1
+ import { claimSetupLinkToken } from '../../auth/setup-link-tokens.js';
2
+ import { jsonResponse } from './json-response.js';
3
+
4
+ /** A setup link is its own credential; this handler never reads the owner bearer. */
5
+ export async function handleSetupClaim(req: Request): Promise<Response> {
6
+ let body: unknown;
7
+ try {
8
+ body = await req.json();
9
+ } catch {
10
+ return jsonResponse({ error: 'bad_request' }, 400);
11
+ }
12
+ if (!body || typeof body !== 'object' || Array.isArray(body)
13
+ || typeof (body as Record<string, unknown>).t !== 'string'
14
+ || !(body as { t: string }).t.trim()) {
15
+ return jsonResponse({ error: 'bad_request' }, 400);
16
+ }
17
+
18
+ const result = claimSetupLinkToken((body as { t: string }).t);
19
+ if (!result.ok) return jsonResponse({ error: result.reason }, 401);
20
+ return jsonResponse({ scope: 'setup', bearer: result.bearer, expiresAt: result.expiresAt });
21
+ }
@@ -0,0 +1,160 @@
1
+ import { debug } from '../../debug/log.js';
2
+ import { loginWithCodexBrowser } from '../../oauth/codex-browser-login.js';
3
+ import {
4
+ CODEX_DEVICE_LOGIN_URL,
5
+ exchangeDeviceAuthCode,
6
+ pollDeviceCode,
7
+ requestDeviceCode,
8
+ } from '../../oauth/codex.js';
9
+ import { expiresAtFromSeconds, saveTokens } from '../../oauth/store.js';
10
+ import { getUserConfig, reloadUserConfig, saveUserConfig } from '../../user-config.js';
11
+
12
+ type Mode = 'browser' | 'device';
13
+ type CodexLoginStatus =
14
+ | { state: 'idle' }
15
+ | { state: 'pending'; mode: Mode; authorizeUrl?: string; userCode?: string; verificationUrl?: string }
16
+ | { state: 'ok'; mode: Mode }
17
+ | { state: 'error'; mode: Mode; error: string };
18
+
19
+ const TIMEOUT_MS = 5 * 60_000;
20
+ const SAFE_ERRORS = [
21
+ 'timeout', 'authorization-denied', 'browser-unavailable', 'port-unavailable',
22
+ 'token-exchange', 'device-code-failed', 'device-poll-failed', 'config-save-failed',
23
+ ] as const;
24
+
25
+ type Failure = typeof SAFE_ERRORS[number];
26
+ function reason(error: unknown, mode: Mode): Failure {
27
+ const message = error instanceof Error ? error.message : '';
28
+ for (const value of SAFE_ERRORS) if (message === value || message === `codex browser login failed: ${value}`) return value;
29
+ return mode === 'browser' ? 'browser-unavailable' : 'device-poll-failed';
30
+ }
31
+
32
+ interface LoginDeps {
33
+ browser: (openBrowser: (url: string) => boolean) => Promise<unknown>;
34
+ device: (ready: (userCode: string, verificationUrl: string) => void) => Promise<unknown>;
35
+ saveProvider: () => void;
36
+ observe: (event: 'started' | 'ok' | 'error', mode: Mode) => void;
37
+ }
38
+
39
+ async function deviceLogin(ready: (userCode: string, verificationUrl: string) => void): Promise<void> {
40
+ const controller = new AbortController();
41
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
42
+ const fetchImpl = ((input, init) => fetch(input, { ...init, signal: controller.signal })) as typeof fetch;
43
+ try {
44
+ let code: Awaited<ReturnType<typeof requestDeviceCode>>;
45
+ try { code = await requestDeviceCode({ fetchImpl }); }
46
+ catch { throw new Error('device-code-failed'); }
47
+ if (controller.signal.aborted) throw new Error('timeout');
48
+ ready(code.user_code, code.verification_uri || CODEX_DEVICE_LOGIN_URL);
49
+ const interval = Math.max(3000, (code.interval ?? 0) * 1000);
50
+ const deadline = Date.now() + TIMEOUT_MS;
51
+ while (!controller.signal.aborted && Date.now() < deadline) {
52
+ await new Promise<void>((resolve) => setTimeout(resolve, Math.min(interval, Math.max(0, deadline - Date.now()))));
53
+ if (controller.signal.aborted || Date.now() >= deadline) break;
54
+ let result: Awaited<ReturnType<typeof pollDeviceCode>>;
55
+ try { result = await pollDeviceCode(code.device_auth_id, code.user_code, { fetchImpl }); }
56
+ catch { throw new Error('device-poll-failed'); }
57
+ if (!result) continue;
58
+ let token: Awaited<ReturnType<typeof exchangeDeviceAuthCode>>;
59
+ try { token = await exchangeDeviceAuthCode(result.authorization_code, result.code_verifier, { fetchImpl }); }
60
+ catch { throw new Error('token-exchange'); }
61
+ if (controller.signal.aborted) break;
62
+ saveTokens('openai-codex', {
63
+ accessToken: token.access_token,
64
+ refreshToken: token.refresh_token,
65
+ ...(token.id_token ? { idToken: token.id_token } : {}),
66
+ expiresAt: expiresAtFromSeconds(token.expires_in),
67
+ scope: token.scope,
68
+ tokenType: token.token_type ?? 'Bearer',
69
+ }, { authMode: 'chatgpt' });
70
+ return;
71
+ }
72
+ throw new Error('timeout');
73
+ } catch (error) {
74
+ if (controller.signal.aborted) throw new Error('timeout');
75
+ throw error;
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+
81
+ export function saveCodexProvider(): void {
82
+ const cur = getUserConfig();
83
+ saveUserConfig({ ...cur, llm: { ...cur.llm, provider: 'openai-codex', apiKey: undefined, baseUrl: undefined, model: undefined } });
84
+ reloadUserConfig();
85
+ }
86
+
87
+ const defaultDeps: LoginDeps = {
88
+ browser: (openBrowser) => loginWithCodexBrowser({ openBrowser, timeoutMs: TIMEOUT_MS }),
89
+ device: deviceLogin,
90
+ saveProvider: saveCodexProvider,
91
+ observe: (event, mode) => { debug.log('setup.codex-login', event, { mode }); },
92
+ };
93
+
94
+ function json(body: CodexLoginStatus | { error: string }, status = 200): Response {
95
+ return new Response(JSON.stringify(body), {
96
+ status, headers: { 'content-type': 'application/json; charset=utf-8', 'access-control-allow-origin': '*' },
97
+ });
98
+ }
99
+
100
+ /** One in-process login survives leaving or reloading the setup page. */
101
+ export function createCodexLoginHandlers(deps: LoginDeps = defaultDeps) {
102
+ let status: CodexLoginStatus = { state: 'idle' };
103
+ let preparing: Promise<void> | undefined;
104
+ let generation = 0;
105
+
106
+ const get = (): Response => json(status);
107
+
108
+ const post = async (req: Request): Promise<Response> => {
109
+ let body: unknown;
110
+ try { body = await req.json(); } catch { return json({ error: 'invalid-json' }, 400); }
111
+ const mode = body && typeof body === 'object' && 'mode' in body ? body.mode : undefined;
112
+ if (mode !== 'browser' && mode !== 'device') return json({ error: 'invalid-mode' }, 400);
113
+ // Same method again → the login already in flight. A different method → start over; the older
114
+ // attempt's result is ignored (generation). Browser → device is the recovery path when a popup
115
+ // was blocked; device needs no local port, so it never collides with the browser's callback server.
116
+ if (status.state === 'pending' && status.mode === mode) {
117
+ if (preparing) await preparing;
118
+ return json(status);
119
+ }
120
+
121
+ status = { state: 'pending', mode };
122
+ const attempt = ++generation;
123
+ deps.observe('started', mode);
124
+ let resolveReady!: () => void;
125
+ preparing = new Promise<void>((resolve) => { resolveReady = resolve; });
126
+ const ready = (fields: Pick<Extract<CodexLoginStatus, { state: 'pending' }>, 'authorizeUrl' | 'userCode' | 'verificationUrl'>) => {
127
+ if (generation === attempt && status.state === 'pending' && status.mode === mode) {
128
+ status = { ...status, ...fields };
129
+ resolveReady();
130
+ }
131
+ };
132
+ void (async () => {
133
+ try {
134
+ if (mode === 'browser') {
135
+ await deps.browser((url) => { ready({ authorizeUrl: url }); return true; });
136
+ } else {
137
+ await deps.device((userCode, verificationUrl) => ready({ userCode, verificationUrl }));
138
+ }
139
+ if (generation !== attempt) return;
140
+ try { deps.saveProvider(); } catch { throw new Error('config-save-failed'); }
141
+ status = { state: 'ok', mode };
142
+ deps.observe('ok', mode);
143
+ } catch (error) {
144
+ if (generation !== attempt) return;
145
+ status = { state: 'error', mode, error: reason(error, mode) };
146
+ deps.observe('error', mode);
147
+ } finally {
148
+ resolveReady();
149
+ if (generation === attempt) preparing = undefined;
150
+ }
151
+ })();
152
+ await preparing;
153
+ return json(status);
154
+ };
155
+ return { get, post };
156
+ }
157
+
158
+ const login = createCodexLoginHandlers();
159
+ export const handleSetupCodexLoginGet = login.get;
160
+ export const handleSetupCodexLoginPost = login.post;