elanous 0.2.16 → 0.2.18

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 (354) 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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  67. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -0
  87. package/apps/pwa/out/decisions/index.txt +30 -0
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +14 -14
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +14 -14
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +14 -14
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/intake/intake-daily.yaml +9 -2
  160. package/graphs/intake/news-sources.yaml +4 -0
  161. package/graphs/intake/recipes.yaml +6 -1
  162. package/package.json +2 -1
  163. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  164. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  165. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  166. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  167. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  168. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  169. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  170. package/scripts/__fixtures__/intake-news.xml +7 -0
  171. package/scripts/ci-public-export-import-gate.ts +7 -2
  172. package/scripts/ci-typecheck-changed.ts +21 -6
  173. package/scripts/discord-seat-probe.ts +3 -2
  174. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  175. package/scripts/goals/authoring-metrics.ts +73 -0
  176. package/scripts/intake-news.ts +169 -0
  177. package/scripts/landing-heal/must-fix-match.ts +7 -7
  178. package/scripts/landing-heal/run-mission.ts +3 -2
  179. package/scripts/public-export-test-run.ts +37 -2
  180. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  181. package/scripts/release-loop/cut-branch.ts +2 -1
  182. package/scripts/release-loop/cutoff.ts +4 -2
  183. package/scripts/release-loop/docs-land-node.ts +77 -11
  184. package/scripts/release-loop/docs-node.ts +1 -1
  185. package/scripts/release-loop/gate-node.ts +99 -14
  186. package/scripts/release-loop/node-verdict.ts +2 -1
  187. package/scripts/release-loop/npm-publish-node.ts +11 -7
  188. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  189. package/scripts/release-loop/preflight.ts +246 -0
  190. package/scripts/release-loop/publish-node.ts +10 -3
  191. package/scripts/release-loop/release-docs.ts +134 -0
  192. package/scripts/release-loop/release-readiness.ts +11 -6
  193. package/scripts/release-loop/release-version.ts +48 -0
  194. package/scripts/release-loop/resume-release.ts +115 -0
  195. package/scripts/release-loop/unattended-release.ts +43 -7
  196. package/scripts/release-loop/version-node.ts +149 -41
  197. package/scripts/release-story/draft.ts +42 -14
  198. package/scripts/rhythm/daily.ts +25 -4
  199. package/scripts/skills/public-check.ts +62 -12
  200. package/scripts/test-deterministic.ts +51 -33
  201. package/scripts/test-diet/effectiveness.ts +62 -1
  202. package/scripts/test-diet/lib.ts +1 -1
  203. package/scripts/test-diet/node.ts +8 -1
  204. package/src/agent-mission/driver.ts +53 -10
  205. package/src/agent-mission/review-loop.ts +4 -3
  206. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  207. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  208. package/src/autopilot/track-agent.ts +5 -3
  209. package/src/briefing/brief-items.ts +86 -17
  210. package/src/cli/autopilot-cli.ts +86 -1
  211. package/src/cli/brief-cli.ts +36 -2
  212. package/src/cli/decisions-cli.ts +4 -1
  213. package/src/cli/freeze-cli.ts +3 -2
  214. package/src/cli/intake-cli.ts +74 -2
  215. package/src/cli/log-event-names.ts +1 -0
  216. package/src/cli/notify-cli.ts +75 -0
  217. package/src/cli/pr-cli.ts +12 -1
  218. package/src/cli/provider-cli.ts +24 -0
  219. package/src/cli/release-cli.ts +101 -17
  220. package/src/cli/self-update-canary.ts +46 -0
  221. package/src/cli/self-update.ts +200 -1
  222. package/src/cli/tasks-cli.ts +309 -3
  223. package/src/context-bus/condense.ts +19 -3
  224. package/src/context-bus/context-day.ts +77 -24
  225. package/src/context-bus/context-first.ts +33 -0
  226. package/src/context-card/index.ts +45 -0
  227. package/src/context-card/task.ts +76 -0
  228. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  229. package/src/decisions/decision-ledger.ts +10 -1
  230. package/src/discord-self-message.ts +20 -2
  231. package/src/discord-slash-wire.ts +1 -0
  232. package/src/domains/outbound-alert.ts +200 -27
  233. package/src/domains/outbound-log-probe.ts +16 -1
  234. package/src/domains/release-tool.ts +2 -2
  235. package/src/execution-loop/gates.ts +11 -2
  236. package/src/flow-loop/split.ts +8 -4
  237. package/src/git-fs/worktree.ts +23 -3
  238. package/src/harness/harness-cli-command.ts +151 -11
  239. package/src/harness/harness-pod-dispatch.ts +11 -0
  240. package/src/harness/harness-queue.ts +466 -32
  241. package/src/harness/helper-scan.ts +10 -3
  242. package/src/harness/nested-elanous-policy.ts +23 -4
  243. package/src/hq/fence-wrapper.ts +123 -0
  244. package/src/hq/hq.ts +8 -0
  245. package/src/hq/seats.ts +369 -0
  246. package/src/index.ts +190 -23
  247. package/src/intake-plane/adopt-gate.ts +49 -0
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/collect-github.ts +57 -25
  250. package/src/intake-plane/digest.ts +39 -4
  251. package/src/intake-plane/discord-seat-work.ts +8 -2
  252. package/src/intake-plane/requirement-funnel.ts +316 -0
  253. package/src/knowledge/know-find.ts +10 -1
  254. package/src/loops/budget.ts +36 -0
  255. package/src/loops/orchestrator/finish-rate.ts +73 -1
  256. package/src/loops/orchestrator/grid-place.ts +11 -0
  257. package/src/loops/orchestrator/tick.ts +295 -20
  258. package/src/maturity/feature-maturity.ts +3 -2
  259. package/src/nexus/api/decisions-route.ts +11 -1
  260. package/src/nexus/api/grid.ts +61 -0
  261. package/src/nexus/api/http-server.ts +19 -1
  262. package/src/nexus/api/loop-edges.ts +69 -13
  263. package/src/nexus/api/ops-seats-sources.ts +2 -2
  264. package/src/nexus/api/task-cards-api.ts +19 -0
  265. package/src/nexus/api/workflow-templates.ts +3 -0
  266. package/src/oauth/codex.ts +120 -2
  267. package/src/onboarding/first-chat-gate.ts +29 -0
  268. package/src/release-loop/checklist.ts +104 -6
  269. package/src/release-loop/feature-store.ts +10 -9
  270. package/src/release-loop/landing-freeze.ts +24 -14
  271. package/src/release-loop/placement.ts +13 -3
  272. package/src/release-loop/rubric.ts +80 -0
  273. package/src/seat-dispatch/seat-ask.ts +79 -16
  274. package/src/seat-loop/seat-loop.ts +107 -37
  275. package/src/self-dev/ask-launch-flow.ts +50 -10
  276. package/src/self-dev/ask-path-candidates.ts +68 -0
  277. package/src/self-dev/dev-pipeline.ts +4 -0
  278. package/src/self-dev/draft-triage-rules.ts +1 -1
  279. package/src/self-dev/orchestrate-cli.ts +22 -0
  280. package/src/self-dev/round-pr-supersede.ts +244 -0
  281. package/src/self-dev/run-store.ts +5 -1
  282. package/src/self-dev/run-supervisor.ts +21 -1
  283. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  284. package/src/self-implement/follow-up-goals.ts +2 -2
  285. package/src/self-implement/frozen-merges.ts +25 -20
  286. package/src/self-implement/gate-remote.ts +427 -0
  287. package/src/self-implement/goal-author-clarification.ts +81 -0
  288. package/src/self-implement/goal-author-cli.ts +33 -5
  289. package/src/self-implement/goal-author.ts +93 -12
  290. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  291. package/src/self-implement/goal-document-write.ts +28 -0
  292. package/src/self-implement/heal-intake.ts +4 -0
  293. package/src/self-implement/host-regate.ts +3 -1
  294. package/src/self-implement/l8-merge-queue.ts +29 -5
  295. package/src/self-implement/orchestrator.ts +78 -13
  296. package/src/self-implement/prior-draft-findings.ts +4 -3
  297. package/src/skills/skill-feedback.ts +76 -0
  298. package/src/task-agent/actions.ts +151 -0
  299. package/src/task-agent/judge.ts +122 -0
  300. package/src/task-agent/mission.ts +423 -0
  301. package/src/task-agent/shadow.ts +90 -0
  302. package/src/task-agent/task-hand.ts +235 -0
  303. package/src/task-cards/card-store.ts +4 -1
  304. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
  308. package/src/telegram-commands.ts +1 -1
  309. package/src/telegram-loops-command.ts +22 -15
  310. package/src/telegram.ts +5 -20
  311. package/src/user-config.ts +86 -4
  312. package/src/version/packed-revision.json +1 -1
  313. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
  352. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  353. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
  354. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -97,3 +97,23 @@ export function parse(body: string): PrCommentMeta | null {
97
97
  }
98
98
  return parsed;
99
99
  }
100
+
101
+ /** 상태 댓글의 details 에 쌓인 라운드 기록을 개별 댓글 본문으로 펼친다. */
102
+ export function expandRecords(body: string): string[] {
103
+ if (!/^<!-- elanous:run-status -->(?:\r?\n|$)/.test(body)) return [body];
104
+
105
+ const records: string[] = [];
106
+ for (const details of body.matchAll(/<details(?:\s[^>]*)?>([\s\S]*?)<\/details>/g)) {
107
+ const content = details[1];
108
+ const headers: Array<{ index: number; round?: number }> = [];
109
+ for (const header of content.matchAll(/^<!-- elanous-pr-comment v1 [^\r\n]* -->$/gm)) {
110
+ const meta = parse(header[0]);
111
+ if (meta) headers.push({ index: header.index, round: meta.round });
112
+ }
113
+ for (let i = 0; i < headers.length; i++) {
114
+ if (headers[i].round === undefined) continue;
115
+ records.push(content.slice(headers[i].index, headers[i + 1]?.index ?? content.length).replace(/(?:\r?\n)+$/, ''));
116
+ }
117
+ }
118
+ return records.length ? records : [body];
119
+ }
@@ -67,8 +67,10 @@ export interface LlmMergeOutcome {
67
67
  testDeclarationUnmeasured?: string[];
68
68
  /** 해결기 응답이 LLM 공급자 오류 문구였다 — 파일 내용이 아니다(🅢 2026-09-27 #21086: 세 파일이 통째로 그 문구가 됐다). */
69
69
  providerFailure?: string[];
70
- /** LLM 입력 상한 초과: 사람 수확으로 넘긴다. 원래 브랜치는 abort 후 보존된다. */
71
- reason?: 'conflict-input-too-large' | 'conflict-resolver-interrupted';
70
+ /** 충돌 미해결의 원인. 기존 두 원인에 마커 잔존·파일 읽기 실패를 추가한다. */
71
+ reason?: 'conflict-input-too-large' | 'conflict-resolver-interrupted' | 'conflict-markers-remain' | 'conflict-file-unreadable';
72
+ /** 해결이 멎은 충돌 파일(worktree 상대 경로). */
73
+ failedFile?: string;
72
74
  inputChars?: number;
73
75
  /** 병합 결과가 양쪽 판 중 작은 쪽의 절반 아래로 줄었다 — 통째로 날린 것으로 보고 해결 실패로 친다. */
74
76
  sizeCollapse?: Array<{ file: string; ours: number; theirs: number; merged: number }>;
@@ -107,6 +109,20 @@ function addSizeChange(size: LlmMergeSizeChange, file: string, before: string, a
107
109
 
108
110
  export function formatLlmMergeOutcome(outcome: LlmMergeOutcome): string {
109
111
  const files = outcome.resolvedFiles?.length ? ` (LLM 종합 ${outcome.resolvedFiles.length}파일: ${outcome.resolvedFiles.join(', ')})` : '';
112
+ if (outcome.status === 'conflict-unresolved') {
113
+ // ⛔ reason·failedFile 은 «선택» 칸이다 — 공급자 오류·시험 선언 유실·크기 붕괴 출구는 계약상 reason 이 없다.
114
+ // 있는 칸만 싣고, 그 출구들은 자기 실제 결과(어느 판정이 멈췄나)를 싣는다 — 「사유 undefined」 금지.
115
+ const parts = [
116
+ ...(outcome.reason ? [`사유 ${outcome.reason}`] : []),
117
+ ...(outcome.failedFile ? [`파일 ${outcome.failedFile}`] : []),
118
+ ...(outcome.providerFailure?.length ? [`공급자 오류 문구 ${outcome.providerFailure.join(', ')}`] : []),
119
+ ...(outcome.testDeclarationLoss?.length
120
+ ? [`시험 선언 유실 ${outcome.testDeclarationLoss.map((l) => `${l.file} ${l.ours}/${l.theirs}→${l.merged}`).join(', ')}`] : []),
121
+ ...(outcome.sizeCollapse?.length
122
+ ? [`크기 붕괴 ${outcome.sizeCollapse.map((c) => `${c.file} ${c.ours}/${c.theirs}→${c.merged}줄`).join(', ')}`] : []),
123
+ ];
124
+ return `${outcome.status}${files}${parts.length ? `; ${parts.join(' · ')}` : ''}`;
125
+ }
110
126
  if (outcome.status !== 'llm-resolved') return `${outcome.status}${files}`;
111
127
  const size = outcome.sizeChange;
112
128
  if (size === undefined) return `${outcome.status}${files}; 규모 변화: 못 쟀다`;
@@ -211,6 +227,10 @@ export async function mergeMainWithLlmResolve(
211
227
  ...outcome,
212
228
  ...(testDeclarationUnmeasured.length ? { testDeclarationUnmeasured } : {}),
213
229
  });
230
+ const unresolved = (outcome: LlmMergeOutcome & { status: 'conflict-unresolved'; failedFile: string }): LlmMergeOutcome => {
231
+ debug.log('self-dev.merge', 'conflict-unresolved', { reason: outcome.reason, failedFile: outcome.failedFile, resolvedCount: resolvedFiles.length });
232
+ return measuredOutcome(outcome);
233
+ };
214
234
  for (const f of files) {
215
235
  const abs = join(worktreePath, f);
216
236
  let conflicted: string;
@@ -219,7 +239,7 @@ export async function mergeMainWithLlmResolve(
219
239
  conflicted = git.readFile(abs);
220
240
  } catch {
221
241
  git.abort(worktreePath);
222
- return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles });
242
+ return unresolved({ status: 'conflict-unresolved', reason: 'conflict-file-unreadable', failedFile: f, resolvedFiles });
223
243
  }
224
244
  try {
225
245
  // release/next.md: concurrent landings each append a line — keep both without asking the LLM (REL7c).
@@ -227,7 +247,7 @@ export async function mergeMainWithLlmResolve(
227
247
  merged = deterministic ?? await resolve(f, conflicted);
228
248
  } catch (error) {
229
249
  git.abort(worktreePath); // LLM 예외 → base 유지
230
- return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles,
250
+ return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f,
231
251
  ...(error instanceof MergeConflictInputTooLarge
232
252
  ? { reason: 'conflict-input-too-large' as const, inputChars: error.inputChars }
233
253
  : { reason: 'conflict-resolver-interrupted' as const }),
@@ -235,11 +255,11 @@ export async function mergeMainWithLlmResolve(
235
255
  }
236
256
  if (PROVIDER_FAILURE_TEXT.test(merged)) {
237
257
  git.abort(worktreePath); // 해결기가 공급자 오류 문구를 «내용»으로 돌려줬다 → base 유지
238
- return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles, providerFailure: [f] });
258
+ return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f, providerFailure: [f] });
239
259
  }
240
260
  if (hasConflictMarkers(merged)) {
241
261
  git.abort(worktreePath); // LLM 이 종합 못 함(마커 잔존) → base 유지
242
- return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles });
262
+ return unresolved({ status: 'conflict-unresolved', reason: 'conflict-markers-remain', failedFile: f, resolvedFiles });
243
263
  }
244
264
  try {
245
265
  const ours = countTestDeclarations(git.readIndexStage(worktreePath, 2, f));
@@ -248,7 +268,7 @@ export async function mergeMainWithLlmResolve(
248
268
  if (mergedDeclarations < Math.min(ours, theirs)) {
249
269
  const testDeclarationLoss = [{ file: f, ours, theirs, merged: mergedDeclarations }];
250
270
  git.abort(worktreePath);
251
- return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles, testDeclarationLoss });
271
+ return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f, testDeclarationLoss });
252
272
  }
253
273
  } catch {
254
274
  testDeclarationUnmeasured.push(f);
@@ -260,7 +280,7 @@ export async function mergeMainWithLlmResolve(
260
280
  const smaller = Math.min(ours, theirs);
261
281
  if (smaller >= SIZE_COLLAPSE_MIN_LINES && mergedLines * 2 < smaller) {
262
282
  git.abort(worktreePath); // 양쪽 판 모두보다 절반 넘게 작다 → 통째로 날렸다 → base 유지
263
- return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles, sizeCollapse: [{ file: f, ours, theirs, merged: mergedLines }] });
283
+ return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f, sizeCollapse: [{ file: f, ours, theirs, merged: mergedLines }] });
264
284
  }
265
285
  } catch {
266
286
  /* 한쪽 판이 없는 충돌(삭제·추가) — 크기 비교 대상이 아니다 */
@@ -47,7 +47,9 @@ const runCli = async (args: string[]): Promise<{ stdout: string }> => {
47
47
  const { argv, env } = universeLaunch(args);
48
48
  return exec('bun', argv, { cwd: repoDir(), env, timeout: 120_000, maxBuffer: 1024 * 1024, encoding: 'utf8' });
49
49
  };
50
- const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|force|pay|purchase|secret|credential|sudo|rm\s+-rf|config\s+set)\b|(?:^|\s)--prod\b|삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|자격|비밀|강제/i;
50
+ // A work topic containing a verb is not necessarily an instruction; the guard matches executable requests.
51
+ // Security words (secret · credential · force · 자격 · 비밀 · 강제) stay blocked anywhere — topic or request (UX harvest).
52
+ const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|pay|purchase|sudo)\b(?=\s+(?:the\s+(?:branch|site|app|release|file|record)\b|(?:this\s+)?branch\b|(?:to|into)\s+main\b|main\b|now\b|(?:site|app)\b))|\bcut\s+a\s+release\b|\brm\s+-rf\b|\bconfig\s+set\b|\b(?:secret|credential|force|sudo)\b|자격|비밀|강제|(?:^|\s)--prod\b|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|강제)(?:해라|하세요|해줘|하라|해\s*주세요|하시오)(?!\S)|(?:^\s*|후\s*)(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*$|\s*후\s*)|(?:^\s*운영\s*(?:배포|재시작)(?=\s*$))|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*(?:해|실행|진행)(?:\s|$))/iu;
51
53
  export const TRACK_AGENT_FORBIDDEN_ACTION_REGEX = new RegExp(forbiddenActionRegex.source, forbiddenActionRegex.flags);
52
54
  let running = 0;
53
55
 
@@ -101,7 +103,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
101
103
  const values = decision as Record<string, unknown>;
102
104
  if (!['say', 'hold'].includes(String(values.action)) || (values.reason !== undefined && typeof values.reason !== 'string')
103
105
  || Object.keys(values).some((key) => !['action', 'reason'].includes(key))) throw new Error('invalid decision');
104
- if (typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
106
+ if (values.action === 'say' && typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
105
107
  return decision as TrackAgentDecision;
106
108
  } catch (error) {
107
109
  log('decision-failed', { track: input.track, taskId: input.taskId, error: redactSecretText(String(error)) });
@@ -111,7 +113,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
111
113
 
112
114
  export async function executeTrackAction(decision: TrackAgentDecision, input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentResult> {
113
115
  if (decision.action !== 'say') return { status: 'held', detail: decision.reason ?? '사람 확인' };
114
- if (forbiddenActionRegex.test(`${input.title}\n${input.prompt}`)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
116
+ if (forbiddenActionRegex.test(input.prompt)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
115
117
  if (!findTrack(input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
116
118
  try {
117
119
  const checkBudget = deps.checkBudget ?? (async () => {
@@ -4,6 +4,7 @@ import { dirname, join } from 'node:path';
4
4
  import { elanousStateRoot } from '../autopilot/state-paths.js';
5
5
  import { debug } from '../debug/log.js';
6
6
  import { dateKey } from '../time/format.js';
7
+ import { readRequirementFunnel, type RequirementFunnelResult } from '../intake-plane/requirement-funnel.js';
7
8
 
8
9
  export const BRIEF_DOMAINS = ['운영', '판', '흡수', '시장', '행정', '코나투스'] as const;
9
10
  export const BRIEF_PRIORITIES = ['P0', 'P1', 'P2'] as const;
@@ -41,6 +42,7 @@ export interface BriefItemsOptions {
41
42
  stateDir?: string;
42
43
  now?: () => Date;
43
44
  log?: (category: string, event: string, data?: unknown) => void;
45
+ requirementFunnel?: (now: Date) => RequirementFunnelResult;
44
46
  }
45
47
 
46
48
  export class BriefItemsInputError extends Error {
@@ -89,8 +91,15 @@ function needsDecision(item: BriefItem, today: string): boolean {
89
91
  * duplicate claims dropped (same normalized sentence, most urgent kept).
90
92
  */
91
93
  export function composeBriefMarkdown(items: readonly BriefItem[], slot: BriefSlot, now: Date): string {
94
+ return composeBriefPage(items, slot, now).markdown;
95
+ }
96
+
97
+ /** The ids are exactly the rows represented by the composed page, after deduplication. */
98
+ export function composeBriefPage(items: readonly BriefItem[], slot: BriefSlot, now: Date): { markdown: string; ids: number[] } {
92
99
  const today = dateKey(now, { timeZone: KST });
93
- const eligible = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot, now));
100
+ // A duplicate left out of a delivered page stays unsent in the ledger, but must not reappear on a later page.
101
+ const deliveredClaims = new Set(items.filter(item => item.sent_at !== null).map(item => normalizeClaim(item.text)));
102
+ const eligible = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot, now) && !deliveredClaims.has(normalizeClaim(item.text)));
94
103
  // Duplicate claims keep the copy that needs a decision today (P0 or due today), then the higher priority, then the earliest (ACP must-fix).
95
104
  const rank = (item: BriefItem) => [needsDecision(item, today) ? 0 : 1, BRIEF_PRIORITIES.indexOf(item.priority), item.id];
96
105
  const better = (a: BriefItem, b: BriefItem) => { const x = rank(a), y = rank(b); return x[0]! - y[0]! || x[1]! - y[1]! || x[2]! - y[2]!; };
@@ -117,7 +126,7 @@ export function composeBriefMarkdown(items: readonly BriefItem[], slot: BriefSlo
117
126
  for (const item of group.sort((a, b) => a.id - b.id)) lines.push(itemLine(item));
118
127
  lines.push('');
119
128
  }
120
- return `${lines.join('\n').trimEnd()}\n`;
129
+ return { markdown: `${lines.join('\n').trimEnd()}\n`, ids: unique.map(item => item.id) };
121
130
  }
122
131
 
123
132
  function itemLine(item: BriefItem): string {
@@ -166,11 +175,13 @@ export class BriefItemsLedger {
166
175
  readonly path: string;
167
176
  private readonly now: () => Date;
168
177
  private readonly log: (category: string, event: string, data?: unknown) => void;
178
+ private readonly requirementFunnel: (now: Date) => RequirementFunnelResult;
169
179
 
170
180
  constructor(options: BriefItemsOptions = {}) {
171
181
  this.path = join(options.stateDir ?? elanousStateRoot(), 'briefing', 'items.sqlite');
172
182
  this.now = options.now ?? (() => new Date());
173
183
  this.log = options.log ?? ((category, event, data) => debug.log(category, event, data));
184
+ this.requirementFunnel = options.requirementFunnel ?? ((now) => readRequirementFunnel({ stateDir: options.stateDir, now }));
174
185
  }
175
186
 
176
187
  private using<T>(work: (db: Database) => T): T {
@@ -246,15 +257,81 @@ export class BriefItemsLedger {
246
257
 
247
258
  /**
248
259
  * Markdown for one slot. Reads only — never marks, rewrites, or sends an item.
249
- * Items created at or after the slot instant are left out.
260
+ * Items created at or after the slot instant are left out. At 08:30 the
261
+ * requirement funnel contributes ephemeral lines without adding ledger rows.
250
262
  */
251
263
  compose(slot: string): string {
264
+ return this.composeWithIds(slot).markdown;
265
+ }
266
+
267
+ private itemsFromDb(db: Database): BriefItem[] {
268
+ return db.query('SELECT id, text, domain, priority, deadline, evidence, source, created_at, (SELECT MIN(sent_at) FROM sends WHERE sends.item_id = items.id) AS sent_at FROM items ORDER BY id').all() as BriefItem[];
269
+ }
270
+
271
+ private pageFromDb(db: Database, slot: BriefSlot): { markdown: string; ids: number[] } {
272
+ const items = this.itemsFromDb(db);
273
+ const now = this.now();
274
+ const morningCutoff = Date.parse(`${dateKey(now, { timeZone: KST })}T08:30:00+09:00`);
275
+ let funnelItems: BriefItem[] = [];
276
+ if (slot === '08:30' && now.getTime() >= morningCutoff) {
277
+ // A funnel failure must not take the morning brief down with it.
278
+ try {
279
+ const extra = this.requirementFunnel(new Date(morningCutoff - 1)).briefItems;
280
+ // Negative ids never collide with ledger rows and keep the funnel's own order (summary first).
281
+ funnelItems = extra.map((item, index): BriefItem => ({
282
+ id: index - extra.length, text: item.text, domain: item.domain, priority: item.priority, deadline: null,
283
+ evidence: item.evidence ?? null, source: item.source,
284
+ created_at: new Date(morningCutoff - 1).toISOString(), sent_at: null,
285
+ }));
286
+ } catch (error) {
287
+ this.log('briefing.items', 'requirement-funnel-failed', { slot, error: error instanceof Error ? error.message : String(error) });
288
+ }
289
+ }
290
+ const composed = composeBriefPage([...items, ...funnelItems], slot, now);
291
+ // Ephemeral funnel lines appear on the page but have no ledger row to mark sent.
292
+ const page = { markdown: composed.markdown, ids: composed.ids.filter(id => id > 0) };
293
+ const candidates = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot, now)).length;
294
+ this.log('briefing.items', 'composed', { slot, candidates, bytes: Buffer.byteLength(page.markdown) });
295
+ return page;
296
+ }
297
+
298
+ /** Read-only page and the exact ledger item ids printed on it (after deduplication). */
299
+ composeWithIds(slot: string): { markdown: string; ids: number[] } {
252
300
  if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
253
- const items = this.list();
254
- const markdown = composeBriefMarkdown(items, slot as BriefSlot, this.now());
255
- const included = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot as BriefSlot, this.now()));
256
- this.log('briefing.items', 'composed', { slot, candidates: included.length, bytes: Buffer.byteLength(markdown) });
257
- return markdown;
301
+ return this.using(db => this.pageFromDb(db, slot as BriefSlot));
302
+ }
303
+
304
+ /** Hold the ledger's SQLite writer lock from selecting unsent rows through delivery and marking.
305
+ * A failed delivery rolls back and releases the lock; the next sender can retry.
306
+ */
307
+ withSendLock<T>(slot: string, work: (page: { markdown: string; ids: number[] }, markSent: (ids: readonly number[], slot: BriefSlot) => number) => T): T {
308
+ if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
309
+ return this.using(db => {
310
+ db.exec('BEGIN IMMEDIATE');
311
+ try {
312
+ const page = this.pageFromDb(db, slot as BriefSlot);
313
+ const result = work(page, (ids, sentSlot) => {
314
+ if (sentSlot !== slot || ids.length !== page.ids.length || ids.some((id, index) => id !== page.ids[index])) {
315
+ throw new BriefItemsInputError('marked ids must match composed page');
316
+ }
317
+ return this.markSentInDb(db, ids, sentSlot);
318
+ });
319
+ db.exec('COMMIT');
320
+ return result;
321
+ } catch (cause) {
322
+ db.exec('ROLLBACK');
323
+ throw cause;
324
+ }
325
+ });
326
+ }
327
+
328
+ private markSentInDb(db: Database, ids: readonly number[], slot: BriefSlot): number {
329
+ const at = this.now().toISOString();
330
+ const insert = db.query('INSERT INTO sends (item_id, slot, sent_at) VALUES (?, ?, ?)');
331
+ let count = 0;
332
+ for (const id of ids) { insert.run(id, slot, at); count++; }
333
+ this.log('briefing.items', 'marked-sent', { slot, count });
334
+ return count;
258
335
  }
259
336
 
260
337
  /**
@@ -263,14 +340,6 @@ export class BriefItemsLedger {
263
340
  */
264
341
  markSent(ids: readonly number[], slot: BriefSlot): number {
265
342
  if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
266
- const at = this.now().toISOString();
267
- const count = this.using(db => {
268
- const insert = db.query('INSERT INTO sends (item_id, slot, sent_at) VALUES (?, ?, ?)');
269
- let n = 0;
270
- db.transaction(() => { for (const id of ids) { insert.run(id, slot, at); n++; } })();
271
- return n;
272
- });
273
- this.log('briefing.items', 'marked-sent', { slot, count });
274
- return count;
343
+ return this.using(db => db.transaction(() => this.markSentInDb(db, ids, slot))());
275
344
  }
276
345
  }
@@ -6,10 +6,95 @@ import { writeStdoutJson } from './stdout-json.js';
6
6
  // runner. Spawns an ACP backend, opens a single session, runs the
7
7
  // AutopilotLoopDriver against the mission, prints agent deltas on
8
8
  // stdout + envelope/termination summary on stderr.
9
- export function registerAutopilotCommands(program: Command): void {
9
+ export interface AutopilotCliDeps {
10
+ /** logs.db 싱크 등록(시험 주입). 기본 = `registerStandaloneLogSink`. `task-agent-action` 이 부른다. */
11
+ registerSink?: (surface: string) => Promise<unknown>;
12
+ }
13
+
14
+ export function registerAutopilotCommands(program: Command, cliDeps: AutopilotCliDeps = {}): void {
15
+ // Explicit runtime entry for a selected action; the task chooser can invoke this entry.
10
16
  const autopilotCmd = program
11
17
  .command('autopilot')
12
18
  .description('Mission-driven autopilot — runs an ACP agent against a single mission with safety + budget guards');
19
+ autopilotCmd.command('task-agent-action <actionJson>')
20
+ .description('Execute a selected task-agent action; defaults to shadow')
21
+ .action(async (actionJson: string) => {
22
+ // 독립 프로세스 — 싱크를 붙이지 않으면 `task-agent`/`action` 관측이 logs.db 에 안 닿는다(fail-open).
23
+ const { registerTaskAgentSink } = await import('./tasks-cli.js');
24
+ await registerTaskAgentSink(cliDeps.registerSink);
25
+ const { executeNextAction } = await import('../task-agent/actions.js');
26
+ const { getUserConfig } = await import('../user-config.js');
27
+ const { spawnSync } = await import('node:child_process');
28
+ const { writeFileSync, mkdirSync } = await import('node:fs');
29
+ // 상태 파일은 «없음»(ENOENT)만 빈 상태다 — 손상·해석 실패면 덮어쓰지 않고 멈춘다(사후 리뷰 must-fix · #24482).
30
+ const { readTaskAgentState, TaskAgentStateError } = await import('../task-agent/task-hand.js');
31
+ const { join, dirname } = await import('node:path');
32
+ const { debug } = await import('../debug/log.js');
33
+ const { withFileLockSync } = await import('../storage/file-lock.js');
34
+ const config = getUserConfig() as ReturnType<typeof getUserConfig> & { taskAgent?: { mode?: string } };
35
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
36
+ const statePath = join(effectiveInstanceRoot(), 'task-agent-actions.json');
37
+ type ActionState = { landingDay?: string; landingsToday?: number; failureCounts?: Record<string, number> };
38
+ let state: ActionState;
39
+ try { state = readTaskAgentState<ActionState>(statePath); } catch (error) {
40
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ const action = JSON.parse(actionJson) as import('../task-agent/actions.js').NextAction;
45
+ const deps: import('../task-agent/actions.js').ActionDeps = {
46
+ ...state,
47
+ failureCounts: state.failureCounts ?? {},
48
+ saveState: () => {
49
+ mkdirSync(dirname(statePath), { recursive: true });
50
+ // 실패 횟수는 incrementFailure 가 잠금 안에서 키별로 더한다 — 여기선 덮어쓰지 않는다.
51
+ },
52
+ mode: config.taskAgent?.mode,
53
+ // 하루 착지 칸은 상태 파일 잠금 안에서 확보·반환한다 — 두 CLI 프로세스가 같은 잔여 칸을 쓰지 않게.
54
+ reserveLanding: (day, limit) => withFileLockSync(`${statePath}.lock`, () => {
55
+ const disk = readTaskAgentState<ActionState>(statePath);
56
+ const used = disk.landingDay === day ? disk.landingsToday ?? 0 : 0;
57
+ if (used >= limit) return false;
58
+ mkdirSync(dirname(statePath), { recursive: true });
59
+ writeFileSync(statePath, JSON.stringify({ ...disk, landingDay: day, landingsToday: used + 1 }));
60
+ deps.landingDay = day; deps.landingsToday = used + 1;
61
+ return true;
62
+ }),
63
+ incrementFailure: (key) => withFileLockSync(`${statePath}.lock`, () => {
64
+ const disk = readTaskAgentState<ActionState>(statePath);
65
+ const counts = { ...(disk.failureCounts ?? {}) };
66
+ counts[key] = (counts[key] ?? 0) + 1;
67
+ mkdirSync(dirname(statePath), { recursive: true });
68
+ writeFileSync(statePath, JSON.stringify({ ...disk, failureCounts: counts }));
69
+ return counts[key]!;
70
+ }),
71
+ releaseLanding: (day) => withFileLockSync(`${statePath}.lock`, () => {
72
+ const disk = readTaskAgentState<ActionState>(statePath);
73
+ if (disk.landingDay !== day || !disk.landingsToday) return;
74
+ writeFileSync(statePath, JSON.stringify({ ...disk, landingsToday: disk.landingsToday - 1 }));
75
+ deps.landingsToday = disk.landingsToday - 1;
76
+ }),
77
+ observe: (_event, data) => debug.log('task-agent', 'action', data),
78
+ command: async args => {
79
+ const p = args[0] === 'gh'
80
+ // PR 조회는 착지할 작업 트리에서 — 다른 저장소에서 불러도 엉뚱한 PR 을 읽지 않게.
81
+ ? spawnSync('gh', args.slice(1), { encoding: 'utf8', ...(action.cwd ? { cwd: action.cwd } : {}) })
82
+ // 지금 돌고 있는 진입점으로 부른다 — 저장소 밖(설치본)에서 불러도 같은 elanous 를 쓴다.
83
+ : spawnSync(process.execPath, [process.argv[1]!, ...(process.argv.includes('--test') ? ['--test'] : []), ...args], { encoding: 'utf8' });
84
+ return { status: p.status ?? 1, stdout: p.stdout ?? '', stderr: p.stderr ?? '' };
85
+ },
86
+ };
87
+ let result: string;
88
+ try { result = await executeNextAction(action, deps); } catch (error) {
89
+ // 잠금 안 읽기가 손상을 만나면 쓰지 않고 여기로 온다 — 실패로 끝낸다. 그 밖의 오류는 종전대로 올린다.
90
+ if (!(error instanceof TaskAgentStateError)) throw error;
91
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+ if (config.taskAgent?.mode === 'live') deps.saveState?.();
96
+ process.stdout.write(`${result}\n`);
97
+ });
13
98
 
14
99
  autopilotCmd
15
100
  .command('run <mission...>')
@@ -1,5 +1,8 @@
1
1
  import type { Command } from 'commander';
2
- import { BRIEF_DOMAINS, BRIEF_PRIORITIES, BRIEF_SLOTS, BriefItemsInputError, BriefItemsLedger, type BriefDomain, type BriefPriority } from '../briefing/brief-items.js';
2
+ import { BRIEF_DOMAINS, BRIEF_PRIORITIES, BRIEF_SLOTS, BriefItemsInputError, BriefItemsLedger, type BriefDomain, type BriefPriority, type BriefSlot } from '../briefing/brief-items.js';
3
+ import { kindRouteTarget } from '../domains/telegram-kind-route.js';
4
+ import { sendTelegramReturningId } from '../autopilot/mission-notify.js';
5
+ import { getUserConfig } from '../user-config.js';
3
6
 
4
7
  export interface BriefCliDeps {
5
8
  ledger?: BriefItemsLedger;
@@ -7,6 +10,7 @@ export interface BriefCliDeps {
7
10
  now?: () => Date;
8
11
  output?: (line: string) => void;
9
12
  error?: (line: string) => void;
13
+ send?: (markdown: string) => number | null;
10
14
  }
11
15
 
12
16
  export function registerBriefCommands(program: Command, deps: BriefCliDeps = {}): void {
@@ -20,7 +24,7 @@ export function registerBriefCommands(program: Command, deps: BriefCliDeps = {})
20
24
  error(`brief ${verb}: ${cause instanceof Error ? cause.message : String(cause)}`);
21
25
  process.exitCode = cause instanceof BriefItemsInputError ? 2 : 1;
22
26
  };
23
- const brief = program.command('brief').description('대표 브리핑 항목 원장 — 발신자는 항목만 내고, 발송은 하지 않는다');
27
+ const brief = program.command('brief').description('대표 브리핑 항목 원장과 슬롯 발송');
24
28
 
25
29
  brief.command('add')
26
30
  .description('주장 한 줄을 원장에 추가한다 (원문 그대로)')
@@ -70,4 +74,34 @@ export function registerBriefCommands(program: Command, deps: BriefCliDeps = {})
70
74
  try { output(ledger().compose(opts.slot)); }
71
75
  catch (cause) { fail('compose', cause); }
72
76
  });
77
+
78
+ brief.command('send')
79
+ .description('슬롯 이전의 미발송 항목을 한 장 발송하고 성공한 항목만 표시한다')
80
+ .requiredOption('--slot <slot>', `슬롯 (${BRIEF_SLOTS.join('|')})`)
81
+ .option('--dry-run', '한 장만 출력하고 발송·표시하지 않는다')
82
+ .action((opts: { slot: string; dryRun?: boolean }) => {
83
+ try {
84
+ const store = ledger();
85
+ if (opts.dryRun) {
86
+ const { markdown, ids } = store.composeWithIds(opts.slot);
87
+ output(ids.length === 0 ? '(보낼 항목 없음)' : markdown);
88
+ return;
89
+ }
90
+ const result = store.withSendLock(opts.slot, ({ markdown, ids }, markSent) => {
91
+ if (ids.length === 0) return '(보낼 항목 없음)';
92
+ const send = deps.send ?? ((text: string) => {
93
+ const target = kindRouteTarget(getUserConfig(), 'ops-report');
94
+ if (!target) return null;
95
+ return sendTelegramReturningId(target.botToken, target.chatId, text);
96
+ });
97
+ const messageId = send(markdown);
98
+ if (typeof messageId !== 'number' || !Number.isFinite(messageId) || messageId <= 0) {
99
+ throw new Error('발송 실패: 양수 메시지 id 없음');
100
+ }
101
+ markSent(ids, opts.slot as BriefSlot);
102
+ return `sent ${messageId} (${ids.length} items)`;
103
+ });
104
+ output(result);
105
+ } catch (cause) { fail('send', cause); }
106
+ });
73
107
  }
@@ -53,6 +53,7 @@ export function formatDecisionDetail(e: DecisionEntry): string {
53
53
  ...(e.note ? [`메모: ${e.note}`] : [])] : []),
54
54
  ...(e.withdrawnAt ? [`철회: ${kst(e.withdrawnAt)} · ${e.withdrawReason}`] : []),
55
55
  '이력:', ...e.history.map(h => ` ${h.type} · ${h.at ? kst(h.at) : '시각 미상'} · ${h.by}${h.choice ? ` · ${h.choice}` : ''}${h.reason ? ` · ${h.reason}` : ''} · ${h.version ? version(h.version) : '판 미상'}`),
56
+ ...(e.pendingQuestion ? [`판단 재료:\n${e.pendingQuestion}`] : []),
56
57
  ...(e.refs?.length ? [`참조: ${e.refs.join(' · ')}`] : [])].join('\n');
57
58
  }
58
59
 
@@ -86,6 +87,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
86
87
  root.command('raise').description('SCQA·선택지와 권고로 결정 항목을 올린다')
87
88
  .requiredOption('--title <text>').requiredOption('--category <category>')
88
89
  .requiredOption('--s <text>').requiredOption('--c <text>').option('--q <text>').option('--a <text>')
90
+ .option('--pending-question <text>', '판단 재료 전문 (무엇·지금까지·재측·선택지별 결과·권고·확신·그냥 두면·근거)')
89
91
  .requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
90
92
  .option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
91
93
  .option('--xcheck <SEAT:메모>', '교차 확인 (반복)', repeat, [] as string[])
@@ -96,7 +98,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
96
98
  .option('--ref <url>', '참조 (반복)', repeat, [] as string[])
97
99
  .option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
98
100
  .option('--json').option('--hq-override')
99
- .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
101
+ .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; pendingQuestion?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
100
102
  if (o.run !== undefined && o.resumeQuestion === undefined) throw new Error('--run requires --resume-question');
101
103
  if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
102
104
  if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
@@ -111,6 +113,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
111
113
  const who = agent(o.agent);
112
114
  if (!mayWrite('raise', o.hqOverride)) return;
113
115
  const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
116
+ ...(o.pendingQuestion ? { pendingQuestion: o.pendingQuestion } : {}),
114
117
  ...(crossCheck.length ? { crossCheck } : { crossCheckSkipped: o.noXcheck ?? 'missing' }),
115
118
  ...(o.alternative !== undefined ? { alternative: o.alternative } : {}), ...(o.dissent !== undefined ? { dissent: o.dissent } : {}),
116
119
  recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
@@ -7,8 +7,9 @@ import { pendingFrozenMerges, sweepFrozenMerges } from '../self-implement/frozen
7
7
  export function registerFreezeCommands(program: Command): void {
8
8
  const freeze = program.command('freeze').description('착지 동결 상태 on/off/status');
9
9
  freeze.command('on').option('--reason <text>', '동결 이유').option('--until <timestamp>', '끝 시각(타임존 포함 ISO)')
10
+ .option('--hold-launches', '착지와 함께 새 하니스 launch도 보류')
10
11
  .option('--wait-seconds <n>', '이미 동결 검사를 지난 병합이 끝나기를 기다릴 최대 초', '600')
11
- .action(async (opts: { reason?: string; until?: string; waitSeconds: string }) => {
12
+ .action(async (opts: { reason?: string; until?: string; holdLaunches?: boolean; waitSeconds: string }) => {
12
13
  const waitSeconds = Number(opts.waitSeconds);
13
14
  if (!/^\d+$/.test(opts.waitSeconds.trim()) || !Number.isSafeInteger(waitSeconds) || waitSeconds > 86_400) {
14
15
  throw new CliUserError(`잘못된 --wait-seconds: ${opts.waitSeconds}`, '0 이상 86400 이하의 정수 초');
@@ -34,7 +35,7 @@ export function registerFreezeCommands(program: Command): void {
34
35
  const state = readLandingFreeze();
35
36
  const pending = pendingFrozenMerges();
36
37
  if (opts.json) console.log(JSON.stringify({ frozen: state !== null, freeze: state, pendingMerges: pending }));
37
- else console.log(`${state ? landingFreezeMessage(state) : '동결 없음'} · 보류 병합 ${pending}건`);
38
+ else console.log(`${state ? landingFreezeMessage(state) : '동결 없음'} · launch 보류 ${state?.holdLaunches === true ? '예' : '아니오'} · 보류 병합 ${pending}건`);
38
39
  });
39
40
  }
40
41
 
@@ -12,6 +12,9 @@ import { runGitCommand } from '../git-fs/runner.js';
12
12
  import { debug } from '../debug/log.js';
13
13
  import { getUserConfig } from '../user-config.js';
14
14
  import { registerIntakeSourceCommands } from './intake-source-cli.js';
15
+ import type { IntakeItem } from '../intake-plane/items.js';
16
+ import type { TaskCard } from '../task-cards/card-store.js';
17
+ import { collectFunnelCandidates, type FunnelDirectiveRow } from '../intake-plane/requirement-funnel.js';
15
18
 
16
19
  export interface IntakeToTasksCliDeps {
17
20
  root: string;
@@ -21,6 +24,62 @@ export interface IntakeToTasksCliDeps {
21
24
  llm?: (input: IntakeTaskInput) => Promise<string>;
22
25
  }
23
26
 
27
+ export interface IntakeCandidatesCliDeps {
28
+ directives?: () => FunnelDirectiveRow[] | Promise<FunnelDirectiveRow[]>;
29
+ intakeItems?: () => IntakeItem[] | Promise<IntakeItem[]>;
30
+ cards?: () => TaskCard[] | Promise<TaskCard[]>;
31
+ write?: (text: string) => Promise<void>;
32
+ }
33
+
34
+ /** `--limit` 원 문자열 → 0 이상의 정수. 숫자만(`/^\d+$/`) 받고 나머지('' · 공백 · 음수 · 소수)는 거부한다. */
35
+ export function parseCandidatesLimit(raw: string): number {
36
+ const limit = /^\d+$/.test(raw) ? Number(raw) : NaN;
37
+ if (!Number.isSafeInteger(limit)) throw new Error(`--limit 는 0 이상의 정수여야 한다: ${JSON.stringify(raw)}`);
38
+ return limit;
39
+ }
40
+
41
+ export async function runIntakeCandidatesCli(
42
+ opts: { json?: boolean; limit?: string },
43
+ deps: IntakeCandidatesCliDeps = {},
44
+ ): Promise<void> {
45
+ // 원 문자열을 «먼저» 본다 — `Number('')`·`Number(' ')` 는 0 이라 빈 목록으로 조용히 통과한다.
46
+ const limit = opts.limit === undefined ? undefined : parseCandidatesLimit(opts.limit);
47
+ const directives = deps.directives ?? (async () => {
48
+ const [{ Database }, { existsSync }, { directiveDbPath }] = await Promise.all([
49
+ import('bun:sqlite'), import('node:fs'), import('../directives/directive-index.js'),
50
+ ]);
51
+ const path = directiveDbPath();
52
+ if (!existsSync(path)) return [];
53
+ const db = new Database(path, { readonly: true });
54
+ try { return db.query('SELECT * FROM directives ORDER BY ts DESC').all() as FunnelDirectiveRow[]; }
55
+ finally { db.close(); }
56
+ });
57
+ const intakeItems = deps.intakeItems ?? (async () => {
58
+ const [{ listIntakeItems }, { effectiveInstanceRoot }] = await Promise.all([
59
+ import('../intake-plane/items.js'), import('../instance/resolve.js'),
60
+ ]);
61
+ return listIntakeItems(effectiveInstanceRoot());
62
+ });
63
+ const cards = deps.cards ?? (async () => {
64
+ const [{ existsSync }, { CardStore, cardIndexPath }] = await Promise.all([
65
+ import('node:fs'), import('../task-cards/card-store.js'),
66
+ ]);
67
+ if (!existsSync(cardIndexPath())) return [];
68
+ const store = new CardStore(undefined, true);
69
+ try { return store.listCards({ open: true }); }
70
+ finally { store.close(); }
71
+ });
72
+ const all = collectFunnelCandidates({
73
+ directives: await directives(), intakeItems: await intakeItems(), cards: await cards(),
74
+ });
75
+ const candidates = limit === undefined ? all : all.slice(0, limit);
76
+ if (opts.json) await (deps.write ?? writeStdoutFully)(JSON.stringify({ candidates }) + '\n');
77
+ else await (deps.write ?? writeStdoutFully)([
78
+ `요구 후보 ${candidates.length}건`,
79
+ ...candidates.map((row) => `${row.source}\t${row.id}\t${row.score ?? '-'}\t${row.grade ?? '-'}\t${row.title}`),
80
+ ].join('\n') + '\n');
81
+ }
82
+
24
83
  export interface IntakeDigestCliDeps {
25
84
  root?: string;
26
85
  annotateLens?: (root: string, day: string) => Promise<unknown>;
@@ -102,7 +161,7 @@ export async function runIntakeDigestCli(
102
161
  }
103
162
  const d = (deps.buildDigest ?? buildIntakeDigest)(root, day);
104
163
  if (opts.telegram) {
105
- if (!d.absorbed.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
164
+ if (!d.absorbed.length && !d.news?.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
106
165
  const text = renderDigestTelegram(d, { ...(opts.vault ? { vaultRoot: opts.vault } : {}), ...(opts.note ? { notePath: opts.note } : {}) });
107
166
  const sent = deps.sendTelegram
108
167
  ? await deps.sendTelegram(text)
@@ -134,7 +193,7 @@ export function resolveIntakeCheckRoot(
134
193
  }
135
194
  }
136
195
 
137
- export function registerIntakeCommands(program: Command): void {
196
+ export function registerIntakeCommands(program: Command, candidateDeps: IntakeCandidatesCliDeps = {}): void {
138
197
  // ── intake check — 바깥 사실을 elanous 현재와 대조 (태스크 등록 없음) ──
139
198
  const intakeCmd = program.command('intake').description('바깥 사실·문서를 elanous 현재와 대조하거나 태스크로 받는다');
140
199
  registerIntakeSourceCommands(intakeCmd);
@@ -229,6 +288,19 @@ export function registerIntakeCommands(program: Command): void {
229
288
  }
230
289
  });
231
290
 
291
+ intakeCmd
292
+ .command('candidates')
293
+ .description('지시·흡수·소원에서 읽기 전용 요구 후보를 모은다')
294
+ .option('--json', '구조화 출력')
295
+ .option('--limit <n>', '출력할 후보 상한')
296
+ .action(async (opts: { json?: boolean; limit?: string }) => {
297
+ if (opts.limit !== undefined) {
298
+ try { parseCandidatesLimit(opts.limit); }
299
+ catch (error) { console.error((error as Error).message); process.exitCode = 2; return; }
300
+ }
301
+ await runIntakeCandidatesCli(opts, candidateDeps);
302
+ });
303
+
232
304
  // 정기 외부 흡수 원장 — RFC-regular-external-intake-and-normalization-pipeline §3 (① 모양 · ② 중복).
233
305
  intakeCmd
234
306
  .command('collect-pod <dir>')
@@ -1064,6 +1064,7 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
1064
1064
  "redecompose-opus-tap",
1065
1065
  "redecompose-respawn",
1066
1066
  "redecompose-tap",
1067
+ "reemit-target",
1067
1068
  "reflect.done",
1068
1069
  "refresh-attempt",
1069
1070
  "refused",