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
@@ -8,7 +8,7 @@ export interface DashboardProviderRotationEntryView {
8
8
  export interface DashboardProviderAvailabilityView {
9
9
  available: boolean;
10
10
  name: string;
11
- model: string;
11
+ model: string | undefined;
12
12
  }
13
13
 
14
14
  export interface DashboardProviderSlashRuntimeDeps {
@@ -31,6 +31,7 @@ export interface DashboardProviderSlashRuntime {
31
31
  overviewLines(
32
32
  rotationEntries: readonly DashboardProviderRotationEntryView[],
33
33
  providers: readonly DashboardProviderAvailabilityView[],
34
+ catalogModelIds?: ReadonlySet<string>,
34
35
  ): string[];
35
36
  }
36
37
 
@@ -48,25 +49,41 @@ export function createDashboardProviderSlashRuntime(
48
49
  switchedLine: (label, provider, model) => deps.success(` ✓ switched → ${label} (${providerDetail(provider, model)})`),
49
50
  resetEmptyLine: () => deps.warning(' rotation empty — nothing to reset'),
50
51
  resetLine: (label) => deps.success(` ✓ reset → ${label}`),
51
- overviewLines: (rotationEntries, providers) => {
52
+ overviewLines: (rotationEntries, providers, catalogModelIds) => {
52
53
  const lines = ['', deps.accent('\u276f /provider')];
53
54
  if (rotationEntries.length > 0) {
54
- lines.push(deps.muted(' Rotation (cycle with `/provider next` or `/provider use <label>`):'));
55
+ lines.push(deps.muted(' 회전(바꾸기: /provider next · /provider use <이름>)'));
55
56
  for (const entry of rotationEntries) {
56
57
  const marker = entry.current ? deps.success('▸') : ' ';
57
- lines.push(` ${marker} ${entry.label.padEnd(14)} ${deps.subtext(entry.provider)} ${deps.muted(entry.model)}`);
58
+ const missing = catalogModelIds && entry.model !== '(provider default)' && !catalogModelIds.has(entry.model)
59
+ ? deps.muted(' · 목록에 없는 모델 — /provider use 로 바꾸기')
60
+ : '';
61
+ lines.push(` ${marker} ${entry.label.padEnd(14)} ${deps.subtext(entry.provider)} ${deps.muted(entry.model)}${missing}`);
58
62
  }
59
63
  lines.push('');
60
64
  }
61
- lines.push(deps.muted(' Available providers (env-detected):'));
65
+ lines.push(deps.muted(' 쓸 수 있는 공급자'));
66
+ // PROVIDERS has anthropic, not claude; collapse the legacy claude display alias.
67
+ const canonicalName = (name: string): string => {
68
+ const lower = name.toLowerCase();
69
+ return lower === 'claude' ? 'anthropic' : lower;
70
+ };
71
+ const unique = new Map<string, DashboardProviderAvailabilityView>();
62
72
  for (const provider of providers) {
63
- const mark = provider.available ? deps.success('\u2713') : deps.muted('\u00B7');
64
- const label = provider.available ? deps.text(provider.name) : deps.muted(provider.name);
65
- const hint = !provider.available
66
- ? deps.muted(` (set ${provider.name.toUpperCase()}_API_KEY${provider.name === 'local' ? ' or LOCAL_LLM_URL' : ''})`)
67
- : '';
68
- lines.push(` ${mark} ${label.padEnd(20)} ${deps.subtext(provider.model)}${hint}`);
73
+ const key = canonicalName(provider.name);
74
+ const previous = unique.get(key);
75
+ if (!previous || (!previous.available && provider.available) || (key === provider.name && previous.name !== key && previous.available === provider.available)) {
76
+ unique.set(key, provider);
77
+ }
78
+ }
79
+ let unavailable = 0;
80
+ for (const provider of unique.values()) {
81
+ if (!provider.available) { unavailable++; continue; }
82
+ lines.push(` ${deps.success('\u2713')} ${deps.text(provider.name).padEnd(20)} ${deps.subtext(provider.model ?? '')}`);
69
83
  }
84
+ if (unavailable > 0) lines.push(deps.muted(` 그 밖 ${unavailable}개는 키·로그인 필요 — elanous setup llm`));
85
+ // Keep the old English search terms as a subordinate hint, not section headings.
86
+ lines.push(deps.muted(' · Rotation (cycle with /provider next) · Available providers'));
70
87
  return lines;
71
88
  },
72
89
  };
@@ -301,6 +301,7 @@ import {
301
301
  import type { LLMProviderName } from '../../user-config.js';
302
302
  import { lookupLlmTierSpec } from '../../model-tier/index.js';
303
303
  import { listProviders } from '../../llm.js';
304
+ import { enabledModels, loadCatalog } from '../../intelligence-map/model-catalog.js';
304
305
  import { selfOrchestrateRuntime } from '../../self-dev/self-orchestrate-runtime.js';
305
306
  import type { ToolRuntime } from '../../tool-runtime/types.js';
306
307
  import { getSessionCwd } from '../../session/working-dir.js';
@@ -742,6 +743,7 @@ export interface DashboardSlashContext {
742
743
  overviewLines(
743
744
  rotation: readonly { label: string; provider: string; model: string; current: boolean }[],
744
745
  providers: readonly { available: boolean; name: string; model: string | undefined }[],
746
+ catalogModelIds?: ReadonlySet<string>,
745
747
  ): readonly string[];
746
748
  };
747
749
  /** 2026-05-05 — open the visual model picker programmatically.
@@ -3993,6 +3995,13 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
3993
3995
  }
3994
3996
 
3995
3997
  if (seSub === 'new' || seSub === 'clear') {
3998
+ if (seSub === 'new') {
3999
+ try {
4000
+ const { suggestProjectForFolder } = await import('../../project/project-store.js');
4001
+ const project = suggestProjectForFolder(process.cwd());
4002
+ if (project) ctx.chatLines.push(ctx.muted(` Project suggestion: ${project.name} (${project.id})`));
4003
+ } catch { /* suggestions must not interrupt a new conversation */ }
4004
+ }
3996
4005
  // Clear the in-memory history and detach if attached. Doesn't
3997
4006
  // delete any session files — those stay on disk for
3998
4007
  // /session list to enumerate.
@@ -5281,6 +5290,13 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
5281
5290
  const cfgNow = getUserConfig();
5282
5291
  const rot = cfgNow.llm.rotation;
5283
5292
  const curIdx = rot && rot.length > 0 ? currentRotationIndex(cfgNow) : -1;
5293
+ let catalogModelIds: Set<string> | undefined;
5294
+ try {
5295
+ const loaded = loadCatalog();
5296
+ if (loaded.source !== 'fallback') {
5297
+ catalogModelIds = new Set(enabledModels(loaded.catalog).map((model) => model.id));
5298
+ }
5299
+ } catch { /* catalog unreadable: no stale-model verdict */ }
5284
5300
  ctx.chatLines.push(...ctx.provider.slashRuntime.overviewLines(
5285
5301
  (rot ?? []).map((entry, index) => ({
5286
5302
  label: rotationEntryLabel(entry),
@@ -5293,6 +5309,7 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
5293
5309
  name: provider.name,
5294
5310
  model: provider.model,
5295
5311
  })),
5312
+ catalogModelIds,
5296
5313
  ));
5297
5314
  const currentRoute = currentRouteDecision('dashboard');
5298
5315
  if (currentRoute) ctx.chatLines.push(ctx.muted(` current route: ${formatRouteDecisionSummary(currentRoute)}`));
@@ -1,4 +1,6 @@
1
1
  import type { SlashCommand } from '../../chat/index.js';
2
+ import { readTuiSlashAudience, slashMaturity, slashVisibleFor } from '../../maturity/tui-slash-maturity.js';
3
+ import type { Role } from '../../maturity/feature-maturity.js';
2
4
  import { visibleWidth } from '../../tui.js';
3
5
 
4
6
  const ESSENTIAL_KEYS = [
@@ -34,46 +36,59 @@ export function buildEssentialHelpLines({
34
36
  names,
35
37
  descriptions,
36
38
  width,
39
+ audience = readTuiSlashAudience(),
37
40
  }: {
38
41
  names: readonly string[];
39
42
  descriptions: readonly Pick<SlashCommand, 'name' | 'aliases' | 'description'>[];
40
43
  width: number;
44
+ audience?: { role: Role; showBeta: boolean };
41
45
  }): string[] {
42
- const descriptionsByName = new Map<string, string>();
43
- for (const command of descriptions) {
44
- for (const name of [command.name, ...(command.aliases ?? [])]) {
45
- descriptionsByName.set(name, command.description);
46
- }
47
- }
48
- const entries = [...new Set(names)]
49
- .sort((a, b) => a.localeCompare(b))
50
- .flatMap((name) => {
51
- // Removing border glyphs from a name would invent a different, unregistered slash.
52
- if (/[│╭╮╰╯┌┐└┘─]/.test(name)) return [];
53
- const description = descriptionsByName.get(name)?.replace(/\s+/g, ' ').trim();
54
- return description ? [{ name, description }] : [];
55
- });
56
- // Pack alphabetically in reading order across columns; the handler checks
57
- // the modal's row budget and uses scrollable chat if the result outgrows it.
58
- const longestName = Math.max(22, ...entries.map(({ name }) => visibleWidth(`/${name} `) + 1));
59
- const columns = Math.max(1, Math.min(Math.ceil(entries.length / 20), Math.floor(width / longestName)));
46
+ const registered = new Set(names);
47
+ const entries = descriptions
48
+ .filter((command) => registered.has(command.name) &&
49
+ ((audience.role === 'owner' && slashMaturity(command.name) === undefined) || slashVisibleFor(command.name, audience.role, { showBeta: audience.showBeta })))
50
+ .map((command) => {
51
+ const aliases = [...new Set(command.aliases ?? [])].filter((alias) => registered.has(alias) && alias !== command.name);
52
+ const label = `/${command.name}${aliases.length ? ` (${aliases.join(', ')})` : ''}`;
53
+ const description = command.description.replace(/\s+/g, ' ').trim()
54
+ .replace(/^(?:(?:H\d+\s+P\d+|CV-\d+|Sprint\s+\d+|Showroom\s+v2)(?:\s*[·—:]\s*|\s+))+/i, '')
55
+ .trim();
56
+ return { label, name: command.name, description };
57
+ })
58
+ .filter(({ label, description }) => description && !/[│╭╮╰╯┌┐└┘─]/.test(label))
59
+ .sort((a, b) => a.name.localeCompare(b.name));
60
+ // Reserve the longest label, 28 description characters, and an ellipsis
61
+ // in each cell. Never trade a missing label for a shorter modal.
62
+ const widestLabel = Math.max(0, ...entries.map(({ label }) => visibleWidth(label)));
63
+ const readableCellWidth = widestLabel + 2 + 28 + 1;
64
+ const columns = entries.length >= 40 && width >= 100
65
+ ? Math.max(1, Math.floor(width / (readableCellWidth + 1)))
66
+ : 1;
60
67
  const columnWidth = Math.floor(width / columns);
61
68
  const commandLines: string[] = [];
62
69
  for (let start = 0; start < entries.length; start += columns) {
63
- const cells = entries.slice(start, start + columns).map(({ name, description }) => {
70
+ const cells = entries.slice(start, start + columns).map(({ label, description }) => {
64
71
  const cellWidth = columnWidth - (columns > 1 ? 1 : 0);
65
- if (visibleWidth(`/${name} `) > cellWidth) return '';
66
- return fitLine(`/${name} ${description}`, cellWidth);
72
+ return visibleWidth(`${label} `) > cellWidth ? '' : fitLine(`${label} ${description}`, cellWidth);
67
73
  });
68
74
  commandLines.push(cells.map((cell, index) =>
69
75
  index === cells.length - 1 ? cell : cell + ' '.repeat(columnWidth - visibleWidth(cell)),
70
76
  ).join('').trimEnd());
71
77
  }
72
- return [
73
- fitLine('Commands', width),
74
- ...commandLines,
75
- '',
76
- fitLine('Keys', width),
77
- ...ESSENTIAL_KEYS.map(([key, description]) => fitLine(`${key} ${description}`, width)),
78
- ];
78
+ const keyItems = ESSENTIAL_KEYS.map(([key, description]) => `${key} ${description}`);
79
+ const keyLines: string[] = [];
80
+ if (entries.length >= 40 && width >= 100) {
81
+ let keyRow = '';
82
+ for (const item of keyItems) {
83
+ if (keyRow && visibleWidth(keyRow) + 2 + visibleWidth(item) > width) {
84
+ keyLines.push(keyRow);
85
+ keyRow = '';
86
+ }
87
+ keyRow += (keyRow ? ' ' : '') + fitLine(item, width);
88
+ }
89
+ if (keyRow) keyLines.push(keyRow);
90
+ } else {
91
+ keyLines.push(...keyItems.map((item) => fitLine(item, width)));
92
+ }
93
+ return [fitLine('Commands', width), ...commandLines, '', fitLine('Keys', width), ...keyLines];
79
94
  }
@@ -236,7 +236,7 @@ export function createTurnStreamFormatter(
236
236
  },
237
237
  onToolCall(call: TurnStreamCall): void {
238
238
  toolCallCount += 1;
239
- deps.thinking.update(`Streaming ${call.name} (${toolCallCount} tools)`);
239
+ deps.thinking.update(`Streaming ${call.name} (${toolCallCount} ${toolCallCount === 1 ? 'tool' : 'tools'})`);
240
240
  const ptyLine = deps.ptyCallLine(call.name, call.args);
241
241
  if (ptyLine) {
242
242
  nonGenericCallIds.add(call.id);
@@ -6,9 +6,10 @@
6
6
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { debug } from '../debug/log.js';
9
+ import { readPendingQuestions } from '../ask-user-question/pending-questions.js';
9
10
  import { DecisionLedger, type DecisionEntry } from './decision-ledger.js';
10
11
 
11
- export type CardPlatform = 'telegram' | 'discord';
12
+ export type CardPlatform = 'telegram' | 'discord' | 'webpush';
12
13
  export interface CardButton { label: string; data: string }
13
14
  export interface CardView { text: string; buttons: CardButton[][] }
14
15
  export interface CardRef { chat: string; message: string }
@@ -99,6 +100,7 @@ export interface DecisionCardServiceOptions {
99
100
  /** Owner user ids for this platform — taps from anyone else are refused and observed. */
100
101
  ownerIds: readonly string[];
101
102
  ledger?: DecisionLedger;
103
+ pendingQuestions?: typeof readPendingQuestions;
102
104
  /** Where this platform keeps which card it sent (one file per platform — the two bots may run in different processes). */
103
105
  statePath?: string;
104
106
  now?: () => Date;
@@ -149,6 +151,52 @@ export class DecisionCardService {
149
151
  }
150
152
  let dirty = false;
151
153
  if (!state.since) { state.since = this.now().toISOString(); dirty = true; debug.log('decisions.telegram', 'baseline', { platform, since: state.since }); }
154
+ // Same state root as the ledger — the H2 answer writer delivers under the ledger's stateDir, so the questions are read there too.
155
+ const pending = this.opts.pendingQuestions ? this.opts.pendingQuestions() : readPendingQuestions({ root: () => join(this.ledger.path, '..', '..') });
156
+ if (pending.ok) {
157
+ const seen = new Set(all.flatMap(entry => entry.resume ? [entry.resume.questionId] : []));
158
+ for (const record of pending.questions) {
159
+ for (const question of record.questions) {
160
+ const data = { questionId: record.id, runId: record.runId };
161
+ if (record.expiresAt && record.expiresAt <= this.now().toISOString()) {
162
+ debug.log('hitl.card-bridge', 'skipped', { ...data, reason: 'expired' });
163
+ continue;
164
+ }
165
+ if (question.impact !== 'high' && question.impact !== 'critical') {
166
+ debug.log('hitl.card-bridge', 'skipped', { ...data, reason: 'impact' });
167
+ continue;
168
+ }
169
+ if (record.questions.length !== 1 || question.options.length < 2 || question.options.length > 4) {
170
+ debug.log('hitl.card-bridge', 'skipped', { ...data, reason: 'options' });
171
+ continue;
172
+ }
173
+ if (seen.has(record.id)) {
174
+ debug.log('hitl.card-bridge', 'dedup', { ...data, decisionId: all.find(entry => entry.resume?.questionId === record.id)?.id, reason: 'already-raised' });
175
+ continue;
176
+ }
177
+ try {
178
+ const keys = ['a', 'b', 'c', 'd'];
179
+ const recommended = question.recommendedIndex;
180
+ const entry = this.ledger.raise({
181
+ title: question.question.split(/\r?\n/, 1)[0]!.slice(0, 120),
182
+ category: question.impact === 'critical' ? 'irreversible' : 'scope',
183
+ scqa: { s: question.question, c: question.question },
184
+ options: question.options.map((option, index) => ({ key: keys[index]!, label: option.label, consequence: option.description })),
185
+ recommendation: recommended !== undefined && Number.isInteger(recommended) && recommended >= 0 && recommended < question.options.length
186
+ ? { option: keys[recommended]!, why: '런이 추천' } : { skipped: true, reason: '추천 없음' },
187
+ raisedBy: { agent: 'harness' },
188
+ resume: { questionId: record.id, ...(record.runId === undefined ? {} : { runId: record.runId }) },
189
+ ...(record.expiresAt ? { dueAt: record.expiresAt } : {}),
190
+ });
191
+ seen.add(record.id);
192
+ all.unshift(entry);
193
+ debug.log('hitl.card-bridge', 'raised', { ...data, decisionId: entry.id });
194
+ } catch (error) {
195
+ debug.log('hitl.card-bridge', 'skipped', { ...data, reason: error instanceof Error ? error.name : 'unknown' });
196
+ }
197
+ }
198
+ }
199
+ } else debug.log('hitl.card-bridge', 'skipped', { reason: 'pending-unreadable' });
152
200
  const chats = await this.opts.transport.ownerChats().catch(() => [] as string[]);
153
201
  for (const e of all) {
154
202
  const card = state.cards[e.id];
@@ -277,7 +325,7 @@ export function ghPrReplier(target: string, run: (args: string[], stdin: string)
277
325
 
278
326
  /** Post as the automation App (not the machine's personal gh login — that would make 대표 a participant and mail them),
279
327
  * with proxy variables removed (they break gh on the ops host). TC review #22660. */
280
- async function runGh(args: string[], stdin: string): Promise<number> {
328
+ export async function runGh(args: string[], stdin: string): Promise<number> {
281
329
  const env: Record<string, string> = {};
282
330
  for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
283
331
  try {
@@ -1,6 +1,7 @@
1
1
  import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, unlinkSync, writeSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { elanousStateRoot } from '../autopilot/state-paths.js';
4
+ import { readPendingQuestionAnswer, readPendingQuestions, writePendingQuestionAnswer } from '../ask-user-question/pending-questions.js';
4
5
  import { debug, redactSecretText } from '../debug/log.js';
5
6
  import { createVersionResolver, type VersionOptions, type Versions } from '../directives/version-at.js';
6
7
  import { getUserConfig } from '../user-config.js';
@@ -15,15 +16,27 @@ export interface DecisionEntry {
15
16
  options: DecisionOption[]; recommendation: Recommendation; raisedAt?: string; importedAt?: string;
16
17
  raisedBy: { agent: string; track?: DecisionTrack; session?: string }; version?: Versions;
17
18
  status: 'open' | 'decided' | 'withdrawn'; refs?: string[];
19
+ resume?: { questionId: string; runId?: string };
18
20
  /** DEC-TG — optional deadline (UTC). Cards remind the owner two hours before it. */
19
21
  dueAt?: string;
20
22
  decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
21
23
  withdrawnAt?: string; withdrawReason?: string;
22
24
  history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
23
25
  }
24
- export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt'> & { raisedAt?: string };
26
+ export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume'> & { raisedAt?: string };
25
27
  type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
26
- export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions }
28
+ export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions; writeAnswer?: typeof writePendingQuestionAnswer }
29
+
30
+ /** null = the decision resumes no run; otherwise whether the answer file was written (or already matched). */
31
+ export type AnswerDelivery = null | { ok: true; questionId: string } | { ok: false; questionId: string; reason: string };
32
+
33
+ /** The decision is durable even when delivery fails; retryAnswer(id) only retries delivery. */
34
+ export class DecisionAnswerDeliveryError extends Error {
35
+ constructor(readonly decisionId: string, readonly questionId: string) {
36
+ super(`decision ${decisionId} recorded but answer delivery failed for ${questionId}; retry with decisions retry-answer ${decisionId}`);
37
+ this.name = 'DecisionAnswerDeliveryError';
38
+ }
39
+ }
27
40
  const CATEGORIES: readonly string[] = ['secret', 'publish', 'money', 'security', 'scope', 'irreversible', 'other'];
28
41
  const TRACKS: readonly string[] = ['S', 'T', 'F', 'O'];
29
42
 
@@ -73,7 +86,11 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
73
86
  ? { skipped: true as const, reason: safe(single(input.recommendation.reason, 'skip reason')) }
74
87
  : { option: input.recommendation.option, why: safe(single(input.recommendation.why, 'recommendation why')) };
75
88
  if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
89
+ if (input.resume && !/^auq:[0-9a-z]+:[0-9a-z]{5}(?![\s\S])/.test(input.resume.questionId)) throw new Error('invalid resume questionId');
90
+ const resume = input.resume ? { questionId: input.resume.questionId,
91
+ ...(input.resume.runId === undefined ? {} : { runId: single(input.resume.runId, 'resume runId') }) } : undefined;
76
92
  return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
93
+ ...(resume ? { resume } : {}),
77
94
  raisedBy: { agent: safe(single(input.raisedBy.agent, 'agent')), ...(input.raisedBy.track ? { track: input.raisedBy.track } : {}), ...(input.raisedBy.session ? { session: safe(single(input.raisedBy.session, 'session')) } : {}) },
78
95
  ...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}),
79
96
  ...(input.dueAt ? { dueAt: utc(input.dueAt) } : {}) };
@@ -83,12 +100,16 @@ export class DecisionLedger {
83
100
  readonly path: string;
84
101
  private readonly lockPath: string;
85
102
  private readonly now: () => Date;
103
+ private readonly writeAnswer: typeof writePendingQuestionAnswer;
104
+ private readonly stateDir: string;
86
105
  private versionResolver?: (at: string) => Versions;
87
106
  private readonly versionOptions: DecisionLedgerOptions;
88
107
  constructor(options: DecisionLedgerOptions = {}) {
89
- this.path = join(options.stateDir ?? elanousStateRoot(), 'decisions', 'decisions.jsonl');
108
+ this.stateDir = options.stateDir ?? elanousStateRoot();
109
+ this.path = join(this.stateDir, 'decisions', 'decisions.jsonl');
90
110
  this.lockPath = `${this.path}.lock`;
91
111
  this.now = options.now ?? (() => new Date());
112
+ this.writeAnswer = options.writeAnswer ?? writePendingQuestionAnswer;
92
113
  this.versionOptions = options;
93
114
  this.versionResolver = options.resolveVersion;
94
115
  }
@@ -196,7 +217,14 @@ export class DecisionLedger {
196
217
  return entry;
197
218
  });
198
219
  }
220
+ /** Record the decision and say whether its answer reached the waiting run (null = the decision resumes nothing). */
221
+ decideWithDelivery(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): { entry: DecisionEntry; delivery: AnswerDelivery } {
222
+ return this.decideLocked(id, choice, by, note, decidedAt);
223
+ }
199
224
  decide(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): DecisionEntry {
225
+ return this.decideLocked(id, choice, by, note, decidedAt).entry;
226
+ }
227
+ private decideLocked(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): { entry: DecisionEntry; delivery: AnswerDelivery } {
200
228
  if (!by || (by.kind !== 'human' && by.kind !== 'auto')) throw new Error('decidedBy is required');
201
229
  if (by.kind === 'auto') {
202
230
  single(by.delegation, 'delegation'); single(by.agent, 'agent');
@@ -211,9 +239,52 @@ export class DecisionLedger {
211
239
  if (old.raisedAt && at < old.raisedAt) throw new Error('decision before raise');
212
240
  this.append({ type: 'decided', id, at, by, choice, version: this.version(at), ...(note ? { note: safe(single(note, 'note')) } : {}) });
213
241
  debug.log('decisions', 'decided', { id, category: old.category, by: by.kind });
214
- return this.show(id);
242
+ const decided = this.show(id);
243
+ return { entry: decided, delivery: this.deliverAnswer(decided) };
215
244
  });
216
245
  }
246
+ /** Retry delivery of the recorded choice without adding another decided event or changing its actor/note. */
247
+ retryAnswer(id: string): DecisionEntry {
248
+ return this.locked(() => {
249
+ const entry = this.show(id);
250
+ if (entry.status !== 'decided' || !entry.resume || !entry.choice || !entry.decidedBy) throw new Error(`decision has no recorded resume answer: ${id}`);
251
+ this.deliverAnswer(entry, true);
252
+ return entry;
253
+ });
254
+ }
255
+ private deliverAnswer(entry: DecisionEntry, throwOnFailure = false): AnswerDelivery {
256
+ if (!entry.resume) return null;
257
+ const { questionId, runId } = entry.resume;
258
+ try {
259
+ const label = entry.options.find(option => option.key === entry.choice)?.label;
260
+ if (!label || !entry.choice) throw new Error('recorded choice not found');
261
+ const deps = { root: () => this.stateDir };
262
+ const pending = readPendingQuestions(deps);
263
+ if (!pending.ok) throw new Error(pending.error);
264
+ const target = pending.questions.find(item => item.id === questionId);
265
+ if (!target || target.questions.length !== 1 || !target.questions[0]?.id) throw new Error('pending question target not found or ambiguous');
266
+ const question = target.questions[0];
267
+ if (question.options.length !== entry.options.length || question.options.some((option, index) =>
268
+ entry.options[index]?.key !== String.fromCharCode(97 + index) || entry.options[index]?.label !== option.label)) {
269
+ throw new Error('pending question options differ from decision');
270
+ }
271
+ const answerKey = question.id;
272
+ const answer = { id: questionId, result: { answers: { [answerKey]: `${entry.choice}) ${label}` },
273
+ ...(entry.note ? { otherText: { [answerKey]: entry.note } } : {}) } };
274
+ const existing = readPendingQuestionAnswer(questionId, deps);
275
+ if (!existing.ok) throw new Error('existing answer unreadable');
276
+ if (existing.answer) {
277
+ if (JSON.stringify(existing.answer) !== JSON.stringify(answer)) throw new Error('pending question already answered differently');
278
+ } else this.writeAnswer(answer, deps);
279
+ debug.log('hitl.card-bridge', 'answered', { decisionId: entry.id, questionId, runId, via: entry.decidedBy?.kind });
280
+ return { ok: true, questionId };
281
+ } catch (error) {
282
+ const reason = error instanceof Error ? (error as NodeJS.ErrnoException).code ?? error.name : typeof error;
283
+ debug.log('hitl.card-bridge', 'answer-write-failed', { decisionId: entry.id, questionId, reason });
284
+ if (throwOnFailure) throw new DecisionAnswerDeliveryError(entry.id, questionId);
285
+ return { ok: false, questionId, reason: error instanceof Error ? error.message.slice(0, 120) : String(reason) };
286
+ }
287
+ }
217
288
  /** Import an open historical request with known alternatives and an unknown original time. */
218
289
  importOpen(input: RaiseInput): DecisionEntry {
219
290
  const clean = validate(input);
@@ -0,0 +1,157 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
5
+ import { createLinearIssue, commentLinearIssue, findLinearDecisionIssue, moveLinearIssueToStateType, resolveLinearProjectId } from '../connectors/linear.js';
6
+ import { debug } from '../debug/log.js';
7
+ import { getSecretAsync } from '../nexus/config/secrets/index.js';
8
+ import { getUserConfig, type UserConfig } from '../user-config.js';
9
+ import { DecisionLedger, type DecisionEntry } from './decision-ledger.js';
10
+
11
+ interface ProjectionRecord { issue: string; closedAt?: string; commentPosted?: boolean }
12
+ type ProjectionState = Record<string, ProjectionRecord>;
13
+ export interface DecisionLinearProjectionDeps {
14
+ stateDir?: string;
15
+ ledger?: Pick<DecisionLedger, 'list'>;
16
+ config?: Pick<UserConfig, 'decisions' | 'coo'>;
17
+ getSecret?: (id: string) => Promise<string | undefined>;
18
+ fetch?: typeof fetch;
19
+ dryRun?: boolean;
20
+ }
21
+ export interface DecisionLinearProjectionResult {
22
+ created: number; closed: number; skipped: number; failed: number;
23
+ reason?: string;
24
+ plan?: Array<{ decisionId: string; action: 'create' | 'close'; issue?: string }>;
25
+ }
26
+
27
+ function readState(path: string): ProjectionState {
28
+ if (!existsSync(path)) return {};
29
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
30
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.entries(value).some(([id, row]) =>
31
+ !/^D-\d{8}-\d+$/.test(id) || !row || typeof row !== 'object' || Array.isArray(row) ||
32
+ typeof (row as ProjectionRecord).issue !== 'string' || !(row as ProjectionRecord).issue ||
33
+ ((row as ProjectionRecord).closedAt !== undefined && typeof (row as ProjectionRecord).closedAt !== 'string') ||
34
+ ((row as ProjectionRecord).commentPosted !== undefined && typeof (row as ProjectionRecord).commentPosted !== 'boolean'))) {
35
+ throw new Error('invalid decision Linear projection state');
36
+ }
37
+ return value as ProjectionState;
38
+ }
39
+
40
+ function saveState(path: string, state: ProjectionState): void {
41
+ mkdirSync(join(path, '..'), { recursive: true });
42
+ const temp = `${path}.${randomUUID()}.tmp`;
43
+ try {
44
+ writeFileSync(temp, JSON.stringify(state), { mode: 0o600 });
45
+ renameSync(temp, path);
46
+ } finally { if (existsSync(temp)) unlinkSync(temp); }
47
+ }
48
+
49
+ function description(entry: DecisionEntry): string {
50
+ return [`카테고리: ${entry.category}`, ...(entry.dueAt ? [`기한: ${entry.dueAt}`] : []),
51
+ `결정 id: ${entry.id}`, '텔레그램/디스코드 카드에서 정한다',
52
+ ...(entry.refs ?? []).filter(ref => ref.startsWith('http'))].join('\n');
53
+ }
54
+
55
+ /** A single writer holds the file lock across remote creation and local persistence. */
56
+ export async function projectDecisionsToLinear(deps: DecisionLinearProjectionDeps = {}): Promise<DecisionLinearProjectionResult> {
57
+ const result: DecisionLinearProjectionResult = { created: 0, closed: 0, skipped: 0, failed: 0 };
58
+ const config = deps.config ?? getUserConfig();
59
+ if (config.decisions?.linearProjection?.enabled !== true) {
60
+ result.reason = 'decisions.linearProjection.enabled 꺼짐';
61
+ debug.log('decisions.linear', 'skipped', { decisionId: null, issue: null, reason: 'disabled' });
62
+ return result;
63
+ }
64
+ // A dry run only reads the local ledger and state — it must work before a key is set.
65
+ const apiKey = (deps.dryRun ? '' : await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey')) ?? '';
66
+ if (!deps.dryRun && !apiKey) {
67
+ result.reason = 'Linear 키가 없습니다 — `elanous connector linear set-key`';
68
+ debug.log('decisions.linear', 'skipped', { decisionId: null, issue: null, reason: 'missing-key' });
69
+ return result;
70
+ }
71
+ const stateDir = deps.stateDir ?? elanousStateRoot();
72
+ const path = join(stateDir, 'decisions', 'linear-projection.json');
73
+ const ledger = deps.ledger ?? new DecisionLedger({ stateDir });
74
+ const entries = ledger.list({ status: 'all' });
75
+ const fetchFn = deps.fetch ?? fetch;
76
+ const lockPath = `${path}.lock`;
77
+ let lock: number | undefined;
78
+ if (!deps.dryRun) {
79
+ mkdirSync(join(path, '..'), { recursive: true });
80
+ const deadline = Date.now() + 10_000;
81
+ for (;;) {
82
+ try { lock = openSync(lockPath, 'wx', 0o600); writeSync(lock, `${process.pid}`); break; }
83
+ catch (error) {
84
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
85
+ try {
86
+ const pid = Number(readFileSync(lockPath, 'utf8'));
87
+ if (Number.isSafeInteger(pid) && pid > 0 && Date.now() - statSync(lockPath).mtimeMs > 30_000) {
88
+ try { process.kill(pid, 0); }
89
+ catch (e) { if ((e as NodeJS.ErrnoException).code === 'ESRCH') { unlinkSync(lockPath); continue; } }
90
+ }
91
+ } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') continue; }
92
+ if (Date.now() >= deadline) throw new Error('decision Linear projection lock timeout');
93
+ await Bun.sleep(25);
94
+ }
95
+ }
96
+ }
97
+ try {
98
+ const state = readState(path);
99
+ if (deps.dryRun) result.plan = [];
100
+ let projectId: string | undefined;
101
+ for (const entry of entries) {
102
+ let issue = state[entry.id]?.issue;
103
+ try {
104
+ const record = state[entry.id];
105
+ if (entry.status === 'open' && !record) {
106
+ if (deps.dryRun) {
107
+ result.plan!.push({ decisionId: entry.id, action: 'create' });
108
+ result.created++;
109
+ } else {
110
+ projectId ??= await resolveLinearProjectId({ apiKey, project: config.coo?.linearProject ?? '외부 행정·큰 일 (COO)', fetch: fetchFn });
111
+ const existing = await findLinearDecisionIssue({ apiKey, projectId, decisionId: entry.id, fetch: fetchFn });
112
+ issue = existing ?? await createLinearIssue({ apiKey, projectId, title: `[결정] ${entry.title}`, description: description(entry),
113
+ ...(entry.dueAt ? { dueDate: entry.dueAt.slice(0, 10) } : {}), fetch: fetchFn });
114
+ state[entry.id] = { issue };
115
+ saveState(path, state);
116
+ if (existing) {
117
+ result.skipped++;
118
+ debug.log('decisions.linear', 'skipped', { decisionId: entry.id, issue, reason: 'recovered' });
119
+ continue;
120
+ }
121
+ result.created++;
122
+ }
123
+ debug.log('decisions.linear', 'created', { decisionId: entry.id, issue: issue ?? null, reason: deps.dryRun ? 'dry-run' : 'created' });
124
+ } else if (entry.status !== 'open' && record && !record.closedAt) {
125
+ if (deps.dryRun) {
126
+ result.plan!.push({ decisionId: entry.id, action: 'close', issue: record.issue });
127
+ } else {
128
+ if (!record.commentPosted) {
129
+ const body = entry.status === 'withdrawn' ? '철회됨'
130
+ : `결정됨: ${entry.choice ?? '선택 키 미상'} · ${entry.decidedBy?.kind ?? '주체 미상'} · ${entry.decidedAt ?? '시각 미상'}`;
131
+ await commentLinearIssue({ apiKey, issueId: record.issue, body, fetch: fetchFn });
132
+ record.commentPosted = true;
133
+ saveState(path, state);
134
+ }
135
+ await moveLinearIssueToStateType({ apiKey, issueId: record.issue, type: entry.status === 'decided' ? 'completed' : 'canceled', fetch: fetchFn });
136
+ record.closedAt = new Date().toISOString();
137
+ saveState(path, state);
138
+ }
139
+ result.closed++;
140
+ debug.log('decisions.linear', 'closed', { decisionId: entry.id, issue: record.issue, reason: deps.dryRun ? 'dry-run' : entry.status });
141
+ } else {
142
+ result.skipped++;
143
+ debug.log('decisions.linear', 'skipped', { decisionId: entry.id, issue: issue ?? null, reason: record?.closedAt ? 'already-closed' : record ? 'already-created' : 'not-projected' });
144
+ }
145
+ } catch (error) {
146
+ result.failed++;
147
+ const reason = error instanceof Error
148
+ ? (error as NodeJS.ErrnoException).code ?? (/^Linear GraphQL HTTP \d{3}$/.test(error.message) ? error.message : error.name)
149
+ : 'unknown';
150
+ debug.log('decisions.linear', 'failed', { decisionId: entry.id, issue: issue ?? null, reason });
151
+ }
152
+ }
153
+ return result;
154
+ } finally {
155
+ if (lock !== undefined) { closeSync(lock); unlinkSync(lockPath); }
156
+ }
157
+ }