elanous 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -1,5 +1,9 @@
1
- import { extname } from 'node:path';
1
+ import { realpathSync, statSync } from 'node:fs';
2
+ import { extname, isAbsolute, join, sep } from 'node:path';
2
3
  import { ExecRequestRunner } from '../../exec-requests/runner.js';
4
+ import type { ExecAttachment } from '../../exec-requests/store.js';
5
+ import { getElanousConfigDir } from '../../elanous-config-dir.js';
6
+ import { defaultAttachmentBaseDir } from '../../boot/attachment-store.js';
3
7
  import { jsonResponse } from './json-response.js';
4
8
 
5
9
  const mime: Record<string, string> = {
@@ -8,7 +12,21 @@ const mime: Record<string, string> = {
8
12
  '.md': 'text/markdown; charset=utf-8', '.txt': 'text/plain; charset=utf-8',
9
13
  };
10
14
 
11
- export async function handleExecRequests(req: Request, runner: ExecRequestRunner): Promise<Response> {
15
+ function resolveAttachment(path: string, roots: string[]): string | null {
16
+ if (!isAbsolute(path)) return null;
17
+ try {
18
+ const real = realpathSync(path);
19
+ if (!statSync(real).isFile()) return null;
20
+ return roots.some(root => {
21
+ try {
22
+ const base = realpathSync(root);
23
+ return real.startsWith(`${base}${sep}`);
24
+ } catch { return false; }
25
+ }) ? real : null;
26
+ } catch { return null; }
27
+ }
28
+
29
+ export async function handleExecRequests(req: Request, runner: ExecRequestRunner, uploadDir = defaultAttachmentBaseDir(), fieldDir = join(getElanousConfigDir(), 'field')): Promise<Response> {
12
30
  const path = new URL(req.url).pathname;
13
31
  if (path === '/v1/exec-requests') {
14
32
  if (req.method === 'POST') {
@@ -16,7 +34,19 @@ export async function handleExecRequests(req: Request, runner: ExecRequestRunner
16
34
  try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
17
35
  const text = body && typeof body === 'object' && !Array.isArray(body) ? (body as { text?: unknown }).text : undefined;
18
36
  if (typeof text !== 'string' || !text.trim()) return jsonResponse({ error: 'bad_request', reason: 'text required' }, 400);
19
- const item = runner.submit(text.trim());
37
+ const supplied = (body as { attachments?: unknown }).attachments;
38
+ if (supplied !== undefined && (!Array.isArray(supplied) || supplied.some(entry =>
39
+ !entry || typeof entry !== 'object' || Array.isArray(entry)
40
+ || typeof entry.name !== 'string' || !entry.name.trim() || typeof entry.path !== 'string'))) {
41
+ return jsonResponse({ error: 'bad_request', reason: 'attachments must be [{name,path}]' }, 400);
42
+ }
43
+ const roots = [uploadDir, fieldDir];
44
+ const resolved: Array<{ name: string; path: string | null }> | undefined = supplied?.map((entry: ExecAttachment) => ({
45
+ name: entry.name, path: resolveAttachment(entry.path, roots),
46
+ }));
47
+ if (resolved?.some(entry => !entry.path)) return jsonResponse({ error: 'attachment-outside' }, 400);
48
+ const attachments: ExecAttachment[] | undefined = resolved?.map(entry => ({ name: entry.name, path: entry.path! }));
49
+ const item = runner.submit(text.trim(), attachments);
20
50
  return jsonResponse({ id: item.id, status: 'planning' }, 202);
21
51
  }
22
52
  if (req.method === 'GET') return jsonResponse({ items: runner.store.list().map(item => {
@@ -19,6 +19,8 @@ import { setInProcessOutbound } from '../../domains/outbound-alert.js';
19
19
  import { getUserConfig } from '../../user-config.js';
20
20
  import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
21
21
  import { spawnSync } from 'node:child_process';
22
+ import { isAbsolute } from 'node:path';
23
+ import { createProject, listProjects } from '../../project/project-store.js';
22
24
  import { compareTokenConstTime } from '../../acp/transport/auth.js';
23
25
  import { ensureAuthToken } from '../../auth/acp-token.js';
24
26
  import type { NexusState } from '../state/state.js';
@@ -134,6 +136,7 @@ import {
134
136
  type ReflectionRouteOpts,
135
137
  } from './reflection.js';
136
138
  import { handleSignalBoard } from './signal-board-api.js';
139
+ import { handleUsageGet } from './usage-api.js';
137
140
  import {
138
141
  handleDashboard,
139
142
  handleDashboardRefreshLive,
@@ -155,11 +158,15 @@ import {
155
158
  import { handleBuildsGet, parseBuildsPath } from './builds-api.js';
156
159
  import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEventsGet, handleHarnessRunScreenGet, handleHarnessRunsGet, handleHarnessStopPost } from './harness-api.js';
157
160
  import { dispatchPersonaRoute } from './personas.js';
161
+ import { handleMe } from './operator.js';
162
+ import { handleOpsApi } from './ops-api.js';
158
163
  import { handleRoleJudge } from './role-judge.js';
159
164
  import { handleAudioStt } from './audio-stt.js';
160
165
  import { handleLlmModels } from './llm-models.js';
161
166
  import { handleLlmHostsConfig } from './llm-hosts-config.js';
162
167
  import { handleLlmRotationGet, handleLlmRotationNext } from './llm-rotation.js';
168
+ import { handleSetupClaim } from './setup-claim.js';
169
+ import { handleSetupCodexLoginGet, handleSetupCodexLoginPost } from './setup-codex-login.js';
163
170
  import {
164
171
  handleLlmProvidersList,
165
172
  handleLlmProviderSet,
@@ -275,7 +282,7 @@ import { handleSchedulesActionPost } from './schedules-action.js';
275
282
  import { handleSchedulesList, handleScheduleDetail, handleScheduleRuns } from './schedules-read.js';
276
283
  import { handleIntakeRunsList } from './intake-runs.js';
277
284
  import { handleMissionsList, handleMissionDetail } from './missions.js';
278
- import { handleSessionsStoreList, handleSessionsStoreGet, handleSessionsStoreFork, handleSessionsStoreDelete } from './sessions-store.js';
285
+ import { handleSessionsStoreList, handleSessionsStoreGet, handleSessionsStoreFork, handleSessionsStoreDelete, handleSessionsStorePatch } from './sessions-store.js';
279
286
  import { handleLogsQuery, handleLogsStream, handleLogsLevelGet, handleLogsLevelPost, handleLogsFacets, handleLogsHistogram, handleLogsInstances } from './log-fabric.js';
280
287
  import { handleTrace, handleTraceEvidence } from './trace.js';
281
288
  import { handleDispatchRunsList } from './dispatch-runs.js';
@@ -312,6 +319,7 @@ import {
312
319
  handleVoiceTranscribe,
313
320
  checkAuth,
314
321
  bearerCredential,
322
+ setupBearerScopeDenied,
315
323
  registerAuthPeerAddress,
316
324
  type MetaApiOpts,
317
325
  } from './meta-api.js';
@@ -461,6 +469,8 @@ export interface NexusHttpServerOpts {
461
469
  state: NexusState;
462
470
  registry: TabRegistry;
463
471
  eventBus: NexusEventBus;
472
+ /** Test seam for the read-only usage collector; production uses the cached handler. */
473
+ usageHandler?: () => Promise<Response>;
464
474
  /** Supervisor handle — required for the write API (PR ι). When omitted
465
475
  * (skipSupervisor mode), mutation routes return 503. */
466
476
  supervisor?: Supervisor;
@@ -791,6 +801,27 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
791
801
  };
792
802
  }
793
803
 
804
+ export function handleProjectsGet(): Response {
805
+ return jsonResponse({ projects: listProjects() });
806
+ }
807
+
808
+ export async function handleProjectsPost(req: Request): Promise<Response> {
809
+ let body: unknown;
810
+ try { body = await req.json(); }
811
+ catch { return jsonResponse({ error: 'invalid_json' }, 400); }
812
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
813
+ return jsonResponse({ error: 'invalid_name' }, 400);
814
+ }
815
+ const { name, primaryFolder } = body as Record<string, unknown>;
816
+ if (typeof name !== 'string' || !name.trim() || name.length > 80) {
817
+ return jsonResponse({ error: 'invalid_name' }, 400);
818
+ }
819
+ if (primaryFolder !== undefined && (typeof primaryFolder !== 'string' || !isAbsolute(primaryFolder))) {
820
+ return jsonResponse({ error: 'invalid_primary_folder' }, 400);
821
+ }
822
+ return jsonResponse({ project: createProject({ name, ...(primaryFolder !== undefined ? { primaryFolder } : {}) }) }, 201);
823
+ }
824
+
794
825
  function isPortBusy(err: unknown): boolean {
795
826
  if (!err) return false;
796
827
  // Bun.serve errors expose `.code === 'EADDRINUSE'` (Node-compatible)
@@ -826,6 +857,9 @@ export async function routeRequest(
826
857
  registerAuthPeerAddress(req, (server as BunServerLike & {
827
858
  requestIP?: (request: Request) => { address: string } | null;
828
859
  }).requestIP?.(req)?.address);
860
+ if (opts.metaApi && setupBearerScopeDenied(req, opts.metaApi)) {
861
+ return jsonResponse({ error: 'forbidden' }, 403);
862
+ }
829
863
 
830
864
  // PR c — try WS upgrade before HTTP routing. On undefined return the
831
865
  // bridge has accepted the upgrade (Bun completes the handshake) and
@@ -852,6 +886,12 @@ export async function routeRequest(
852
886
  const { pathname } = url;
853
887
  const method = req.method;
854
888
 
889
+ // Operator proxy credentials are distinct from the owner bearer. The ops handler
890
+ // performs its own operatorSignal gate before reading any run or checklist data.
891
+ if (pathname === '/v1/ops' || pathname.startsWith('/v1/ops/')) {
892
+ return handleOpsApi(req, opts.metaApi);
893
+ }
894
+
855
895
  // Default-deny for every `/v1/` path that is not on PUBLIC_ROUTES.
856
896
  // Reuses checkAuth (same-origin exemption + constant-time bearer).
857
897
  // setupMode is not implemented on this server (P24b) — pass false.
@@ -1272,11 +1312,22 @@ export async function routeRequest(
1272
1312
  return handleGraphsValidatePost(req, opts.metaApi);
1273
1313
  }
1274
1314
 
1315
+ if (pathname === '/v1/projects' && (method === 'GET' || method === 'POST')) {
1316
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1317
+ return method === 'GET' ? handleProjectsGet() : handleProjectsPost(req);
1318
+ }
1319
+
1275
1320
  if (pathname === '/v1/config/chat-fast-path' && (method === 'GET' || method === 'PUT')) {
1276
1321
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1277
1322
  return handleChatFastPathConfig(req);
1278
1323
  }
1279
1324
 
1325
+ if (pathname === '/v1/usage') {
1326
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1327
+ if (method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
1328
+ return (opts.usageHandler ?? handleUsageGet)();
1329
+ }
1330
+
1280
1331
  // Mutation routes (PR ι) — POST/PATCH/DELETE on /v1/nexus/tabs[/:id[/action]].
1281
1332
  // Templates POST handled here too (PR κ).
1282
1333
  // Config / secrets PUT/POST/DELETE here too (PR μ).
@@ -1520,9 +1571,18 @@ export async function routeRequest(
1520
1571
  if (pathname === '/v1/llm/rotation' && method === 'OPTIONS') {
1521
1572
  return handleLlmRotationGet(req);
1522
1573
  }
1574
+ // The one-use link is verified by the claim handler, not by the owner bearer.
1575
+ if (pathname === '/v1/setup/claim' && (method === 'POST' || method === 'OPTIONS')) {
1576
+ return method === 'OPTIONS' ? corsPreflight('POST, OPTIONS') : handleSetupClaim(req);
1577
+ }
1523
1578
  // PWA `/setup` wizard Phase 1 (2026-05-19) — LLM provider 첫 셋업.
1524
1579
  // POST 는 mutation 블록 안에서 등록 (memory `feedback_post_route_must_be_in_method_block`).
1525
1580
  // GET sibling 은 아래 GET-only 블록에 등록.
1581
+ if (pathname === '/v1/setup/codex-login' && (method === 'POST' || method === 'OPTIONS')) {
1582
+ if (method === 'OPTIONS') return corsPreflight('GET, POST, OPTIONS');
1583
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1584
+ return handleSetupCodexLoginPost(req);
1585
+ }
1526
1586
  if (pathname === '/v1/setup/llm-provider' && (method === 'POST' || method === 'OPTIONS')) {
1527
1587
  if (method === 'POST') { if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401); }
1528
1588
  return handleLlmProviderSet(req);
@@ -1887,6 +1947,11 @@ export async function routeRequest(
1887
1947
  const id = decodeURIComponent(pathname.slice('/v1/sessions/store/'.length, -'/fork'.length));
1888
1948
  return handleSessionsStoreFork(req, id, opts.metaApi);
1889
1949
  }
1950
+ if (pathname.startsWith('/v1/sessions/store/') && !pathname.endsWith('/fork') && method === 'PATCH') {
1951
+ if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1952
+ const id = decodeURIComponent(pathname.slice('/v1/sessions/store/'.length));
1953
+ return handleSessionsStorePatch(req, id, opts.metaApi);
1954
+ }
1890
1955
  // on-disk 세션 삭제(파괴적) — 라이브 세션 관리. /store/:id (fork/transcript 와 구분).
1891
1956
  if (pathname.startsWith('/v1/sessions/store/') && !pathname.endsWith('/fork') && method === 'DELETE') {
1892
1957
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
@@ -2153,6 +2218,9 @@ export async function routeRequest(
2153
2218
  }
2154
2219
  return jsonResponse({ error: 'static-not-wired' }, 404);
2155
2220
  }
2221
+ // OPS1·OPS2 — whether this caller is an operator (draws the ops area). Answers everyone before any auth gate;
2222
+ // false unless this daemon's config has operator.enabled.
2223
+ if (pathname === '/v1/me' && req.method === 'GET') return handleMe(req, opts.metaApi ?? ({} as MetaApiOpts));
2156
2224
  if (pathname === '/v1/health') {
2157
2225
  return handleHealth(opts.state, opts.registry, {
2158
2226
  bindHost: bind.hostname,
@@ -2405,6 +2473,10 @@ export async function routeRequest(
2405
2473
  }
2406
2474
  // PWA `/setup` wizard Phase 1 (2026-05-19) — provider catalog 표시.
2407
2475
  // POST sibling (`/v1/setup/llm-provider`) 는 위 mutation 블록.
2476
+ if (pathname === '/v1/setup/codex-login') {
2477
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
2478
+ return handleSetupCodexLoginGet();
2479
+ }
2408
2480
  if (pathname === '/v1/setup/llm-providers') {
2409
2481
  return handleLlmProvidersList(req);
2410
2482
  }
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { randomUUID } from 'node:crypto';
18
18
  import { matchTempToken } from '../../auth/temp-tokens.js';
19
+ import { matchSetupBearer } from '../../auth/setup-link-tokens.js';
19
20
  import { existsSync, readFileSync, statSync } from 'node:fs';
20
21
  import { extname } from 'node:path';
21
22
 
@@ -146,6 +147,10 @@ export interface MetaApiOpts {
146
147
  * compared constant-time against the `Authorization: Bearer …`
147
148
  * header. */
148
149
  bearerToken?: string;
150
+ /** Server-side human identity verification, independent of the shared bearer credential.
151
+ * Never derive this result from request body, claimed source, or bearer possession alone.
152
+ * Without a verifier, prompt tool mutations remain unavailable. */
153
+ resolveVerifiedOwner?: (req: Request) => { id: string } | undefined;
149
154
  /** Convenience for Tailscale-only / dogfood-loopback mode. */
150
155
  noAuth?: boolean;
151
156
  // ── PR k · runtime DI for the lifted endpoints ─────────────────
@@ -215,6 +220,8 @@ type AuthReason =
215
220
  | 'untrusted-same-origin-peer'
216
221
  | 'bearer-match'
217
222
  | 'temp-token'
223
+ | 'setup-bearer'
224
+ | 'setup-scope-denied'
218
225
  | 'no-bearer-configured'
219
226
  | 'missing-auth-header'
220
227
  | 'bearer-length-mismatch'
@@ -248,8 +255,27 @@ export function registerAuthPeerAddress(req: Request, peerAddress: string | unde
248
255
  authPeerAddresses.set(req, peerAddress);
249
256
  }
250
257
 
258
+ function setupRouteAllowed(req: Request): boolean {
259
+ const path = new URL(req.url).pathname;
260
+ if (path.startsWith('/v1/setup/')) return true;
261
+ return (req.method === 'POST' && path === '/v1/prompt/stream')
262
+ || (req.method === 'GET' && (
263
+ /^\/v1\/sessions\/store\/[^/]+$/.test(path) || path === '/v1/chat/events'
264
+ ));
265
+ }
266
+
251
267
  function decideAuth(req: Request, opts: MetaApiOpts): { ok: boolean; reason: AuthReason } {
252
268
  if (opts.noAuth) return { ok: true, reason: 'noauth' };
269
+ const offered = req.headers.get('authorization');
270
+ if (offered?.startsWith('Bearer elsb_')) {
271
+ const owner = decideBearer(req, opts);
272
+ if (owner.ok) return owner;
273
+ const bearer = offered.slice('Bearer '.length).trim();
274
+ if (!matchSetupBearer(bearer)) return { ok: false, reason: 'bearer-mismatch' };
275
+ return setupRouteAllowed(req)
276
+ ? { ok: true, reason: 'setup-bearer' }
277
+ : { ok: false, reason: 'setup-scope-denied' };
278
+ }
253
279
  const peerAddress = authPeerAddresses.get(req);
254
280
  if (isSameOriginRequest(req, peerAddress)) return { ok: true, reason: 'same-origin' };
255
281
  const peerRejected = req.headers.get('sec-fetch-site') === 'same-origin'
@@ -292,6 +318,10 @@ export function bearerCredential(req: Request, opts: MetaApiOpts): 'noauth' | 'b
292
318
  // check — without duplicating security-sensitive code.
293
319
  const authDecisions = new WeakMap<Request, { ok: boolean; reason: AuthReason }>();
294
320
 
321
+ export function setupBearerScopeDenied(req: Request, opts: MetaApiOpts): boolean {
322
+ return decideAuth(req, opts).reason === 'setup-scope-denied';
323
+ }
324
+
295
325
  export function checkAuth(req: Request, opts: MetaApiOpts): boolean {
296
326
  const decision = decideAuth(req, opts);
297
327
  authDecisions.set(req, decision);
@@ -322,6 +352,7 @@ export function checkAuth(req: Request, opts: MetaApiOpts): boolean {
322
352
  * intentionally collapse here to avoid exposing token-length information. */
323
353
  function authFailureResponse(req: Request, opts: MetaApiOpts): Response {
324
354
  const decision = authDecisions.get(req) ?? decideAuth(req, opts);
355
+ if (decision.reason === 'setup-scope-denied') return jsonResponse({ error: 'forbidden' }, 403);
325
356
  return jsonResponse({ error: 'unauthorized', reason: publicAuthFailureReason(decision.reason) }, 401);
326
357
  }
327
358
 
@@ -761,6 +792,11 @@ function notFound(payload: Record<string, unknown> = {}): Response {
761
792
 
762
793
  // ── /v1/prompt ────────────────────────────────────────────────────
763
794
 
795
+ export function promptVerifiedOwner(req: Request, opts: MetaApiOpts): { id: string } | undefined {
796
+ const owner = opts.resolveVerifiedOwner?.(req);
797
+ return typeof owner?.id === 'string' && owner.id.trim() ? { id: owner.id.trim() } : undefined;
798
+ }
799
+
764
800
  export async function handlePromptPost(
765
801
  req: Request,
766
802
  opts: MetaApiOpts,
@@ -804,6 +840,7 @@ export async function handlePromptPost(
804
840
  surface: surfaceResolution.surface,
805
841
  surfaceResolutionReason: surfaceResolution.reason,
806
842
  ...(parsed.value.userContent ? { promptBlocks: parsed.value.userContent } : {}),
843
+ ...((owner) => owner ? { verifiedOwner: owner } : {})(promptVerifiedOwner(req, opts)),
807
844
  ...(resolvePromptHitlChannels(typeof body.sessionId === 'string' ? body.sessionId : '', opts) ?? {}),
808
845
  ...(effectiveSurface ? { toolSurface: effectiveSurface } : {}),
809
846
  ...(opts.toolCwd ? { toolCwd: opts.toolCwd } : {}),
@@ -1161,6 +1198,7 @@ export async function handlePromptStreamPost(
1161
1198
  surface: surfaceResolution.surface,
1162
1199
  surfaceResolutionReason: surfaceResolution.reason,
1163
1200
  ...(parsed.value.userContent ? { promptBlocks: parsed.value.userContent } : {}),
1201
+ ...((owner) => owner ? { verifiedOwner: owner } : {})(promptVerifiedOwner(req, opts)),
1164
1202
  ...(resolvePromptHitlChannels(typeof body.sessionId === 'string' ? body.sessionId : '', opts) ?? {}),
1165
1203
  ...(effectiveSurface ? { toolSurface: effectiveSurface } : {}),
1166
1204
  ...(opts.toolCwd ? { toolCwd: opts.toolCwd } : {}),
@@ -0,0 +1,61 @@
1
+ // OPS1·OPS2 — who is an «operator» (sees the ops area). Off unless this daemon's config says `operator.enabled: true`
2
+ // (only our own machines); an external install never reports an operator, whatever header or token arrives.
3
+ // Design: 내부 문서 `DESIGN-pwa-menu-reorg-and-ops-area-2026-10-02` §2.
4
+ import { timingSafeEqual } from 'node:crypto';
5
+ import { readFileSync, statSync } from 'node:fs';
6
+ import { debug } from '../../debug/log.js';
7
+ import { getUserConfig } from '../../user-config.js';
8
+ import { checkAuth, type MetaApiOpts } from './meta-api.js';
9
+
10
+ /** Set by the op.elanous.ai proxy (Caddy) after Google sign-in, server side; the proxy strips any incoming copy. */
11
+ export const OPERATOR_HEADER = 'x-elanous-operator';
12
+
13
+ export type OperatorSource = 'op-proxy' | 'owner' | null;
14
+ export interface OperatorSignal { operator: boolean; operatorSource: OperatorSource }
15
+ export interface OperatorConfig { enabled: boolean; proxySecretFile?: string }
16
+
17
+ export function readOperatorConfig(raw: unknown = getUserConfig().raw?.operator): OperatorConfig {
18
+ const o = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
19
+ return {
20
+ enabled: o.enabled === true,
21
+ ...(typeof o.proxySecretFile === 'string' && o.proxySecretFile.trim() ? { proxySecretFile: o.proxySecretFile.trim() } : {}),
22
+ };
23
+ }
24
+
25
+ /** The proxy secret, only from a regular file nobody but the owner can read (mode 600); anything else turns the header path off. */
26
+ export function readProxySecret(path: string | undefined): string | null {
27
+ if (!path) return null;
28
+ try {
29
+ const st = statSync(path);
30
+ if (!st.isFile() || (st.mode & 0o077) !== 0) return null;
31
+ const value = readFileSync(path, 'utf8').trim();
32
+ return value.length >= 16 ? value : null;
33
+ } catch { return null; }
34
+ }
35
+
36
+ function sameSecret(offered: string, secret: string): boolean {
37
+ const a = Buffer.from(offered);
38
+ const b = Buffer.from(secret);
39
+ return a.length === b.length && timingSafeEqual(a, b);
40
+ }
41
+
42
+ export interface OperatorDeps { config?: OperatorConfig; readSecret?: (path: string | undefined) => string | null; isOwner?: (req: Request) => boolean }
43
+
44
+ export function operatorSignal(req: Request, opts: MetaApiOpts, deps: OperatorDeps = {}): OperatorSignal {
45
+ const config = deps.config ?? readOperatorConfig();
46
+ if (!config.enabled) return { operator: false, operatorSource: null };
47
+ const offered = req.headers.get(OPERATOR_HEADER);
48
+ if (offered) {
49
+ const secret = (deps.readSecret ?? readProxySecret)(config.proxySecretFile);
50
+ if (secret && sameSecret(offered, secret)) return { operator: true, operatorSource: 'op-proxy' };
51
+ }
52
+ if ((deps.isOwner ?? ((r: Request) => checkAuth(r, opts)))(req)) return { operator: true, operatorSource: 'owner' };
53
+ return { operator: false, operatorSource: null };
54
+ }
55
+
56
+ /** `GET /v1/me` — answers every caller (no auth needed: it only says whether to draw the ops area). */
57
+ export function handleMe(req: Request, opts: MetaApiOpts, deps: OperatorDeps = {}): Response {
58
+ const signal = operatorSignal(req, opts, deps);
59
+ debug.log('ops.api', 'me', { operator: signal.operator, source: signal.operatorSource });
60
+ return Response.json(signal, { headers: { 'cache-control': 'no-store' } });
61
+ }
@@ -0,0 +1,148 @@
1
+ import { lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { debug, redactSecretText } from '../../debug/log.js';
4
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
5
+ import { listChecklist } from '../../release-loop/checklist.js';
6
+ import { getSchedule } from '../../release-loop/release-schedule.js';
7
+ import { jsonResponse } from './json-response.js';
8
+ import { operatorSignal } from './operator.js';
9
+ import { createSeatsCache, todayKst } from './ops-seats.js';
10
+ import { liveSeatsSources } from './ops-seats-sources.js';
11
+ import type { MetaApiOpts } from './meta-api.js';
12
+
13
+ const VERSION = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
14
+ const IDENTIFIER = /^[A-Za-z0-9-]+$/;
15
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
16
+ let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
17
+ /** Tests swap the sources; production builds them lazily on first use. */
18
+ export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
19
+ const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
20
+
21
+ interface RunNode { nodeId: string; ok?: unknown; output?: unknown }
22
+ interface RunRecord {
23
+ status: string;
24
+ path: string[];
25
+ startedAt: string;
26
+ input?: unknown;
27
+ nodes: RunNode[];
28
+ }
29
+
30
+ function recordAt(path: string): RunRecord | null {
31
+ try {
32
+ if (!lstatSync(path).isFile()) return null;
33
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
34
+ if (!value || typeof value !== 'object') return null;
35
+ const run = value as Partial<RunRecord>;
36
+ if (typeof run.status !== 'string' || !Array.isArray(run.path)
37
+ || typeof run.startedAt !== 'string' || !Array.isArray(run.nodes)) return null;
38
+ return run as RunRecord;
39
+ } catch { return null; }
40
+ }
41
+
42
+ function outputText(output: unknown): string {
43
+ if (typeof output === 'string') return output;
44
+ if (output === undefined || output === null) return '';
45
+ return JSON.stringify(output) ?? '';
46
+ }
47
+
48
+ function nodeSummary(output: unknown): string {
49
+ let value = output;
50
+ if (typeof value === 'string') {
51
+ const text = value;
52
+ try { value = JSON.parse(text) as unknown; }
53
+ catch {
54
+ const last = text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
55
+ try { value = JSON.parse(last ?? '') as unknown; } catch { return ''; }
56
+ }
57
+ }
58
+ if (!value || typeof value !== 'object') return '';
59
+ const summary = (value as { summary?: unknown }).summary;
60
+ return typeof summary === 'string' ? redactSecretText(summary).slice(0, 300) : '';
61
+ }
62
+
63
+ function runVersion(input: unknown): string | null {
64
+ if (!input || typeof input !== 'object') return null;
65
+ const version = (input as { version?: unknown }).version;
66
+ return typeof version === 'string' && VERSION.test(version) ? version : null;
67
+ }
68
+
69
+ /** The sole data gate for every read-only /v1/ops endpoint. */
70
+ export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Response | Promise<Response> {
71
+ const url = new URL(req.url);
72
+ const { pathname } = url;
73
+ if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
74
+
75
+ const signal = metaApi ? operatorSignal(req, metaApi) : { operator: false, operatorSource: null };
76
+ if (!signal.operator) {
77
+ debug.log('ops.api', 'refused', { path: pathname, source: signal.operatorSource });
78
+ return jsonResponse({ error: 'forbidden' }, 403);
79
+ }
80
+ const served = (response: Response): Response => {
81
+ debug.log('ops.api', 'served', { path: pathname, source: signal.operatorSource });
82
+ response.headers.set('cache-control', 'no-store');
83
+ return response;
84
+ };
85
+ if (pathname === '/v1/ops/seats') {
86
+ const date = url.searchParams.get('date') ?? todayKst();
87
+ if (!DATE.test(date) || Number.isNaN(Date.parse(`${date}T00:00:00+09:00`))) return jsonResponse({ error: 'invalid-date' }, 400);
88
+ seatsCache ??= createSeatsCache(liveSeatsSources());
89
+ return seatsCache(date).then((board) => served(jsonResponse(board)), () => jsonResponse({ error: 'seats-unavailable' }, 500));
90
+ }
91
+ const version = url.searchParams.get('version');
92
+ if (pathname === '/v1/ops/checklist') {
93
+ if (!version || !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
94
+ // OPS2 header «컷 · 착지 마감»: the version's row in the release schedule (REL9a), or null when none is set.
95
+ let schedule: { cutAt: string; landBy: string | null } | null = null;
96
+ try {
97
+ const row = getSchedule(version);
98
+ schedule = row ? { cutAt: row.cutAt, landBy: row.landBy ?? null } : null;
99
+ } catch (error) {
100
+ debug.log('ops.api', 'schedule-unreadable', { version, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
101
+ }
102
+ return served(jsonResponse({ ...listChecklist(version), schedule }));
103
+ }
104
+ if (pathname === '/v1/ops/release/runs') {
105
+ if (version !== null && !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
106
+ const dir = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop');
107
+ let filenames: string[];
108
+ try {
109
+ filenames = readdirSync(dir, { withFileTypes: true })
110
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json')
111
+ && IDENTIFIER.test(entry.name.slice(0, -5)))
112
+ .map((entry) => entry.name);
113
+ }
114
+ catch (error) {
115
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return served(jsonResponse([]));
116
+ return jsonResponse({ error: 'runs-unavailable' }, 500);
117
+ }
118
+ const runs = filenames.flatMap((filename) => {
119
+ const run = recordAt(join(dir, filename));
120
+ if (!run) return [];
121
+ const v = runVersion(run.input);
122
+ if (version !== null && v !== version) return [];
123
+ return [{
124
+ runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
125
+ startedAt: redactSecretText(run.startedAt), version: v,
126
+ path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
127
+ nodes: run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
128
+ nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
129
+ summary: nodeSummary(node.output),
130
+ })),
131
+ }];
132
+ }).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
133
+ return served(jsonResponse(runs));
134
+ }
135
+ // WHATWG URL normalizes a literal runs/../nodes/... before routing.
136
+ if (pathname.startsWith('/v1/ops/release/nodes/')) return jsonResponse({ error: 'invalid-id' }, 400);
137
+ if (pathname.startsWith('/v1/ops/release/runs/')) {
138
+ const match = LOG_PATH.exec(pathname);
139
+ if (!match) return jsonResponse({ error: 'invalid-id' }, 400);
140
+ const [, id, nodeId] = match;
141
+ if (!IDENTIFIER.test(id!) || !IDENTIFIER.test(nodeId!)) return jsonResponse({ error: 'invalid-id' }, 400);
142
+ const run = recordAt(join(effectiveInstanceRoot(), 'graph-runs', 'release-loop', `${id}.json`));
143
+ const node = run?.nodes.find((candidate) => candidate?.nodeId === nodeId);
144
+ if (!node || node.output === undefined || node.output === null) return jsonResponse({ error: 'not-found' }, 404);
145
+ return served(jsonResponse({ log: redactSecretText(outputText(node.output)).slice(-4_000) }));
146
+ }
147
+ return jsonResponse({ error: 'not-found' }, 404);
148
+ }
@@ -0,0 +1,95 @@
1
+ // Live sources for /v1/ops/seats: the coordination PR and merged PRs through gh (automation App identity, proxy variables
2
+ // removed — same rule as decision replies), the release checklist, and the decision ledger. Each failure becomes null upstream.
3
+ import { DecisionLedger } from '../../decisions/decision-ledger.js';
4
+ import { listCoordEvents, type CoordEvent } from '../../context-bus/coord-events.js';
5
+ import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
6
+ import { getUserConfig } from '../../user-config.js';
7
+ import { SEATS, kstDayRange, type ChannelComment, type ChannelSource, type MergedPr, type SeatsSources } from './ops-seats.js';
8
+
9
+ const GH_TIMEOUT_MS = 20_000;
10
+
11
+ async function ghLines(args: string[]): Promise<string[] | null> {
12
+ const env: Record<string, string> = {};
13
+ for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
14
+ try {
15
+ const { githubAutomationToken } = await import('../../auth/github-app-token.js');
16
+ const token = githubAutomationToken();
17
+ if (token) env.GH_TOKEN = token;
18
+ } catch { /* machine login */ }
19
+ const proc = Bun.spawn(['gh', ...args], { stdin: 'ignore', stdout: 'pipe', stderr: 'ignore', env });
20
+ const timer = setTimeout(() => proc.kill(), GH_TIMEOUT_MS);
21
+ try {
22
+ const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
23
+ return code === 0 ? out.split('\n').filter(Boolean) : null;
24
+ } finally { clearTimeout(timer); }
25
+ }
26
+
27
+ function channelTarget(): { repo: string; pr: string } | null {
28
+ const raw = (getUserConfig().raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
29
+ const m = typeof raw === 'string' ? /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(raw.trim()) : null;
30
+ return m ? { repo: m[1]!, pr: m[2]! } : null;
31
+ }
32
+
33
+ export async function channelCommentsSince(sinceIso: string): Promise<Array<{ body: string; createdAt: string; url: string }> | null> {
34
+ const target = channelTarget();
35
+ if (!target) return null;
36
+ const lines = await ghLines(['api', '--paginate', `repos/${target.repo}/issues/${target.pr}/comments?since=${sinceIso}&per_page=100`,
37
+ '--jq', '.[] | {body: .body, createdAt: .created_at, url: .html_url} | tojson']);
38
+ return lines?.map((line) => JSON.parse(line) as { body: string; createdAt: string; url: string }) ?? null;
39
+ }
40
+
41
+ function nextPatch(version: string): string {
42
+ const [major, minor, patch] = version.split('.').map(Number);
43
+ return `${major}.${minor}.${patch! + 1}`;
44
+ }
45
+
46
+ export function liveSeatsSources(deps: { listEvents?: typeof listCoordEvents; githubChannel?: typeof channelCommentsSince } = {}): SeatsSources {
47
+ return {
48
+ async channel(date) {
49
+ const since = kstDayRange(date).start;
50
+ let events: CoordEvent[] = [];
51
+ try { events = (deps.listEvents ?? listCoordEvents)({ since }); } catch { /* GitHub fallback */ }
52
+ const latest = new Map<string, CoordEvent>();
53
+ for (const event of events) {
54
+ if (!SEATS.some(({ seat }) => seat === event.refs.seat)) continue;
55
+ const prior = latest.get(event.refs.seat);
56
+ if (!prior || event.at > prior.at) latest.set(event.refs.seat, event);
57
+ }
58
+ const seatSources: Partial<Record<(typeof SEATS)[number]['seat'], ChannelSource>> = {};
59
+ if (latest.size) {
60
+ const comments: ChannelComment[] = [];
61
+ for (const { seat } of SEATS) {
62
+ const event = latest.get(seat);
63
+ if (!event) { seatSources[seat] = 'unknown'; continue; }
64
+ comments.push({ body: `**[${seat}]** ${event.text}`, createdAt: event.at, url: event.refs.url ?? '' });
65
+ seatSources[seat] = 'ledger';
66
+ }
67
+ return { source: 'ledger', comments, seatSources };
68
+ }
69
+ let github: ChannelComment[] | null;
70
+ try { github = await (deps.githubChannel ?? channelCommentsSince)(since); }
71
+ catch { github = null; }
72
+ if (github === null) return null;
73
+ for (const { seat } of SEATS) seatSources[seat] = 'github';
74
+ return { source: 'github', comments: github, seatSources };
75
+ },
76
+ async merged(date) {
77
+ const target = channelTarget();
78
+ if (!target) return null;
79
+ const { start, end } = kstDayRange(date);
80
+ const lines = await ghLines(['pr', 'list', '--repo', target.repo, '--state', 'merged', '--limit', '300',
81
+ '--search', `merged:${start.slice(0, 19)}Z..${end.slice(0, 19)}Z`, '--json', 'number,title,body,mergedAt', '--jq', '.[] | tojson']);
82
+ return lines?.map((line) => JSON.parse(line) as MergedPr) ?? null;
83
+ },
84
+ checklist() {
85
+ const dev = devVersion();
86
+ const next = listChecklist(nextPatch(dev)).items;
87
+ const current = listChecklist(dev).items;
88
+ const all: ChecklistItem[] = [...current, ...next];
89
+ return { current: next.length ? next : current, all };
90
+ },
91
+ openDecisionRaisers() {
92
+ return new DecisionLedger().list({ status: 'open' }).map((entry) => entry.raisedBy.agent);
93
+ },
94
+ };
95
+ }