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
@@ -46,7 +46,11 @@ import { getUserConfig } from '../user-config.js';
46
46
  import { getModelFamily } from '../models/prompts.js';
47
47
  import { resolveModelContextWindow } from '../models/context-window.js';
48
48
  import type { AcpServerOptions, AcpTurnContext } from './server.js';
49
+ import { acpToolDenialResult } from './tool-approval.js';
49
50
  import { readOriginSessionMeta } from './origin-session-meta.js';
51
+ import { isFactQuestion } from './fact-question.js';
52
+
53
+ const FACT_QUESTION_INSTRUCTION = '사실·최신 정보 질문이다. WebSearch 를 1~2회 쓰고 검색 결과(제목·스니펫·URL)로 답하라. 출처 링크를 붙여라. 셸·브라우저·파일 도구는 검색 결과로 부족할 때만. 도구 예산에 닿으면 지금까지 근거로 답하고 끝에 «더 찾아볼까요?»를 붙여라.';
50
54
 
51
55
  export interface CoreTurnBridgeDeps {
52
56
  /** Build the seed message list for this ACP prompt. Implementations
@@ -112,6 +116,28 @@ export interface CoreTurnBridgeDeps {
112
116
  * pegging the event loop. */
113
117
  export const DEFAULT_BRIDGE_ABORT_POLL_MS = 50;
114
118
 
119
+ /** PCH-2b — ask the ACP client before a write/shell tool runs. A denied,
120
+ * cancelled or timed-out tool is not run; the LLM gets a «not run» result. */
121
+ export function acpApprovalGatedDispatch(
122
+ turnCtx: AcpTurnContext,
123
+ dispatchTool: CoreTurnDispatchTool,
124
+ signal: AbortSignal,
125
+ ): CoreTurnDispatchTool {
126
+ const approveTool = turnCtx.approveTool;
127
+ if (!approveTool) return dispatchTool;
128
+ return async (name, args, dispatchCtx) => {
129
+ if (signal.aborted || turnCtx.isAborted()) return acpToolDenialResult(name, 'cancelled');
130
+ const approval = await approveTool({
131
+ toolCallId: dispatchCtx?.callId ?? `${name}-${Date.now().toString(36)}`,
132
+ toolName: name,
133
+ toolArgs: args,
134
+ });
135
+ if (!approval.allowed) return acpToolDenialResult(name, approval.reason);
136
+ if (signal.aborted || turnCtx.isAborted()) return acpToolDenialResult(name, 'cancelled');
137
+ return dispatchTool(name, args, dispatchCtx);
138
+ };
139
+ }
140
+
115
141
  /** Extract the trailing assistant message text from runCoreTurn's
116
142
  * `newMessages` array. emitTurnComplete (src/llm.ts:3835) appends
117
143
  * `{role:'assistant', content:[{type:'text', text:finalAssistantText}]}`
@@ -173,7 +199,23 @@ export function bridgeCoreTurnToAcp(
173
199
  }),
174
200
  );
175
201
  const modelOverride = deps.resolveModel?.({ sessionId: turnCtx.sessionId, userText: turnCtx.userText });
176
- const maxToolTurns = deps.resolveMaxToolTurns?.({ sessionId: turnCtx.sessionId });
202
+ const factQuestion = isFactQuestion(turnCtx.userText);
203
+ const explicitMaxToolTurns = deps.resolveMaxToolTurns?.({ sessionId: turnCtx.sessionId });
204
+ const maxToolTurns = explicitMaxToolTurns ?? (factQuestion ? getUserConfig().chat.factQuestion.maxToolTurns : undefined);
205
+ const turnMessages: LLMMessage[] = factQuestion
206
+ ? [{ role: 'system', content: FACT_QUESTION_INSTRUCTION }, ...messages]
207
+ : messages;
208
+ let toolCallCount = 0;
209
+ const toolKinds = new Set<string>();
210
+ const observeToolBudget = () => {
211
+ try {
212
+ debug.log('chat.turn', 'tool-budget', {
213
+ factQuestion, maxToolTurns: maxToolTurns ?? null,
214
+ count: toolCallCount, kinds: [...toolKinds],
215
+ });
216
+ } catch { /* observation cannot interrupt a turn */ }
217
+ };
218
+ observeToolBudget();
177
219
 
178
220
  // 2026-05-03 PM++ — Track streamed text so onTurnComplete can
179
221
  // detect runtime-injected synthesis (W5-E/W5-F/W5-G) and surface
@@ -235,17 +277,29 @@ export function bridgeCoreTurnToAcp(
235
277
  } catch {
236
278
  // Observability must not prevent the turn from reaching its dispatcher.
237
279
  }
280
+ const approvedDispatch = acpApprovalGatedDispatch(turnCtx, deps.dispatchTool, ctrl.signal);
281
+ const dispatchTool: CoreTurnDispatchTool = factQuestion && maxToolTurns !== undefined && maxToolTurns > 0
282
+ ? async (name, args, dispatchCtx) => {
283
+ if (toolCallCount >= maxToolTurns) {
284
+ return { error: 'Tool budget reached. Answer using the evidence already collected.' };
285
+ }
286
+ toolCallCount += 1;
287
+ toolKinds.add(name);
288
+ observeToolBudget();
289
+ return approvedDispatch(name, args, dispatchCtx);
290
+ }
291
+ : approvedDispatch;
238
292
  await runTurnDispatch({
239
293
  sessionId: turnCtx.sessionId,
240
294
  ...(originSessionId ? { originSessionId } : {}),
241
295
  ...(turnCtx.userText ? { userText: turnCtx.userText } : {}),
242
- messages,
296
+ messages: turnMessages,
243
297
  tools,
244
- dispatchTool: deps.dispatchTool,
298
+ dispatchTool,
245
299
  signal: ctrl.signal,
246
300
  ...(modelOverride !== undefined ? { modelOverride } : {}),
247
301
  ...(maxToolTurns !== undefined ? { maxToolTurns } : {}),
248
- ...(deps.budgetGrant !== undefined ? { budgetGrant: deps.budgetGrant } : {}),
302
+ ...((!factQuestion || explicitMaxToolTurns !== undefined) && deps.budgetGrant !== undefined ? { budgetGrant: deps.budgetGrant } : {}),
249
303
  callbacks: {
250
304
  onText: (delta) => {
251
305
  // Fire-and-forget: ACP `push` is async but the core-turn
@@ -262,7 +316,14 @@ export function bridgeCoreTurnToAcp(
262
316
  // callers see today. Fire-and-forget for the same reason
263
317
  // onText is: callbacks are synchronous, `turnCtx.push*` is
264
318
  // async, ordering is preserved by the SDK.
265
- onToolCall: (call) => { void turnCtx.pushToolCall(call); },
319
+ onToolCall: (call) => {
320
+ if (!factQuestion || maxToolTurns === undefined || maxToolTurns <= 0) {
321
+ toolCallCount += 1;
322
+ toolKinds.add(call.name);
323
+ observeToolBudget();
324
+ }
325
+ void turnCtx.pushToolCall(call);
326
+ },
266
327
  onToolResult: (call) => { void turnCtx.pushToolResult(call); },
267
328
  onUsage: (usage) => { void turnCtx.pushUsage(usage); },
268
329
  // Codex Responses API reasoning summary stream. Currently
@@ -301,26 +362,64 @@ export function bridgeCoreTurnToAcp(
301
362
  // re-emit would cause the "displayed then deleted then
302
363
  // re-displayed" flicker the user reported.
303
364
  const finalAssistantText = extractLastAssistantText(newMessages);
304
- const synthesisAlreadyStreamed =
305
- finalAssistantText !== null
306
- && finalAssistantText.length > 0
365
+ const budgetReached = factQuestion && maxToolTurns !== undefined
366
+ && maxToolTurns > 0 && toolCallCount >= maxToolTurns;
367
+ const followup = '더 찾아볼까요?';
368
+ const needsFollowup = budgetReached && finalAssistantText !== null
369
+ && finalAssistantText.trim().length > 0
370
+ && !finalAssistantText.trimEnd().endsWith(followup);
371
+ let lastAssistantIndex = -1;
372
+ for (let i = newMessages.length - 1; i >= 0; i--) {
373
+ if (newMessages[i]?.role === 'assistant') { lastAssistantIndex = i; break; }
374
+ }
375
+ const followupSuffix = `\n${followup}`;
376
+ const completedMessages: LLMMessage[] = needsFollowup
377
+ ? newMessages.map((message, index) => {
378
+ if (index !== lastAssistantIndex) return message;
379
+ if (typeof message.content === 'string') {
380
+ return { ...message, content: message.content + followupSuffix };
381
+ }
382
+ if (Array.isArray(message.content)) {
383
+ let lastTextIndex = -1;
384
+ for (let blockIndex = message.content.length - 1; blockIndex >= 0; blockIndex--) {
385
+ if (message.content[blockIndex]?.type === 'text') { lastTextIndex = blockIndex; break; }
386
+ }
387
+ if (lastTextIndex >= 0) {
388
+ return { ...message, content: message.content.map((block, blockIndex) =>
389
+ blockIndex === lastTextIndex && block.type === 'text'
390
+ ? { ...block, text: block.text + followupSuffix }
391
+ : block) };
392
+ }
393
+ }
394
+ return message;
395
+ })
396
+ : newMessages;
397
+ const visibleFinalText = needsFollowup ? extractLastAssistantText(completedMessages) : finalAssistantText;
398
+ const originalFinalStreamed = needsFollowup && finalAssistantText !== null
307
399
  && streamedTextSum.endsWith(finalAssistantText);
400
+ const synthesisAlreadyStreamed =
401
+ visibleFinalText !== null
402
+ && visibleFinalText.length > 0
403
+ && streamedTextSum.endsWith(visibleFinalText);
308
404
  const needsReSurface =
309
- finalAssistantText !== null
310
- && finalAssistantText !== streamedTextSum
311
- && !synthesisAlreadyStreamed;
312
- if (debug.enabled && finalAssistantText !== null) {
405
+ visibleFinalText !== null
406
+ && visibleFinalText !== streamedTextSum
407
+ && !synthesisAlreadyStreamed
408
+ && !originalFinalStreamed;
409
+ if (debug.enabled && visibleFinalText !== null) {
313
410
  debug.log('llm.synthesis-surface', 'acp-bridge.surface-final-text', {
314
411
  streamedChars: streamedTextSum.length,
315
- finalChars: finalAssistantText.length,
316
- delta: finalAssistantText.length - streamedTextSum.length,
412
+ finalChars: visibleFinalText.length,
413
+ delta: visibleFinalText.length - streamedTextSum.length,
317
414
  startsWithStreamed: streamedTextSum.length > 0
318
- && finalAssistantText.startsWith(streamedTextSum),
415
+ && visibleFinalText.startsWith(streamedTextSum),
319
416
  endsWithStreamedTail: synthesisAlreadyStreamed,
320
417
  needsReSurface,
321
418
  });
322
419
  }
323
- if (needsReSurface) {
420
+ if (originalFinalStreamed) {
421
+ void turnCtx.push(followupSuffix);
422
+ } else if (needsReSurface) {
324
423
  // Emit a clear-then-replace pair so clients that follow
325
424
  // the empty-chunk-as-clear convention (dashboard plain
326
425
  // chat) reset their per-round buffer before receiving
@@ -329,12 +428,12 @@ export function bridgeCoreTurnToAcp(
329
428
  // would only have the new finalAssistantText delta to
330
429
  // work with anyway).
331
430
  void turnCtx.push('');
332
- void turnCtx.push(finalAssistantText!);
431
+ void turnCtx.push(visibleFinalText!);
333
432
  }
334
433
  if (deps.onTurnComplete) {
335
434
  await deps.onTurnComplete({
336
435
  sessionId: turnCtx.sessionId,
337
- newMessages,
436
+ newMessages: completedMessages,
338
437
  });
339
438
  }
340
439
  },
@@ -0,0 +1,11 @@
1
+ export function isFactQuestion(userText: string): boolean {
2
+ const text = userText.trim();
3
+ if (!text || text.length > 120 || /[\r\n]/.test(text)) return false;
4
+ if (/`|\bsrc\/|(?:^|[\s/])[^\s/]+\.(?:ts|tsx|js|jsx|json|md|py|sh|yaml|yml)\b|고쳐|구현|실행|만들어|수정|디버그|파일|읽어|열어|저장|작성|삭제|편집|검색해|찾아줘|보여줘|\/cc\b|delegate_code_agent/i.test(text)) return false;
5
+ // A request («…해줘?», «재시작해», «restart …») is work, not a fact question — even with a «?» or «지금»(round 3: «지금 서버를 재시작해줘?»).
6
+ const request = text.replace(/알려\s*(?:줘|주세요|줄래|주실래요)[??]?$/, '');
7
+ if (/(?:해|해\s*줘|해\s*주세요|하세요|해라|하자|줘|주세요|줄래|주실래요|봐|봐줘)[??]?$/.test(request)) return false;
8
+ if (/재시작|시작|중지|멈춰|꺼|켜|설치|배포|돌려|보내|올려|지워|바꿔|추가|업데이트|재부팅|재실행|정리|등록|\b(?:restart|start|stop|run|install|deploy|delete|remove|create|update|fix|kill|reboot)\b/i.test(text)) return false;
9
+ if (!/[??]$|(?:뭐야|무엇인가요|인가요|나요|입니까|얼마야|언제야|누구야|알려줘|알려주세요)$/.test(text)) return false;
10
+ return /최근|최신|지금|현재|몇|언제|누가|누구|얼마|가격|버전|출시|최저시급|\b(?:latest|current|when|who|how many|how much|price|version|release)\b/i.test(text);
11
+ }
package/src/acp/server.ts CHANGED
@@ -101,6 +101,8 @@ import { readOriginSessionMeta } from './origin-session-meta.js';
101
101
  import type { CodexPlugin } from './codex-plugins.js';
102
102
  import { flattenMcpServersToCodexConfig } from './codex-approval-adapter.js';
103
103
  import { runAcpTurn } from './turn-runner.js';
104
+ import type { AcpPermissionApprovalRequest } from './client.js';
105
+ import { createAcpSessionApprovalPolicy, type AcpToolApprovalResult } from './tool-approval.js';
104
106
  import { projectPwaToolResult, type ToolResultMeta } from '../nexus/api/meta-api.js';
105
107
  import type { LlmBrand } from '../llm-vision-capability.js';
106
108
  import {
@@ -233,6 +235,9 @@ export interface AcpServerOptions {
233
235
  * receives the user message text and a push() for streaming back
234
236
  * assistant chunks. When omitted, the server echoes the prompt. */
235
237
  runTurn?: (ctx: AcpTurnContext) => Promise<void>;
238
+ /** PCH-2b — ask the client before write/shell tools run ('ask', default).
239
+ * 'off' keeps the host's own approvals (the in-process TUI dashboard). */
240
+ toolApproval?: 'ask' | 'off';
236
241
  /** ACP standalone entry: require session cwd or an explicit boot default. */
237
242
  requireSessionToolCwd?: boolean;
238
243
  bootToolCwd?: string;
@@ -850,6 +855,8 @@ export interface AcpTurnContext {
850
855
  toolArgs?: Record<string, unknown>;
851
856
  timeoutMs?: number;
852
857
  }) => Promise<AcpApprovalDecision>;
858
+ /** The session-scoped gate used by core-turn tool dispatchers. */
859
+ approveTool?: (req: Parameters<AcpTurnContext['requestApproval']>[0]) => Promise<AcpToolApprovalResult>;
853
860
  }
854
861
 
855
862
  /** F1 — Phase 3 · resolved decision from `AcpTurnContext.requestApproval`.
@@ -866,6 +873,14 @@ export type AcpApprovalDecision =
866
873
  * `_PERMISSION_DEFAULT_TIMEOUT_S`; overrideable per call. */
867
874
  export const DEFAULT_APPROVAL_TIMEOUT_MS = 60_000;
868
875
 
876
+ export {
877
+ acpToolDenialResult,
878
+ createAcpSessionApprovalPolicy,
879
+ requiresAcpToolApproval,
880
+ type AcpToolApprovalResult,
881
+ type AcpToolDenialReason,
882
+ } from './tool-approval.js';
883
+
869
884
  /** ACP streaming Phase E (PLAN-pwa-webterm-voice-control v1.2 · 2026-05-07) —
870
885
  * module-level handle to the broadcaster owned by the active runAcpServer
871
886
  * instance. The chat REST handler (`src/nexus/api/meta-api.ts handlePromptStreamPost`)
@@ -991,6 +1006,7 @@ interface AcpServerContext {
991
1006
  /** One skip diagnostic per session, cleared when its last peer leaves. */
992
1007
  plainEnvelopeLogged: Set<string>;
993
1008
  dualRole: DualRoleManager;
1009
+ approvalPolicy: ReturnType<typeof createAcpSessionApprovalPolicy>;
994
1010
  /** Mints the unique suffix of a new session id. Injected so tests can
995
1011
  * pin it deterministically — production wires `mintAcpSessionToken`. */
996
1012
  nextSessionToken: () => string;
@@ -1054,7 +1070,7 @@ function wireAcpConnection(
1054
1070
  stream: ReturnType<typeof ndJsonStream>,
1055
1071
  ctx: AcpServerContext,
1056
1072
  ): AgentSideConnection {
1057
- const { sessions, sessionPeers, awarePeers, plainEnvelopeLogged, dualRole, opts } = ctx;
1073
+ const { sessions, sessionPeers, awarePeers, plainEnvelopeLogged, dualRole, approvalPolicy, opts } = ctx;
1058
1074
  // 2026-05-13 (M2 of AskUserQuestion cross-surface) — bound connection
1059
1075
  // widened to expose `extMethod` + `extNotification` (SDK's elanous
1060
1076
  // extension escape hatch). pushAskRequest / pushAskCancel route through
@@ -1771,7 +1787,9 @@ function wireAcpConnection(
1771
1787
  );
1772
1788
  };
1773
1789
  const requestApproval: AcpTurnContext['requestApproval'] = async (approvalReq) => {
1774
- const timeoutMs = approvalReq.timeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
1790
+ const timeoutMs = approvalReq.timeoutMs
1791
+ ?? (await import('../user-config.js')).getUserConfig().acp.toolApproval?.timeoutMs
1792
+ ?? DEFAULT_APPROVAL_TIMEOUT_MS;
1775
1793
  const optionBase = `${approvalReq.toolCallId}-`;
1776
1794
  const options = [
1777
1795
  { optionId: `${optionBase}allow-once`, name: 'Allow once', kind: 'allow_once' as const },
@@ -1787,6 +1805,8 @@ function wireAcpConnection(
1787
1805
  },
1788
1806
  options,
1789
1807
  });
1808
+ // A late rejection after the timeout won the race must not go unhandled.
1809
+ permissionPromise.catch(() => undefined);
1790
1810
  let timer: ReturnType<typeof setTimeout> | null = null;
1791
1811
  const timeoutPromise = new Promise<'timeout'>((resolve) => {
1792
1812
  timer = setTimeout(() => resolve('timeout'), timeoutMs);
@@ -1972,6 +1992,7 @@ function wireAcpConnection(
1972
1992
  }
1973
1993
  }
1974
1994
 
1995
+ const rejectedThisTurn = new Set<string>();
1975
1996
  const turnCtx: AcpTurnContext = {
1976
1997
  sessionId: req.sessionId,
1977
1998
  cwd: s.cwd,
@@ -1987,6 +2008,10 @@ function wireAcpConnection(
1987
2008
  pushSessionUpdate,
1988
2009
  pushUsage,
1989
2010
  requestApproval,
2011
+ ...(opts.toolApproval !== 'off'
2012
+ ? { approveTool: (approvalReq: Parameters<AcpTurnContext['requestApproval']>[0]) =>
2013
+ approvalPolicy.resolve(req.sessionId, approvalReq, requestApproval, rejectedThisTurn) }
2014
+ : {}),
1990
2015
  };
1991
2016
 
1992
2017
  // RC — intercept hook runs first. Lets the host treat the
@@ -2020,6 +2045,20 @@ function wireAcpConnection(
2020
2045
  chatId: req.sessionId,
2021
2046
  cwd: s.cwd,
2022
2047
  codexArgs: s.codexArgs,
2048
+ ...(turnCtx.approveTool ? {
2049
+ // PCH-2b — codex asks this turn's ACP client instead of auto-approving.
2050
+ permissionApprover: async (approvalReq: AcpPermissionApprovalRequest) => {
2051
+ if (s.aborted) return false;
2052
+ const decision = await turnCtx.approveTool!({
2053
+ toolCallId: `codex-${randomBytes(6).toString('hex')}`,
2054
+ // «codex exec» · «codex network» · «codex mcp» — allow-always is per kind.
2055
+ toolName: /^codex [a-z]+/.exec(approvalReq.title)?.[0] ?? `codex ${approvalReq.kind ?? 'action'}`,
2056
+ ...(approvalReq.rawInput && typeof approvalReq.rawInput === 'object' && !Array.isArray(approvalReq.rawInput)
2057
+ ? { toolArgs: approvalReq.rawInput as Record<string, unknown> } : {}),
2058
+ });
2059
+ return decision.allowed && !s.aborted;
2060
+ },
2061
+ } : {}),
2023
2062
  streamer: { edit: (text) => { void push(text); } },
2024
2063
  });
2025
2064
  } else if (opts.runTurn) {
@@ -2169,8 +2208,8 @@ function wireAcpConnection(
2169
2208
  const adopted = matches.find((match) => match.pt.isAlive);
2170
2209
  const existing = sameSession?.isAlive ? sameSession : adopted?.pt;
2171
2210
  if (existing) {
2211
+ registerPreviewTerminalForWebTap(existing, p.sessionId, tid, handle);
2172
2212
  if (existing !== sameSession && adopted) {
2173
- registerPreviewTerminalForWebTap(existing, p.sessionId, tid, handle);
2174
2213
  debug.log('webterm.acp', 'spawn.adopt', {
2175
2214
  terminalId: tid, fromSessions: adopted.sessionIds, toSession: p.sessionId,
2176
2215
  });
@@ -2207,10 +2246,10 @@ function wireAcpConnection(
2207
2246
  // appear duplicated. Pinning TERM to xterm-256color matches
2208
2247
  // the actual rendering surface and makes the redraws idempotent.
2209
2248
  termName: 'xterm-256color',
2210
- // ★ P0b-2 (실행 substrate 통합·기본 off) — 이 진짜 PWA/iOS 라이브 셸을 공유 registry 버스로
2211
- // 흡수(정체성·크로스서피스 goto·3-스택 통합). 라이브 렌더(yazi/마우스/커서/alt-screen) 검증이
2212
- // 기기 왕복을 요하므로 per-run env 로만 켠다(대표가 별도 부팅해 수습). 미설정=기존 dup-fd(무회귀).
2213
- useRegistry: process.env.ELANOUS_PREVIEW_TERMINAL_REGISTRY === '1',
2249
+ // The shared PTY bus delivers output to PreviewTerminal's headless
2250
+ // renderer and ACP taps. The direct /dev/fd duplicate can observe
2251
+ // shell exit without ever reading its prompt (black web terminal).
2252
+ useRegistry: true,
2214
2253
  ...(typeof p.shell === 'string' && p.shell.length > 0 ? { shell: p.shell } : {}),
2215
2254
  onExit: () => {
2216
2255
  void import('../web-terminal/preview-tap-registry.js')
@@ -3343,6 +3382,7 @@ function wireAcpConnection(
3343
3382
  if (remaining === 0) {
3344
3383
  sessionPeers.delete(sid);
3345
3384
  plainEnvelopeLogged.delete(sid);
3385
+ approvalPolicy.clearSession(sid);
3346
3386
  }
3347
3387
  if (debug.enabled) {
3348
3388
  debug.log('acp.peer.unregister', sid, { remainingPeers: remaining });
@@ -3487,6 +3527,7 @@ export async function runAcpServer(opts: AcpServerOptions = {}): Promise<void> {
3487
3527
  awarePeers,
3488
3528
  plainEnvelopeLogged,
3489
3529
  dualRole,
3530
+ approvalPolicy: createAcpSessionApprovalPolicy(),
3490
3531
  nextSessionToken: mintAcpSessionToken,
3491
3532
  agentName: opts.agentName ?? 'elanous-agent',
3492
3533
  agentVersion: opts.agentVersion ?? readPkgVersion(),
@@ -0,0 +1,126 @@
1
+ // PCH-2b (0.2.10 · security D-20261002-08) — ACP turns ask the client before
2
+ // write/shell tools run; «allow always» is remembered per session and tool.
3
+ // Goal: 내부 문서 `ASK-pwa-chat-turns-run-write-and-shell-tools-without-approval-2026-10-02`
4
+ import { debug } from '../debug/log.js';
5
+ import type { AcpApprovalDecision, AcpTurnContext } from './server.js';
6
+
7
+ /** PCH-2b — tools that write files or run shell commands. ACP turns (PWA·iOS·
8
+ * remote TUI) ask the client before these run; everything else runs as before.
9
+ * One list, here, because the daemon catalog has no read-only/mutating flag. */
10
+ const ACP_APPROVAL_TOOLS = new Set([
11
+ 'write', 'edit', 'multiedit', 'notebookedit', 'applypatch', 'apply_patch', 'patch',
12
+ 'bash', 'shell', 'runshell', 'exec_command',
13
+ 'ptyshellstart', 'ptyshellsend', 'webterminalinput',
14
+ 'delegate_code_agent',
15
+ ]);
16
+ export function requiresAcpToolApproval(toolName: string): boolean {
17
+ const name = toolName.startsWith('functions.') ? toolName.slice('functions.'.length) : toolName;
18
+ // Codex app-server only sends a permission request when it wants to act.
19
+ return ACP_APPROVAL_TOOLS.has(name.toLowerCase()) || name.startsWith('codex ');
20
+ }
21
+
22
+ export type AcpToolDenialReason = 'rejected' | 'cancelled' | 'timeout' | 'error';
23
+ export type AcpToolApprovalResult =
24
+ | { allowed: true }
25
+ | { allowed: false; reason: AcpToolDenialReason };
26
+
27
+ /** The tool result the LLM sees when a protected tool was not run. */
28
+ export function acpToolDenialResult(toolName: string, reason: AcpToolDenialReason): { error: string } {
29
+ if (reason === 'rejected') {
30
+ return { error: `${toolName}: 사람이 이 호출을 거절했다. 같은 턴에서 같은 도구를 다시 요청하지 말고, 이유를 묻거나 다른 길을 제안하라 — 실행하지 않았다 (tool not run: approval ${reason})` };
31
+ }
32
+ if (reason === 'timeout') {
33
+ return { error: `${toolName}: 사람의 답이 없었다(시한 초과) — 실행하지 않았다. 사용자에게 한 줄로 알려라 (tool not run: approval ${reason})` };
34
+ }
35
+ const why = reason === 'cancelled' ? '승인 요청이 취소됐다' : '승인을 받지 못했다';
36
+ return { error: `${toolName}: ${why} — 실행하지 않았다 (tool not run: approval ${reason})` };
37
+ }
38
+
39
+ /** One policy per ACP server: grants are scoped to the session AND exact tool
40
+ * name, not to a turn, connection, argument set, or other session. */
41
+ export function createAcpSessionApprovalPolicy(): {
42
+ resolve: (
43
+ sessionId: string,
44
+ request: Parameters<AcpTurnContext['requestApproval']>[0],
45
+ requestApproval: AcpTurnContext['requestApproval'],
46
+ rejectedThisTurn?: Set<string>,
47
+ ) => Promise<AcpToolApprovalResult>;
48
+ clearSession: (sessionId: string) => void;
49
+ } {
50
+ const alwaysAllowed = new Map<string, Set<string>>();
51
+ const sessionGenerations = new Map<string, number>();
52
+ const observe = (event: 'requested' | 'decided' | 'timeout' | 'remembered', sessionId: string, tool: string, outcome?: string) => {
53
+ // Tool arguments are deliberately not logged.
54
+ debug.log('acp.approval', event, { sessionId, tool, ...(outcome !== undefined ? { outcome } : {}) });
55
+ };
56
+ // Same-turn calls to one tool are asked one at a time, so a concurrent second call sees the first's denial
57
+ // instead of opening a second sheet (round 1 should-fix).
58
+ const turnInflight = new WeakMap<Set<string>, Map<string, Promise<void>>>();
59
+ const decide = async (
60
+ sessionId: string,
61
+ request: Parameters<AcpTurnContext['requestApproval']>[0],
62
+ requestApproval: AcpTurnContext['requestApproval'],
63
+ rejectedThisTurn?: Set<string>,
64
+ ): Promise<AcpToolApprovalResult> => {
65
+ if (rejectedThisTurn?.has(request.toolName)) {
66
+ debug.log('acp.approval', 'repeat-denied', { sessionId, tool: request.toolName });
67
+ return { allowed: false, reason: 'rejected' };
68
+ }
69
+ if (alwaysAllowed.get(sessionId)?.has(request.toolName)) {
70
+ observe('remembered', sessionId, request.toolName, 'allow-always');
71
+ return { allowed: true };
72
+ }
73
+ const generation = sessionGenerations.get(sessionId) ?? 0;
74
+ observe('requested', sessionId, request.toolName);
75
+ let decision: AcpApprovalDecision;
76
+ try {
77
+ decision = await requestApproval(request);
78
+ } catch {
79
+ observe('decided', sessionId, request.toolName, 'error');
80
+ return { allowed: false, reason: 'error' };
81
+ }
82
+ observe(decision === 'timeout' ? 'timeout' : 'decided', sessionId, request.toolName, decision);
83
+ if (generation !== (sessionGenerations.get(sessionId) ?? 0)) return { allowed: false, reason: 'cancelled' };
84
+ if (decision === 'allow-always') {
85
+ let tools = alwaysAllowed.get(sessionId);
86
+ if (!tools) {
87
+ tools = new Set();
88
+ alwaysAllowed.set(sessionId, tools);
89
+ }
90
+ tools.add(request.toolName);
91
+ }
92
+ if (decision === 'allow-once' || decision === 'allow-always') return { allowed: true };
93
+ if (decision === 'deny-once' || decision === 'deny-always') rejectedThisTurn?.add(request.toolName);
94
+ return {
95
+ allowed: false,
96
+ reason: decision === 'timeout' ? 'timeout' : decision === 'cancelled' ? 'cancelled' : 'rejected',
97
+ };
98
+ };
99
+ return {
100
+ async resolve(sessionId, request, requestApproval, rejectedThisTurn) {
101
+ if (!requiresAcpToolApproval(request.toolName)) return { allowed: true };
102
+ if (!rejectedThisTurn) return decide(sessionId, request, requestApproval);
103
+ let inflight = turnInflight.get(rejectedThisTurn);
104
+ if (!inflight) {
105
+ inflight = new Map();
106
+ turnInflight.set(rejectedThisTurn, inflight);
107
+ }
108
+ const prior = inflight.get(request.toolName);
109
+ let release!: () => void;
110
+ const mine = new Promise<void>((done) => { release = done; });
111
+ const chained = (prior ?? Promise.resolve()).then(() => mine);
112
+ inflight.set(request.toolName, chained);
113
+ try {
114
+ if (prior) await prior;
115
+ return await decide(sessionId, request, requestApproval, rejectedThisTurn);
116
+ } finally {
117
+ release();
118
+ if (inflight.get(request.toolName) === chained) inflight.delete(request.toolName);
119
+ }
120
+ },
121
+ clearSession(sessionId) {
122
+ alwaysAllowed.delete(sessionId);
123
+ sessionGenerations.set(sessionId, (sessionGenerations.get(sessionId) ?? 0) + 1);
124
+ },
125
+ };
126
+ }
@@ -40,6 +40,11 @@ import { unsafeBrandSessionUri } from '../mss/uri/brand.js';
40
40
  import { debug } from '../debug/log.js';
41
41
  import type { AgentStatusStore } from '../agent-status/store.js';
42
42
  import type { AcpPermissionApprover, AcpQuestionApprover } from './client.js';
43
+ import {
44
+ CODEX_APPROVAL_OPT_ONCE,
45
+ CODEX_APPROVAL_OPT_REJECT,
46
+ CODEX_APPROVAL_QUESTION_ID,
47
+ } from './codex-approval-adapter.js';
43
48
  import type { ConfirmChannel } from '../hitl/confirm.js';
44
49
  import type { QuestionChannel } from '../hitl/question.js';
45
50
  import {
@@ -69,6 +74,7 @@ import { getUserConfig } from '../user-config.js';
69
74
  // ACP `cancelled`), matching the pre-existing default-deny posture for
70
75
  // unattended turns — so nothing hangs when a surface opts out.
71
76
  const turnHitlChannels = new Map<string, ConfirmChannel[]>();
77
+ const turnPermissionApprovers = new Map<string, AcpPermissionApprover>();
72
78
  /** Parallel registry for multi-option question channels (same keying
73
79
  * as `turnHitlChannels`). Present ⇒ the question approver fans out
74
80
  * real N-option prompts; absent ⇒ it falls back to yes/no collapse
@@ -81,6 +87,8 @@ const turnHitlQuestionChannels = new Map<string, QuestionChannel[]>();
81
87
  const HITL_TURN_TIMEOUT_MS = 120_000;
82
88
 
83
89
  const surfacePermissionApprover: AcpPermissionApprover = async (req) => {
90
+ const turnApprover = turnPermissionApprovers.get(req.sessionId);
91
+ if (turnApprover) return turnApprover(req);
84
92
  // Autonomous by default: edit/command PERMISSIONS auto-approve. Asking the
85
93
  // human to approve every write defeats delegation (the interactive-editor
86
94
  // paradigm ACP inherited doesn't fit autonomous telegram delegation).
@@ -102,7 +110,31 @@ const surfacePermissionApprover: AcpPermissionApprover = async (req) => {
102
110
  return answer;
103
111
  };
104
112
 
113
+ /** PCH-2b — answer codex's 3-way approval question with a yes/no permission
114
+ * approver. Returns null when the request is not that question. */
115
+ export async function answerCodexApprovalQuestion(
116
+ req: Parameters<AcpQuestionApprover>[0],
117
+ approver: AcpPermissionApprover,
118
+ ): Promise<Awaited<ReturnType<AcpQuestionApprover>> | null> {
119
+ const q = req.questions[0];
120
+ if (req.questions.length !== 1 || q?.id !== CODEX_APPROVAL_QUESTION_ID) return null;
121
+ const approved = await approver({
122
+ backendId: req.backendId,
123
+ sessionId: req.sessionId,
124
+ title: q.question,
125
+ options: [],
126
+ });
127
+ return { answers: { [q.id]: approved ? CODEX_APPROVAL_OPT_ONCE : CODEX_APPROVAL_OPT_REJECT } };
128
+ }
129
+
105
130
  const surfaceQuestionApprover: AcpQuestionApprover = async (req) => {
131
+ // PCH-2b — an ACP server turn answers codex's approval question through its
132
+ // own client (yes/no), not the messenger HITL channels.
133
+ const turnApprover = turnPermissionApprovers.get(req.sessionId);
134
+ if (turnApprover) {
135
+ const answered = await answerCodexApprovalQuestion(req, turnApprover);
136
+ if (answered) return answered;
137
+ }
106
138
  const channels = turnHitlChannels.get(req.sessionId);
107
139
  if (!channels || channels.length === 0) return { answers: {}, cancelled: true };
108
140
  // When the surface provided real multi-option QuestionChannels, fan
@@ -306,6 +338,8 @@ export interface RunAcpTurnOpts {
306
338
  cwd?: string;
307
339
  /** Session-scoped Codex app-server configuration arguments. */
308
340
  codexArgs?: readonly string[];
341
+ /** ACP server turn's permission request, routed through its client rather than the messenger HITL channel. */
342
+ permissionApprover?: AcpPermissionApprover;
309
343
  /** Soft focus budget (tool-turns) for a TARGETED slash invocation
310
344
  * (`/cc`·/cdx·/gem). When set, a tight-budget directive is prepended
311
345
  * to the prompt so the sub-agent stays focused (minimize re-reads /
@@ -663,6 +697,7 @@ export async function runAcpTurn(opts: RunAcpTurnOpts): Promise<RunAcpTurnResult
663
697
  // session id (== the `req.sessionId` seen in requestPermission).
664
698
  const hasHitl = !!opts.hitlConfirmChannels && opts.hitlConfirmChannels.length > 0;
665
699
  if (hasHitl) turnHitlChannels.set(sessionId, opts.hitlConfirmChannels!);
700
+ if (opts.permissionApprover) turnPermissionApprovers.set(sessionId, opts.permissionApprover);
666
701
  const hasHitlQuestions = !!opts.hitlQuestionChannels && opts.hitlQuestionChannels.length > 0;
667
702
  if (hasHitlQuestions) turnHitlQuestionChannels.set(sessionId, opts.hitlQuestionChannels!);
668
703
 
@@ -784,6 +819,7 @@ export async function runAcpTurn(opts: RunAcpTurnOpts): Promise<RunAcpTurnResult
784
819
  } finally {
785
820
  inFlightTurns.delete(key);
786
821
  if (hasHitl) turnHitlChannels.delete(sessionId);
822
+ if (opts.permissionApprover) turnPermissionApprovers.delete(sessionId);
787
823
  if (hasHitlQuestions) turnHitlQuestionChannels.delete(sessionId);
788
824
  }
789
825
  }
@@ -26,6 +26,7 @@ import { buildAutonomousToolSpecs, dispatchAutonomousTool, isAutonomousTool, del
26
26
  import { isToolSearchCall, routeToolSearch } from '../skills/tools/tool-search-route.js';
27
27
  import { financeEnabled, financeAgentSystemPrompt, marketClock } from '../domains/finance.js';
28
28
  import { buildFinanceTools } from '../domains/finance-tools.js';
29
+ import { dispatchReleaseChange } from '../domains/release-tool.js';
29
30
  import { surfaceEventsDbPath, openSurfaceEventsDb, recentSentDigest, recordInboundTurn } from '../domains/surface-events.js';
30
31
  import { elanousSelfAccessPrompt, elanousSelfAmbientParts } from './self-ambient.js';
31
32
  import { localRefGroundingAmbient } from './ref-grounding.js';
@@ -188,6 +189,13 @@ export function makeElanousAgentRunTurn(cfg: UserConfig, surface: SessionSource,
188
189
  },
189
190
  });
190
191
  }
192
+ if (name === 'release_change') {
193
+ return dispatchReleaseChange(args, {
194
+ surface: 'chat', sessionId: opts.sessionId,
195
+ ...(opts.verifiedOwner ? { verifiedOwner: opts.verifiedOwner } : {}),
196
+ ...(surfaceChannels ? { confirmChannels: surfaceChannels } : {}),
197
+ });
198
+ }
191
199
  // Thread the turn's abort signal so a long in-flight Bash is killed by
192
200
  // `/cancel` (dispatchBash SIGTERMs its child on abort).
193
201
  return baseDispatch(name, args, opts.signal);