elanous 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -0,0 +1,98 @@
1
+ // The owner hands a seat a task from Telegram: /coo /cto /cmo /cxo → one «[대표] → <seat>» line on the coordination PR,
2
+ // one 'ceo-task' message in the seat store, and a «받음» reply. The Telegram entrypoint writes only for the owner in a private chat.
3
+ // Goal: 내부 문서 `ASK-the-ceo-cannot-hand-a-seat-a-task-from-telegram-2026-10-02` (C2 phase 1).
4
+ import { debug } from '../debug/log.js';
5
+ import type { MessageEnvelope } from '../msg/msg-store.js';
6
+
7
+ export const CEO_SEAT_COMMANDS = { coo: 'OP', cto: 'TC', cmo: 'MK', cxo: 'UX' } as const;
8
+ export type CeoSeatCommand = keyof typeof CEO_SEAT_COMMANDS;
9
+
10
+ export interface CeoCommandContext { chatId: number | string; userId: number | string }
11
+
12
+ export interface CeoCommandDeps {
13
+ /** Telegram user id of the owner (`telegramDecisionOwner`), null when none is configured. */
14
+ ownerId: string | null;
15
+ /** `decisions.replyGhPr` — "owner/repo#N". */
16
+ replyTarget: string | null;
17
+ runGh: (args: string[], stdin: string) => Promise<number>;
18
+ append: (message: MessageEnvelope) => unknown;
19
+ /** Whether the seat inbox already holds a message carrying this request ref (retry after a crash must not deliver twice). */
20
+ hasMessage?: (seat: string, ref: string) => boolean;
21
+ now?: () => Date;
22
+ }
23
+
24
+ function kstStamp(at: Date): string {
25
+ return new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', dateStyle: 'short', timeStyle: 'short' }).format(at);
26
+ }
27
+
28
+ export function ceoTaskLine(seat: string, task: string, at: Date): string {
29
+ return `**[대표]** ${kstStamp(at)} KST → ${seat} · ${task}`;
30
+ }
31
+
32
+ export async function handleCeoSeatCommand(command: CeoSeatCommand, args: string[], ctx: CeoCommandContext, deps: CeoCommandDeps): Promise<string> {
33
+ const seat = CEO_SEAT_COMMANDS[command];
34
+ if (!deps.ownerId || String(ctx.userId) !== deps.ownerId) {
35
+ debug.log('seat.dispatch', 'refused', { seat, reason: 'not-owner' });
36
+ return '소유자만 쓸 수 있습니다.';
37
+ }
38
+ // A group would show the task to every member — private chat only (same rule as /decisions).
39
+ if (String(ctx.chatId) !== String(ctx.userId)) {
40
+ debug.log('seat.dispatch', 'refused', { seat, reason: 'not-private' });
41
+ return '개인 대화에서만 쓸 수 있습니다.';
42
+ }
43
+ const task = args.join(' ').replace(/\s+/g, ' ').trim();
44
+ if (!task) {
45
+ debug.log('seat.dispatch', 'refused', { seat, reason: 'empty' });
46
+ return `사용법: /${command} <할 일>`;
47
+ }
48
+ return (await dispatchCeoTask(seat, task, deps)).reply;
49
+ }
50
+
51
+ export interface CeoTaskExtra {
52
+ attachments?: Array<{ name: string; path: string }>;
53
+ /** Paths supplied by callers that do not have attachment names. */
54
+ attachmentPaths?: readonly string[];
55
+ via?: 'telegram' | 'discord' | 'pwa';
56
+ /** Request ref written into the message body; with `deps.hasMessage` it makes delivery idempotent per request. */
57
+ ref?: string;
58
+ }
59
+
60
+ export interface CeoTaskResult { reply: string; channel: 'posted' | 'failed' | 'unknown'; channelError?: string }
61
+
62
+ /** Dispatch an already-authorized seat request; callers must authenticate before invoking this helper. */
63
+ export async function dispatchCeoTask(seat: string, text: string, deps: CeoCommandDeps, extra: CeoTaskExtra = {}): Promise<CeoTaskResult> {
64
+ const task = text.replace(/\s+/g, ' ').trim();
65
+ if (!task) {
66
+ debug.log('seat.dispatch', 'refused', { seat, reason: 'empty' });
67
+ return { reply: '할 일을 입력해 주세요.', channel: 'unknown' };
68
+ }
69
+ const attachments = extra.attachments ?? [];
70
+ const attachmentPaths = extra.attachmentPaths ?? [];
71
+ const body = `${task}${attachments.map(({ name, path }) => `\n첨부: ${name} — ${path}`).join('')}${attachmentPaths.length ? `\n\n첨부 파일:\n${attachmentPaths.join('\n')}` : ''}${extra.ref ? `\n요청: ${extra.ref}` : ''}`;
72
+ if (extra.ref && deps.hasMessage?.(seat, extra.ref)) {
73
+ // A retry after a crash between delivery and the journal outcome: the message is already there, and whether
74
+ // the channel line went out is unknown — say so instead of posting it again or claiming it was posted.
75
+ debug.log('seat.dispatch', 'ceo-task-already-delivered', { seat, via: extra.via });
76
+ return { reply: `받음 — ${seat} 메시지함에 이미 있습니다.`, channel: 'unknown' };
77
+ }
78
+ deps.append({ from: 'CEO', to: seat, body, kind: 'ceo-task' });
79
+ debug.log('seat.dispatch', 'ceo-task', { seat, ...(extra.via ? { via: extra.via, attachments: attachments.length + attachmentPaths.length } : {}) });
80
+ const channelError = await postToChannel(seat, task, deps, extra);
81
+ if (channelError) {
82
+ debug.log('seat.dispatch', 'channel-failed', { seat, reason: channelError }, { level: 'warn' });
83
+ return { reply: `받음 — ${seat} 메시지함에는 넣었습니다. 다만 조율 채널에 못 남겼습니다(${channelError}).`, channel: 'failed', channelError };
84
+ }
85
+ return { reply: `받음 — ${seat}에 전했습니다.`, channel: 'posted' };
86
+ }
87
+
88
+ async function postToChannel(seat: string, task: string, deps: CeoCommandDeps, extra: CeoTaskExtra): Promise<string | null> {
89
+ const target = /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(deps.replyTarget?.trim() ?? '');
90
+ if (!target) return deps.replyTarget ? 'decisions.replyGhPr 형식이 owner/repo#N 이 아님' : 'decisions.replyGhPr 미설정';
91
+ try {
92
+ const attachmentCount = (extra.attachments?.length ?? 0) + (extra.attachmentPaths?.length ?? 0);
93
+ const code = await deps.runGh(['pr', 'comment', target[2]!, '--repo', target[1]!, '--body-file', '-'], `${ceoTaskLine(seat, task, deps.now?.() ?? new Date())}${attachmentCount ? ` · 첨부 ${attachmentCount}` : ''}${extra.via === 'pwa' ? ' (PWA)' : ''}`);
94
+ return code === 0 ? null : `gh 종료 코드 ${code}`;
95
+ } catch (error) {
96
+ return error instanceof Error ? error.message.slice(0, 80) : 'gh 실행 실패';
97
+ }
98
+ }
@@ -0,0 +1,38 @@
1
+ import { runGh } from '../decisions/decision-cards.js';
2
+ import { openMsgStore } from '../msg/msg-store.js';
3
+ import type { UserConfig } from '../user-config.js';
4
+ import type { CeoCommandDeps } from './ceo-commands.js';
5
+
6
+ /** Endings are ordered from explicit punctuation to Korean question endings. Every row has a test example. */
7
+ export const CEO_QUESTION_ENDINGS = [
8
+ /[??]$/,
9
+ /야$/,
10
+ /니$/,
11
+ /나요$/,
12
+ /까요$/,
13
+ /인가요$/,
14
+ /어때$/,
15
+ /알려\s?줘$/,
16
+ /보여줘$/,
17
+ /상황$/,
18
+ /어디까지$/,
19
+ ] as const;
20
+
21
+ export function classifyCeoIntent(text: string): 'question' | 'task' {
22
+ const body = text.trim();
23
+ return CEO_QUESTION_ENDINGS.some((ending) => ending.test(body)) ? 'question' : 'task';
24
+ }
25
+
26
+ /** The same channel + seat inbox delivery dependencies used by /coo, /cto, /cmo and /cxo. */
27
+ export function ceoTaskDeps(cfg: UserConfig, ownerId: string | null): CeoCommandDeps {
28
+ const replyTarget = (cfg.raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
29
+ return {
30
+ ownerId,
31
+ replyTarget: typeof replyTarget === 'string' ? replyTarget : null,
32
+ runGh,
33
+ append: (message) => {
34
+ const store = openMsgStore();
35
+ try { return store.append(message); } finally { store.close(); }
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,231 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { Database } from 'bun:sqlite';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
7
+ import { debug } from '../debug/log.js';
8
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
9
+ import { getUserConfig, type SeatLoopConfig } from '../user-config.js';
10
+ import { listChecklist } from '../release-loop/checklist.js';
11
+ import { readSchedules } from '../release-loop/feature-store.js';
12
+
13
+ // `kind`/`createdAt` are the V3 shadow-compare keys (내부 문서 `METHOD-v3-shadow-compare-2026-10-02` · MK 10-02 18:54).
14
+ export type SeatItem = { source: 'request' | 'checklist'; kind?: 'request' | 'cell'; id: string; title: string; text: string; version?: string; queuedAt?: string; createdAt?: string };
15
+ export type SeatInputs = { requests: SeatItem[]; checklist: SeatItem[]; role: string };
16
+ // V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door or skipped-*) · `reason` (forbidden word for a decision).
17
+ export type SeatAction = 'decision' | 'harness' | 'skipped-budget' | 'skipped-empty';
18
+ export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'launched' | 'hitl' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; action?: SeatAction; reason?: string; runId?: string };
19
+ export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
20
+ export type SeatDeps = {
21
+ root?: string;
22
+ repo?: string;
23
+ config?: SeatLoopConfig;
24
+ now?: () => Date;
25
+ read?: (path: string) => string;
26
+ run?: (args: string[]) => Promise<string>;
27
+ append?: (path: string, entry: SeatEntry) => void;
28
+ versions?: () => string[];
29
+ /** Checklist items of one version — default reads the release ledger DB (checklist.json is only a legacy import source since REL5b). */
30
+ checklistItems?: (version: string) => Array<{ id: string; title: string; status: string; owner?: string | null }>;
31
+ ledgerFiles?: (directory: string) => string[];
32
+ lockContended?: () => void;
33
+ };
34
+
35
+ const repoRoot = resolve(import.meta.dir, '../..');
36
+ const exec = promisify(execFile);
37
+ const defaultRead = (path: string): string => {
38
+ try { return readFileSync(path, 'utf8'); }
39
+ catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return ''; throw error; }
40
+ };
41
+ const defaultRun = async (args: string[]): Promise<string> => {
42
+ const { argv, env } = universeLaunch(args);
43
+ const { stdout } = await exec('bun', argv, {
44
+ cwd: repoRoot, env, encoding: 'utf8',
45
+ timeout: args[0] === 'harness' && args[1] === 'say' ? 3_600_000 : 120_000, maxBuffer: 4 * 1024 * 1024,
46
+ });
47
+ return stdout;
48
+ };
49
+ const defaultAppend = (path: string, entry: SeatEntry): void => {
50
+ mkdirSync(dirname(path), { recursive: true });
51
+ appendFileSync(path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
52
+ };
53
+ const observe = (event: string, data: Record<string, unknown>): void => {
54
+ try { debug.log('seat.loop', event, data); } catch { /* logging cannot change execution */ }
55
+ };
56
+
57
+ // SQLite's OS lock survives the awaited external call and is released if the process exits.
58
+ async function withSeatLock<T>(directory: string, action: () => Promise<T>, contended?: () => void): Promise<T> {
59
+ mkdirSync(directory, { recursive: true });
60
+ const path = join(directory, '.once.mutex.sqlite');
61
+ const db = new Database(path, { create: true, strict: true });
62
+ try {
63
+ chmodSync(path, 0o600);
64
+ const deadline = Date.now() + 120_000;
65
+ let signalled = false;
66
+ while (true) {
67
+ try { db.exec('BEGIN IMMEDIATE'); break; }
68
+ catch (error) {
69
+ if ((error as { code?: string }).code !== 'SQLITE_BUSY' || Date.now() >= deadline) throw error;
70
+ if (!signalled) { contended?.(); signalled = true; }
71
+ await new Promise((done) => setTimeout(done, 25));
72
+ }
73
+ }
74
+ try { return await action(); }
75
+ finally { db.exec('ROLLBACK'); }
76
+ } finally { db.close(); }
77
+ }
78
+
79
+ export function seatDay(now: Date): string {
80
+ return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
81
+ }
82
+
83
+ export function seatLedgerPath(seat: string, root: string, now: Date): string {
84
+ return join(root, 'seat-loop', seat, `${seatDay(now)}.jsonl`);
85
+ }
86
+
87
+ function rows(text: string): Record<string, unknown>[] {
88
+ return text.split('\n').filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
89
+ }
90
+
91
+ function versionOrder(a: string, b: string): number {
92
+ const aa = a.split('.').map(Number), bb = b.split('.').map(Number);
93
+ return (aa[0]! - bb[0]!) || (aa[1]! - bb[1]!) || (aa[2]! - bb[2]!);
94
+ }
95
+
96
+ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}): Promise<SeatInputs> {
97
+ const read = deps.read ?? defaultRead;
98
+ const root = deps.root ?? effectiveInstanceRoot();
99
+ const requests = new Map<string, Record<string, unknown>>();
100
+ for (const row of rows(read(join(root, 'seat-requests', 'requests.jsonl')))) {
101
+ if (typeof row.key === 'string') requests.set(row.key, row);
102
+ }
103
+ const pending = [...requests.values()]
104
+ .filter((row) => row.seat === seat && (row.status === 'pending' || row.status === 'queued') && typeof row.text === 'string')
105
+ .map((row) => ({ source: 'request' as const, kind: 'request' as const, id: String(row.key), title: String(row.text), text: String(row.text), queuedAt: String(row.queuedAt ?? ''), createdAt: String(row.queuedAt ?? '') }))
106
+ .sort((a, b) => a.queuedAt.localeCompare(b.queuedAt) || a.id.localeCompare(b.id));
107
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
108
+ const versions = deps.versions ?? (() => {
109
+ const dir = join(releaseRoot, 'release');
110
+ const fromDirs = existsSync(dir) ? readdirSync(dir).filter((v) => /^\d+\.\d+\.\d+$/.test(v)) : [];
111
+ let fromSchedule: string[] = [];
112
+ try { fromSchedule = readSchedules().map((row) => row.version); } catch { /* ledger unreadable — directories only */ }
113
+ return [...new Set([...fromDirs, ...fromSchedule])].filter((v) => /^\d+\.\d+\.\d+$/.test(v));
114
+ });
115
+ const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
116
+ const checklist: SeatItem[] = [];
117
+ for (const version of versions().sort(versionOrder)) {
118
+ let items: ReturnType<typeof itemsOf>;
119
+ try { items = itemsOf(version); } catch (error) {
120
+ debug.log('seat.loop', 'checklist-unreadable', { seat, version, error: String(error).slice(0, 200) });
121
+ continue;
122
+ }
123
+ for (const item of items) {
124
+ if (item.owner === seat && (item.status === 'yellow' || item.status === 'red')) {
125
+ checklist.push({ source: 'checklist', kind: 'cell', version, id: item.id, title: item.title, text: item.title });
126
+ }
127
+ }
128
+ }
129
+ checklist.sort((a, b) => versionOrder(a.version!, b.version!) || a.id.localeCompare(b.id));
130
+ return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
131
+ }
132
+
133
+ function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
134
+
135
+ // In shadow mode a shadowed item counts as handled, so a rehearsal day walks the queue like the seat would.
136
+ export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean } = {}): SeatItem | null {
137
+ const handled = new Set(ledger.filter((entry) => entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'attempting' || entry.status === 'outcome-unknown' || (opts.shadow === true && entry.status === 'shadow'))
138
+ .filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string').map((entry) => itemKey(entry.item)));
139
+ return [...inputs.requests, ...inputs.checklist].find((item) => !handled.has(itemKey(item))) ?? null;
140
+ }
141
+
142
+ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' | 'harness'; text: string; reason?: string } {
143
+ const hit = `${item.id}\n${item.title}\n${item.text}`.match(TRACK_AGENT_FORBIDDEN_ACTION_REGEX);
144
+ const kind = hit ? 'decision' : 'harness';
145
+ return { kind, text: seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`, ...(hit ? { reason: hit[0] } : {}) };
146
+ }
147
+
148
+ // The launched sentence names the seat, the source and the release so same-named items stay distinct.
149
+ export function seatTaskText(seat: string, item: SeatItem): string {
150
+ const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : `자리 요청 ${item.id}`;
151
+ return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
152
+ }
153
+
154
+ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
155
+ if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
156
+ const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'off' };
157
+ if (config.mode === 'off' || !(config.seats ?? ['MK']).includes(seat)) {
158
+ observe('skipped-off', { seat });
159
+ return { seat, status: 'skipped-off' };
160
+ }
161
+ const now = (deps.now ?? (() => new Date()))();
162
+ const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
163
+ const directory = dirname(path);
164
+ return withSeatLock(directory, async () => {
165
+ const files = deps.ledgerFiles?.(directory) ?? (existsSync(directory) ? readdirSync(directory) : []);
166
+ const ledger = files.filter((name) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(name) && name <= `${seatDay(now)}.jsonl`)
167
+ .flatMap((name) => rows((deps.read ?? defaultRead)(join(directory, name))) as SeatEntry[]);
168
+ const inputs = await gatherSeatInputs(seat, deps);
169
+ const item = pickNext(inputs, ledger, { shadow: config.mode === 'shadow' });
170
+ if (item) observe('picked', { seat, item, date: seatDay(now) });
171
+ const planned = item ? planAction(item, seat) : undefined;
172
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
173
+ ...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
174
+ const append = deps.append ?? defaultAppend;
175
+ if (config.mode === 'shadow' || !item) {
176
+ append(path, entry);
177
+ if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
178
+ return entry;
179
+ }
180
+ const run = deps.run ?? defaultRun;
181
+ let outcome: string | undefined;
182
+ try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
183
+ catch { /* failed budget observation is never permission to launch */ }
184
+ if (outcome !== 'proceed' && outcome !== 'next-provider') {
185
+ entry.status = 'skipped-budget';
186
+ entry.action = 'skipped-budget';
187
+ delete entry.reason;
188
+ append(path, entry);
189
+ observe('skipped-budget', { seat, item, outcome: outcome ?? 'unavailable' });
190
+ return entry;
191
+ }
192
+ const action = planned!;
193
+ // Persist the intent before crossing the process boundary: an unreturned call may already have acted.
194
+ append(path, { ...entry, status: 'attempting' });
195
+ try {
196
+ if (action.kind === 'decision') {
197
+ const title = `${seat}: ${action.text.replace(/[\r\n]+/g, ' ')}`;
198
+ const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
199
+ '--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}\n역할: ${inputs.role}`, '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
200
+ '--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
201
+ '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--agent', 'seat-loop', '--json']);
202
+ const decision: unknown = JSON.parse(raised.trim());
203
+ if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
204
+ throw new Error('decisions raise returned no decision id');
205
+ }
206
+ entry.status = 'hitl';
207
+ append(path, entry);
208
+ observe('hitl', { seat, item });
209
+ return entry;
210
+ }
211
+ const output = await run(['harness', 'say', action.text, '--substrate', 'pod', '--pod-pool', config.podPool ?? 'pool-node-b@node-b:8', '--base', 'main', '--json']);
212
+ const results: unknown = JSON.parse(output.trim());
213
+ if (!Array.isArray(results) || results.length === 0 || results.some((result) =>
214
+ !result || typeof result !== 'object' || (result as { status?: unknown }).status !== 'done')) {
215
+ throw new Error('harness say did not complete successfully');
216
+ }
217
+ const runId = (results[0] as { runId?: unknown }).runId;
218
+ if (typeof runId !== 'string' || !/^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(runId)) {
219
+ throw new Error('harness say output has no runId');
220
+ }
221
+ entry.status = 'launched';
222
+ entry.runId = runId;
223
+ append(path, entry);
224
+ observe('launched', { seat, item, runId });
225
+ return entry;
226
+ } catch (error) {
227
+ append(path, { ...entry, status: 'outcome-unknown' });
228
+ throw error;
229
+ }
230
+ }, deps.lockContended);
231
+ }
@@ -0,0 +1,64 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { debug } from '../debug/log.js';
7
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
8
+ import { getUserConfig, type SeatLoopConfig } from '../user-config.js';
9
+ import { seatDay, seatLedgerPath, type SeatEntry } from './seat-loop.js';
10
+
11
+ export type SeatReportDeps = {
12
+ root?: string;
13
+ repo?: string;
14
+ now?: () => Date;
15
+ read?: (path: string) => string;
16
+ config?: SeatLoopConfig;
17
+ post?: boolean;
18
+ send?: (body: string, seat: string, pr: number) => Promise<void> | void;
19
+ };
20
+ export type SeatReportResult = { seat: string; date: string; body: string; posted: boolean };
21
+ const repoRoot = resolve(import.meta.dir, '../..');
22
+ const exec = promisify(execFile);
23
+
24
+ async function defaultSend(body: string, seat: string, pr: number, repo: string): Promise<void> {
25
+ const dir = mkdtempSync(join(tmpdir(), 'seat-report-'));
26
+ try {
27
+ const file = join(dir, 'body.md');
28
+ writeFileSync(file, body);
29
+ await exec('bash', [join(repo, 'scripts', 'coord-post.sh'), file], {
30
+ cwd: repo, env: { ...process.env, COORD_ID: seat, CH_PR: String(pr) }, timeout: 30_000,
31
+ });
32
+ } finally { rmSync(dir, { recursive: true, force: true }); }
33
+ }
34
+
35
+ export async function seatReport(seat: string, deps: SeatReportDeps = {}): Promise<SeatReportResult> {
36
+ if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
37
+ const now = (deps.now ?? (() => new Date()))();
38
+ const date = seatDay(now);
39
+ let raw = '';
40
+ try { raw = (deps.read ?? ((path) => readFileSync(path, 'utf8')))(seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now)); }
41
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
42
+ const entries = raw.split('\n').filter(Boolean).map((line) => JSON.parse(line) as SeatEntry);
43
+ // One line per item, its last state wins: an `attempting` row followed by `launched` is not «unconfirmed».
44
+ const latest = new Map<string, SeatEntry>();
45
+ for (const entry of entries) {
46
+ const key = entry.item ? `${entry.item.source}:${entry.item.version ?? ''}:${entry.item.id}` : `none:${entry.status}`;
47
+ latest.delete(key);
48
+ latest.set(key, entry);
49
+ }
50
+ const detail = [...latest.values()].map((entry) => {
51
+ const label = entry.item ? `${entry.item.version ? `${entry.item.version} ` : ''}${entry.item.id} ${entry.item.title}`.replace(/\s+/g, ' ').trim() : '배정 없음';
52
+ if (entry.status === 'launched') return `발사 ${label} (${entry.runId ?? 'runId 없음'})`;
53
+ if (entry.status === 'hitl') return `결정 상정 ${label}`;
54
+ if (entry.status === 'shadow') return `shadow ${label}`;
55
+ if (entry.status === 'attempting' || entry.status === 'outcome-unknown') return `결과 확인 필요 ${entry.status} ${label}`;
56
+ return `건너뜀 ${entry.status} ${label}`;
57
+ });
58
+ const body = `**[${seat}]** {{TS}} → 보고 ${date}: ${detail.join(' · ') || '원장 기록 없음'}`;
59
+ const pr = (deps.config ?? getUserConfig().loops?.seat)?.reportPr;
60
+ const posted = deps.post === true && pr !== undefined;
61
+ if (posted) await (deps.send ?? ((text, id, number) => defaultSend(text, id, number, deps.repo ?? repoRoot)))(body, seat, pr);
62
+ try { debug.log('seat.loop', 'reported', { seat, date, entries: entries.length, posted }); } catch { /* observation is fail-soft */ }
63
+ return { seat, date, body, posted };
64
+ }
@@ -156,6 +156,8 @@ interface SelfDevSubtaskGradeRationale {
156
156
  }
157
157
 
158
158
  export interface SelfDevDecompositionObservation extends SelfDevDecompositionMeta {
159
+ /** Number of registration-only goals folded into their dependency. */
160
+ foldedRegistration: number;
159
161
  /** Number of retained subtasks whose original feature text grades `too_large`. */
160
162
  tooLargeSubtaskCount: number;
161
163
  /** Number of retained subtasks whose original feature text grades `too_small`. */
@@ -250,7 +252,7 @@ function observeDecomposerSelection(selection: SelfDevDecomposerSelection, obser
250
252
  }
251
253
 
252
254
  function applyGoalDefaultsAndHotPaths(goals: SelfDevGoal[], defaults: { base?: string; autoMerge?: boolean }): SelfDevGoal[] {
253
- return pruneDanglingDependencies(goals.map((goal) => {
255
+ return goals.map((goal) => {
254
256
  const hotPaths = [...new Set([...(goal.hotPaths ?? []), ...inferHotPaths(goal.feature ?? '')])];
255
257
  return {
256
258
  ...goal,
@@ -258,7 +260,55 @@ function applyGoalDefaultsAndHotPaths(goals: SelfDevGoal[], defaults: { base?: s
258
260
  ...(defaults.base !== undefined ? { base: defaults.base } : {}),
259
261
  ...(defaults.autoMerge !== undefined ? { autoMerge: defaults.autoMerge } : {}),
260
262
  };
261
- }));
263
+ });
264
+ }
265
+
266
+ const REGISTRATION_FILES = new Set([
267
+ 'src/nexus/api/http-server.ts',
268
+ 'src/index.ts',
269
+ 'src/telegram-commands.ts',
270
+ ]);
271
+
272
+ function isRegistrationFile(path: string): boolean {
273
+ return REGISTRATION_FILES.has(path) || /^graphs\/(?:[^/]+\/)*[^/]+\.yaml$/.test(path);
274
+ }
275
+
276
+ /** Fold only registration-only goals into their one retained dependency. */
277
+ export function foldRegistrationOnlyGoals(goals: SelfDevGoal[]): SelfDevGoal[] {
278
+ let remaining = [...goals];
279
+ let changed = true;
280
+ while (changed) {
281
+ changed = false;
282
+ for (const registration of remaining) {
283
+ const files = registration.hotPaths ?? [];
284
+ if (files.length === 0 || !files.every(isRegistrationFile) || registration.dependsOn?.length !== 1) continue;
285
+ const dependencyId = registration.dependsOn[0];
286
+ const targetIndex = remaining.findIndex((goal) => goal.id === dependencyId && goal !== registration);
287
+ if (targetIndex < 0) continue;
288
+ const target = remaining[targetIndex]!;
289
+ remaining = remaining.filter((goal) => goal !== registration).map((goal) => {
290
+ if (goal === target) {
291
+ const dependsOn = goal.dependsOn?.filter((id) => id !== registration.id && id !== goal.id);
292
+ const { dependsOn: _old, ...rest } = goal;
293
+ return {
294
+ ...rest,
295
+ ...(dependsOn?.length ? { dependsOn } : {}),
296
+ feature: `${goal.feature}\nAlso register: ${registration.feature}`,
297
+ hotPaths: [...new Set([...(goal.hotPaths ?? []), ...files])],
298
+ };
299
+ }
300
+ if (!goal.dependsOn?.includes(registration.id!)) return goal;
301
+ const dependsOn = [...new Set(goal.dependsOn.map((id) => id === registration.id ? target.id! : id))]
302
+ .filter((id) => id !== goal.id);
303
+ const { dependsOn: _old, ...rest } = goal;
304
+ return dependsOn.length ? { ...rest, dependsOn } : rest;
305
+ });
306
+ debug.log('self-dev.decompose', 'fold-registration', { from: registration.id, into: target.id, files });
307
+ changed = true;
308
+ break;
309
+ }
310
+ }
311
+ return remaining;
262
312
  }
263
313
 
264
314
  function observeDecomposition(
@@ -267,12 +317,14 @@ function observeDecomposition(
267
317
  arcClassification?: { arcs: SelfDevArc[]; hintDeviation?: { requested: number; actual: number } },
268
318
  observationContext: SelfDevDecompositionObservationContext = {},
269
319
  pathGroundingOptions?: DecomposePathGroundingOptions,
320
+ foldedRegistration = 0,
270
321
  ): SelfDevDecomposition {
271
322
  const retainedSubtasks = decomposition.outcome === 'decomposed' ? goals : [];
272
323
  const gradedSubtasks = retainedSubtasks.map(gradeRetainedSubtask);
273
324
  const tooLargeSubtaskCount = gradedSubtasks.filter(({ grade }) => grade.verdict === 'too_large').length;
274
325
  const observation: SelfDevDecompositionObservation & { goalId: string | null; runId: string | null } = {
275
326
  ...decomposition,
327
+ foldedRegistration,
276
328
  tooLargeSubtaskCount,
277
329
  tooSmallSubtaskCount: gradedSubtasks.filter(({ grade }) => grade.verdict === 'too_small').length,
278
330
  hasMultipleTooLargeSubtasks: tooLargeSubtaskCount >= 2,
@@ -467,14 +519,15 @@ async function finalizeDecomposition(
467
519
  decomposition: SelfDevDecompositionMeta,
468
520
  opts: SelfDevDecomposeOptions,
469
521
  arcHint: number | undefined,
522
+ foldedRegistration = 0,
470
523
  ): Promise<SelfDevDecomposition> {
471
- if (arcHint === undefined || goals.length === 0) return observeDecomposition(goals, decomposition, undefined, opts.observation, opts.pathGrounding);
524
+ if (arcHint === undefined || goals.length === 0) return observeDecomposition(goals, decomposition, undefined, opts.observation, opts.pathGrounding, foldedRegistration);
472
525
  try {
473
526
  const arcLlm = opts.arcLlm ?? opts.llm ?? (await defaultDecomposeLlm(opts.model));
474
527
  const tasks = goals.map((goal) => ({ id: goal.id ?? '', title: goal.feature ?? '', description: goal.feature, dependsOn: goal.dependsOn }));
475
528
  const grouping = parseSelfDevArcGrouping(await arcLlm(buildSelfDevArcClassifyPrompt(feature, tasks, arcHint)));
476
529
  const classification = grouping ? deriveSelfDevArcsFromGrouping(tasks, grouping, arcHint) : null;
477
- if (!classification) return observeDecomposition(goals, decomposition, undefined, opts.observation, opts.pathGrounding);
530
+ if (!classification) return observeDecomposition(goals, decomposition, undefined, opts.observation, opts.pathGrounding, foldedRegistration);
478
531
  const arcByTask = new Map(classification.arcs.flatMap((arc) => arc.taskIds.map((taskId) => [taskId, arc])));
479
532
  const taskIdsForArc = new Map(classification.arcs.map((arc) => [arc.id, arc.taskIds]));
480
533
  const orderedGoals = goals.map((goal) => {
@@ -482,9 +535,9 @@ async function finalizeDecomposition(
482
535
  const dependsOn = [...new Set([...(goal.dependsOn ?? []), ...arcDependencies])].filter((id) => id !== goal.id);
483
536
  return dependsOn.length ? { ...goal, dependsOn } : goal;
484
537
  });
485
- return observeDecomposition(pruneDanglingDependencies(orderedGoals), decomposition, classification, opts.observation, opts.pathGrounding);
538
+ return observeDecomposition(pruneDanglingDependencies(orderedGoals), decomposition, classification, opts.observation, opts.pathGrounding, foldedRegistration);
486
539
  } catch {
487
- return observeDecomposition(goals, decomposition, undefined, opts.observation, opts.pathGrounding);
540
+ return observeDecomposition(goals, decomposition, undefined, opts.observation, opts.pathGrounding, foldedRegistration);
488
541
  }
489
542
  }
490
543
 
@@ -514,17 +567,18 @@ export async function decomposeSelfDevGoal(
514
567
  }
515
568
  observeDecomposerSelection('fabric', opts.observation);
516
569
  const actualTaskCount = result.goals.length;
517
- const goals = actualTaskCount > 0
570
+ const prepared = actualTaskCount > 0
518
571
  ? applyGoalDefaultsAndHotPaths(result.goals.slice(0, hardMaxTasks), defaults)
519
572
  : [single];
520
- return finalizeDecomposition(feature, goals, {
573
+ const goals = foldRegistrationOnlyGoals(prepared);
574
+ return finalizeDecomposition(feature, pruneDanglingDependencies(goals), {
521
575
  recommendedMaxTasks: maxTasks,
522
576
  actualTaskCount,
523
577
  truncatedAtHardMax: actualTaskCount > hardMaxTasks,
524
578
  exceededRecommendedMax: actualTaskCount > maxTasks,
525
579
  outcome: actualTaskCount === 0 ? 'single-no-subtasks' : 'decomposed',
526
580
  omittedGoalCount: result.omittedGoalCount,
527
- }, opts, actualTaskCount > 0 ? arcHint : undefined);
581
+ }, opts, actualTaskCount > 0 ? arcHint : undefined, prepared.length - goals.length);
528
582
  }
529
583
 
530
584
  observeDecomposerSelection('default', opts.observation);
@@ -539,8 +593,9 @@ export async function decomposeSelfDevGoal(
539
593
  const actualTaskCount = rawSubtasks.length;
540
594
  const decomposition: SelfDevDecompositionMeta = { recommendedMaxTasks: maxTasks, actualTaskCount, truncatedAtHardMax: actualTaskCount > hardMaxTasks, exceededRecommendedMax: actualTaskCount > maxTasks, outcome: actualTaskCount === 0 ? 'single-no-subtasks' : 'decomposed' };
541
595
  const parsedGoals = parseSelfDevDecomposition(JSON.stringify({ subtasks: rawSubtasks.slice(0, hardMaxTasks) }), defaults);
542
- const goals = parsedGoals.length > 0 ? pruneDanglingDependencies(parsedGoals) : [single];
543
- return finalizeDecomposition(feature, goals, decomposition, opts, parsedGoals.length > 0 ? arcHint : undefined);
596
+ const foldedGoals = foldRegistrationOnlyGoals(parsedGoals);
597
+ const goals = parsedGoals.length > 0 ? pruneDanglingDependencies(foldedGoals) : [single];
598
+ return finalizeDecomposition(feature, goals, decomposition, opts, parsedGoals.length > 0 ? arcHint : undefined, parsedGoals.length - foldedGoals.length);
544
599
  }
545
600
 
546
601
  /** Default LLM seam — lazy `streamLLM` wire (kept out of the test path). */
@@ -111,6 +111,8 @@ function runPath(runId: string, dir: string): string {
111
111
  return path;
112
112
  }
113
113
 
114
+ const loggedAbsentCheckpoints = new Set<string>();
115
+
114
116
  function loadSelfDevRunFromPath(path: string): SelfDevRunState | null {
115
117
  const runId = path.slice(path.lastIndexOf(sep) + 1, -'.json'.length);
116
118
  try {
@@ -118,7 +120,14 @@ function loadSelfDevRunFromPath(path: string): SelfDevRunState | null {
118
120
  if (o && typeof o.runId === 'string' && Array.isArray(o.results)) return o;
119
121
  try { debug.log('self-dev.run-store', 'read-failed', { runId, reason: 'invalid checkpoint shape' }); } catch { /* preserve fail-soft reads */ }
120
122
  } catch (error) {
121
- try { debug.log('self-dev.run-store', 'read-failed', { runId, reason: String(error) }); } catch { /* preserve fail-soft reads */ }
123
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
124
+ if (!loggedAbsentCheckpoints.has(runId)) {
125
+ loggedAbsentCheckpoints.add(runId);
126
+ try { debug.log('self-dev.run-store', 'checkpoint-absent', { runId }); } catch { /* preserve fail-soft reads */ }
127
+ }
128
+ } else {
129
+ try { debug.log('self-dev.run-store', 'read-failed', { runId, reason: String(error) }); } catch { /* preserve fail-soft reads */ }
130
+ }
122
131
  }
123
132
  return null;
124
133
  }