elanous 0.2.17 → 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 (309) 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-aac7332576c4d6fc.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/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  66. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  67. package/apps/pwa/out/approvals/index.html +1 -1
  68. package/apps/pwa/out/approvals/index.txt +14 -14
  69. package/apps/pwa/out/autopilot/index.html +1 -1
  70. package/apps/pwa/out/autopilot/index.txt +14 -14
  71. package/apps/pwa/out/board/index.html +1 -1
  72. package/apps/pwa/out/board/index.txt +14 -14
  73. package/apps/pwa/out/botlab/index.html +1 -1
  74. package/apps/pwa/out/botlab/index.txt +14 -14
  75. package/apps/pwa/out/bots/index.html +1 -1
  76. package/apps/pwa/out/bots/index.txt +14 -14
  77. package/apps/pwa/out/ceo/index.html +1 -1
  78. package/apps/pwa/out/ceo/index.txt +14 -14
  79. package/apps/pwa/out/chat/index.html +1 -1
  80. package/apps/pwa/out/chat/index.txt +14 -14
  81. package/apps/pwa/out/consult/index.html +1 -1
  82. package/apps/pwa/out/consult/index.txt +14 -14
  83. package/apps/pwa/out/control/index.html +1 -1
  84. package/apps/pwa/out/control/index.txt +14 -14
  85. package/apps/pwa/out/decisions/index.html +1 -0
  86. package/apps/pwa/out/decisions/index.txt +30 -0
  87. package/apps/pwa/out/design-check/index.html +1 -1
  88. package/apps/pwa/out/design-check/index.txt +14 -14
  89. package/apps/pwa/out/editor/index.html +1 -1
  90. package/apps/pwa/out/editor/index.txt +14 -14
  91. package/apps/pwa/out/exec/index.html +1 -1
  92. package/apps/pwa/out/exec/index.txt +14 -14
  93. package/apps/pwa/out/field/index.html +1 -1
  94. package/apps/pwa/out/field/index.txt +14 -14
  95. package/apps/pwa/out/healthz +1 -1
  96. package/apps/pwa/out/index.html +1 -1
  97. package/apps/pwa/out/index.txt +14 -14
  98. package/apps/pwa/out/inside/index.html +1 -1
  99. package/apps/pwa/out/inside/index.txt +14 -14
  100. package/apps/pwa/out/intake/index.html +1 -1
  101. package/apps/pwa/out/intake/index.txt +14 -14
  102. package/apps/pwa/out/live/index.html +1 -1
  103. package/apps/pwa/out/live/index.txt +14 -14
  104. package/apps/pwa/out/loops/index.html +1 -1
  105. package/apps/pwa/out/loops/index.txt +14 -14
  106. package/apps/pwa/out/market/index.html +1 -1
  107. package/apps/pwa/out/market/index.txt +14 -14
  108. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  109. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  110. package/apps/pwa/out/morning/index.html +1 -1
  111. package/apps/pwa/out/morning/index.txt +14 -14
  112. package/apps/pwa/out/observatory/index.html +1 -1
  113. package/apps/pwa/out/observatory/index.txt +14 -14
  114. package/apps/pwa/out/ops/checklist/index.html +1 -1
  115. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  116. package/apps/pwa/out/ops/release/index.html +1 -1
  117. package/apps/pwa/out/ops/release/index.txt +14 -14
  118. package/apps/pwa/out/ops/seats/index.html +1 -1
  119. package/apps/pwa/out/ops/seats/index.txt +14 -14
  120. package/apps/pwa/out/outputs/index.html +1 -1
  121. package/apps/pwa/out/outputs/index.txt +14 -14
  122. package/apps/pwa/out/reflection/index.html +1 -1
  123. package/apps/pwa/out/reflection/index.txt +15 -15
  124. package/apps/pwa/out/scheduler/index.html +1 -1
  125. package/apps/pwa/out/scheduler/index.txt +14 -14
  126. package/apps/pwa/out/sessions/index.html +1 -1
  127. package/apps/pwa/out/sessions/index.txt +14 -14
  128. package/apps/pwa/out/settings/devices/index.html +1 -1
  129. package/apps/pwa/out/settings/devices/index.txt +14 -14
  130. package/apps/pwa/out/settings/index.html +1 -1
  131. package/apps/pwa/out/settings/index.txt +14 -14
  132. package/apps/pwa/out/setup/done/index.html +1 -1
  133. package/apps/pwa/out/setup/done/index.txt +14 -14
  134. package/apps/pwa/out/setup/index.html +1 -1
  135. package/apps/pwa/out/setup/index.txt +14 -14
  136. package/apps/pwa/out/share/index.html +1 -1
  137. package/apps/pwa/out/share/index.txt +14 -14
  138. package/apps/pwa/out/showroom/index.html +1 -1
  139. package/apps/pwa/out/showroom/index.txt +14 -14
  140. package/apps/pwa/out/tasks/index.html +1 -1
  141. package/apps/pwa/out/tasks/index.txt +14 -14
  142. package/apps/pwa/out/term/index.html +1 -1
  143. package/apps/pwa/out/term/index.txt +14 -14
  144. package/apps/pwa/out/today/index.html +1 -1
  145. package/apps/pwa/out/today/index.txt +14 -14
  146. package/apps/pwa/out/trace/index.html +1 -1
  147. package/apps/pwa/out/trace/index.txt +14 -14
  148. package/apps/pwa/out/vault/index.html +1 -1
  149. package/apps/pwa/out/vault/index.txt +14 -14
  150. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  151. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  152. package/apps/pwa/out/workflows/index.html +1 -1
  153. package/apps/pwa/out/workflows/index.txt +14 -14
  154. package/apps/pwa/out/workspace/index.html +1 -1
  155. package/apps/pwa/out/workspace/index.txt +14 -14
  156. package/apps/pwa/out/worktrees/index.html +1 -1
  157. package/apps/pwa/out/worktrees/index.txt +14 -14
  158. package/package.json +2 -1
  159. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  160. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  161. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  162. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  163. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  164. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  165. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  166. package/scripts/ci-public-export-import-gate.ts +7 -2
  167. package/scripts/ci-typecheck-changed.ts +21 -6
  168. package/scripts/discord-seat-probe.ts +3 -2
  169. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  170. package/scripts/goals/authoring-metrics.ts +73 -0
  171. package/scripts/landing-heal/must-fix-match.ts +7 -7
  172. package/scripts/landing-heal/run-mission.ts +3 -2
  173. package/scripts/public-export-test-run.ts +37 -2
  174. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  175. package/scripts/release-loop/cut-branch.ts +2 -1
  176. package/scripts/release-loop/cutoff.ts +4 -2
  177. package/scripts/release-loop/docs-land-node.ts +8 -1
  178. package/scripts/release-loop/docs-node.ts +1 -1
  179. package/scripts/release-loop/gate-node.ts +99 -14
  180. package/scripts/release-loop/node-verdict.ts +2 -1
  181. package/scripts/release-loop/npm-publish-node.ts +11 -7
  182. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  183. package/scripts/release-loop/preflight.ts +246 -0
  184. package/scripts/release-loop/publish-node.ts +8 -1
  185. package/scripts/release-loop/release-readiness.ts +11 -6
  186. package/scripts/release-loop/release-version.ts +48 -0
  187. package/scripts/release-loop/resume-release.ts +115 -0
  188. package/scripts/release-loop/unattended-release.ts +43 -7
  189. package/scripts/release-loop/version-node.ts +145 -40
  190. package/scripts/rhythm/daily.ts +25 -4
  191. package/scripts/skills/public-check.ts +62 -12
  192. package/scripts/test-deterministic.ts +51 -33
  193. package/scripts/test-diet/effectiveness.ts +62 -1
  194. package/scripts/test-diet/lib.ts +1 -1
  195. package/scripts/test-diet/node.ts +8 -1
  196. package/src/agent-mission/review-loop.ts +4 -3
  197. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  198. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  199. package/src/briefing/brief-items.ts +86 -17
  200. package/src/cli/autopilot-cli.ts +30 -11
  201. package/src/cli/brief-cli.ts +36 -2
  202. package/src/cli/freeze-cli.ts +3 -2
  203. package/src/cli/intake-cli.ts +73 -1
  204. package/src/cli/notify-cli.ts +75 -0
  205. package/src/cli/provider-cli.ts +12 -2
  206. package/src/cli/release-cli.ts +101 -17
  207. package/src/cli/tasks-cli.ts +309 -3
  208. package/src/context-bus/condense.ts +19 -3
  209. package/src/context-bus/context-day.ts +77 -24
  210. package/src/context-bus/context-first.ts +33 -0
  211. package/src/discord-self-message.ts +20 -2
  212. package/src/discord-slash-wire.ts +1 -0
  213. package/src/domains/outbound-alert.ts +200 -27
  214. package/src/domains/outbound-log-probe.ts +16 -1
  215. package/src/domains/release-tool.ts +2 -2
  216. package/src/execution-loop/gates.ts +11 -2
  217. package/src/flow-loop/split.ts +8 -4
  218. package/src/git-fs/worktree.ts +23 -3
  219. package/src/harness/harness-cli-command.ts +10 -2
  220. package/src/harness/harness-queue.ts +388 -33
  221. package/src/harness/helper-scan.ts +10 -3
  222. package/src/hq/seats.ts +369 -0
  223. package/src/index.ts +133 -15
  224. package/src/intake-plane/adopt-gate.ts +49 -0
  225. package/src/intake-plane/check.ts +3 -0
  226. package/src/intake-plane/collect-github.ts +57 -25
  227. package/src/intake-plane/discord-seat-work.ts +8 -2
  228. package/src/intake-plane/requirement-funnel.ts +316 -0
  229. package/src/knowledge/know-find.ts +10 -1
  230. package/src/loops/budget.ts +36 -0
  231. package/src/loops/orchestrator/finish-rate.ts +73 -1
  232. package/src/loops/orchestrator/grid-place.ts +11 -0
  233. package/src/loops/orchestrator/tick.ts +295 -20
  234. package/src/maturity/feature-maturity.ts +3 -2
  235. package/src/nexus/api/decisions-route.ts +11 -1
  236. package/src/nexus/api/loop-edges.ts +69 -13
  237. package/src/nexus/api/ops-seats-sources.ts +2 -2
  238. package/src/nexus/api/workflow-templates.ts +3 -0
  239. package/src/oauth/codex.ts +38 -0
  240. package/src/onboarding/first-chat-gate.ts +29 -0
  241. package/src/release-loop/checklist.ts +104 -6
  242. package/src/release-loop/feature-store.ts +10 -9
  243. package/src/release-loop/landing-freeze.ts +6 -4
  244. package/src/release-loop/placement.ts +13 -3
  245. package/src/release-loop/rubric.ts +80 -0
  246. package/src/seat-dispatch/seat-ask.ts +79 -16
  247. package/src/seat-loop/seat-loop.ts +12 -6
  248. package/src/self-dev/ask-launch-flow.ts +24 -3
  249. package/src/self-dev/ask-path-candidates.ts +68 -0
  250. package/src/self-dev/dev-pipeline.ts +4 -0
  251. package/src/self-dev/draft-triage-rules.ts +1 -1
  252. package/src/self-dev/orchestrate-cli.ts +22 -0
  253. package/src/self-dev/round-pr-supersede.ts +244 -0
  254. package/src/self-dev/run-supervisor.ts +14 -0
  255. package/src/self-implement/follow-up-goals.ts +2 -2
  256. package/src/self-implement/gate-remote.ts +427 -0
  257. package/src/self-implement/goal-author.ts +8 -3
  258. package/src/self-implement/l8-merge-queue.ts +29 -5
  259. package/src/self-implement/orchestrator.ts +59 -12
  260. package/src/self-implement/prior-draft-findings.ts +4 -3
  261. package/src/skills/skill-feedback.ts +76 -0
  262. package/src/task-agent/actions.ts +39 -6
  263. package/src/task-agent/judge.ts +31 -5
  264. package/src/task-agent/mission.ts +423 -0
  265. package/src/task-agent/shadow.ts +90 -0
  266. package/src/task-agent/task-hand.ts +235 -0
  267. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  268. package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
  269. package/src/telegram-loops-command.ts +1 -1
  270. package/src/telegram.ts +5 -20
  271. package/src/user-config.ts +63 -3
  272. package/src/version/packed-revision.json +1 -1
  273. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  307. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  308. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  309. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -1,8 +1,9 @@
1
- import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
1
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
2
2
  import { dlopen, FFIType } from 'bun:ffi';
3
3
  import { join } from 'node:path';
4
4
  import { listMemories, memoryRoot, saveMemory, type MemoryEntry } from '../memory.js';
5
5
  import { DecisionLedger } from '../decisions/decision-ledger.js';
6
+ import { debug } from '../debug/log.js';
6
7
  import { condenseContextWindowWithReport, readCondenseEvents, type CondenseFunnel, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
7
8
 
8
9
  const summarize = async (source: MemorySource): Promise<MemorySummary> =>
@@ -51,6 +52,7 @@ export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'u
51
52
 
52
53
  type CondenseStore = {
53
54
  list(): MemoryEntry[];
55
+ hasMemoryDirectory?(): boolean;
54
56
  save(input: Parameters<typeof saveMemory>[0]): MemoryEntry;
55
57
  raise(input: Parameters<DecisionLedger['raiseOnce']>[0], ref: string): string;
56
58
  withLock<T>(action: () => T): T;
@@ -96,6 +98,7 @@ function defaultStore(root?: string): CondenseStore {
96
98
  const ledger = new DecisionLedger(root ? { stateDir: root } : {});
97
99
  return {
98
100
  list: () => listMemories({}, memoryDir),
101
+ hasMemoryDirectory: () => existsSync(memoryDir),
99
102
  save: input => saveMemory(input, memoryDir),
100
103
  raise: (input, ref) => ledger.raiseOnce(input, ref).id,
101
104
  withLock: action => withMemoryLock(memoryDir, action),
@@ -118,10 +121,22 @@ function newestActive(entries: readonly MemoryEntry[]): MemoryEntry | undefined
118
121
  }
119
122
 
120
123
  /** Apply candidate cards through the existing curated memory and decision ledger paths. */
121
- export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore): NonNullable<CondenseReport['applied']> {
124
+ export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore, now: Date = new Date()): NonNullable<CondenseReport['applied']> {
122
125
  const decisions: string[] = [];
123
126
  let written = 0;
124
127
  let retired = 0;
128
+ const cutoff = now.getTime() - 30 * 24 * 3_600_000;
129
+ const retireAged = () => {
130
+ let agedRetired = 0;
131
+ for (const old of store.list()) {
132
+ if (old.contextCard?.status !== 'active' || !(Date.parse(old.contextCard.updatedAt) < cutoff)) continue;
133
+ store.save({ type: old.type, name: old.name, description: old.description, body: old.body, id: old.id,
134
+ pinned: false, priority: old.priority, contextCard: { ...old.contextCard, status: 'retired' } });
135
+ agedRetired++;
136
+ }
137
+ retired += agedRetired;
138
+ debug.log('context.condense', 'aged-out', { retired: agedRetired });
139
+ };
125
140
  for (const card of cards) {
126
141
  if (card.conflict) {
127
142
  const ref = `context-condense:${JSON.stringify([card.project, card.seat, card.topic, card.source, card.updatedAt])}`;
@@ -162,6 +177,7 @@ export function applyCondensedCards(cards: readonly MemoryItem[], store: Condens
162
177
  };
163
178
  store.withLock(applyTopic);
164
179
  }
180
+ if (store.hasMemoryDirectory?.() !== false) store.withLock(retireAged);
165
181
  return { written, retired, decisions };
166
182
  }
167
183
 
@@ -184,6 +200,6 @@ export async function runContextCondense(options: {
184
200
  const conflicts = cards.filter(card => card.conflict !== undefined).length;
185
201
  const report: CondenseReport = { since, until, cards, conflicts, skipped, folded, droppedHarnessChild, funnel,
186
202
  markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped, droppedHarnessChild, funnel }) };
187
- if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root));
203
+ if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root), now);
188
204
  return report;
189
205
  }
@@ -12,6 +12,16 @@ export interface ContextDayRow {
12
12
  status: '들어옴' | '안 들어옴';
13
13
  }
14
14
  export interface ContextDayReport { since: string; until: string; rows: ContextDayRow[] }
15
+ export interface ContextDayTimelineEvent {
16
+ at: string;
17
+ source: ContextDaySource;
18
+ kind: ContextDayKind | 'other';
19
+ summary: string;
20
+ link: string | null;
21
+ }
22
+ export interface ContextDayTimelineReport { since: string; until: string; events: ContextDayTimelineEvent[] }
23
+ interface JournalEvent { surface: string; kind: string; ts: string; refs: string | null; summary: string | null }
24
+ type DayDeps = { now?: () => Date; dbPath?: string; seat?: ContextDaySource };
15
25
 
16
26
  /** Reject malformed windows instead of silently changing the time range being judged. */
17
27
  export function contextDayHours(since: string): number {
@@ -34,43 +44,86 @@ function kindFor(surface: string, kind: string): ContextDayKind | null {
34
44
  return null;
35
45
  }
36
46
 
47
+ function sourceFor(event: JournalEvent): { source: ContextDaySource; refs: { url?: unknown; source?: unknown } } | null {
48
+ let refs: { seat?: unknown; origin?: unknown; url?: unknown; source?: unknown };
49
+ try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { return null; }
50
+ if (!refs || typeof refs !== 'object') return null;
51
+ const source = event.surface === 'context:external' ? refs.origin : refs.seat;
52
+ if (typeof source !== 'string' || !CONTEXT_DAY_SOURCES.some(item => item === source)) return null;
53
+ const isExternal = CONTEXT_DAY_SOURCES.indexOf(source as ContextDaySource) >= 4;
54
+ // Do not attribute an external hook to a seat (or a seat event to an external origin).
55
+ if ((event.surface === 'context:external') !== isExternal) return null;
56
+ return { source: source as ContextDaySource, refs };
57
+ }
58
+
37
59
  /** Read only the context journal, not recallEvents (which mutates recall counts) or the writable DB opener. */
38
- export function contextDay(since = '24h', deps: { now?: () => Date; dbPath?: string } = {}): ContextDayReport {
60
+ function readDay(since: string, deps: DayDeps): { since: string; until: string; events: JournalEvent[] } {
39
61
  const hours = contextDayHours(since);
40
62
  const now = (deps.now ?? (() => new Date()))();
41
63
  const until = now.toISOString();
42
64
  const from = new Date(now.getTime() - hours * 3_600_000).toISOString();
43
- const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
44
- source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
45
- }));
46
- const bySource = new Map(rows.map(row => [row.source, row]));
47
65
  const path = deps.dbPath ?? surfaceEventsDbPath();
48
- if (!existsSync(path)) return { since: from, until, rows };
66
+ if (!existsSync(path)) return { since: from, until, events: [] };
49
67
  const db = new Database(path, { readonly: true });
50
68
  try {
51
- const events = db.prepare(`SELECT surface, kind, ts, refs FROM events
69
+ const events = db.prepare(`SELECT surface, kind, ts, refs, summary FROM events
52
70
  WHERE surface IN ('coord:channel', 'context:session', 'context:external')
53
- AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as Array<{
54
- surface: string; kind: string; ts: string; refs: string | null;
55
- }>;
56
- for (const event of events) {
57
- let refs: { seat?: unknown; origin?: unknown };
58
- try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { continue; }
59
- const source = event.surface === 'context:external' ? refs.origin : refs.seat;
60
- if (typeof source !== 'string') continue;
61
- const row = bySource.get(source as ContextDaySource);
62
- if (!row) continue;
63
- // Do not attribute an external hook to a seat (or a seat event to an external origin).
64
- if ((event.surface === 'context:external') !== (rows.indexOf(row) >= 4)) continue;
65
- row.lastAt = event.ts;
66
- row.status = '들어옴';
67
- const kind = kindFor(event.surface, event.kind);
68
- if (kind) row.counts[kind]++;
69
- }
71
+ AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as JournalEvent[];
72
+ return { since: from, until, events };
70
73
  } finally { db.close(); }
74
+ }
75
+
76
+ export function contextDay(since = '24h', deps: { now?: () => Date; dbPath?: string } = {}): ContextDayReport {
77
+ const { since: from, until, events } = readDay(since, deps);
78
+ const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
79
+ source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
80
+ }));
81
+ const bySource = new Map(rows.map(row => [row.source, row]));
82
+ for (const event of events) {
83
+ const attribution = sourceFor(event);
84
+ if (!attribution) continue;
85
+ const row = bySource.get(attribution.source)!;
86
+ row.lastAt = event.ts;
87
+ row.status = '들어옴';
88
+ const kind = kindFor(event.surface, event.kind);
89
+ if (kind) row.counts[kind]++;
90
+ }
71
91
  return { since: from, until, rows };
72
92
  }
73
93
 
94
+ export function contextDayTimeline(since = '24h', deps: DayDeps = {}): ContextDayTimelineReport {
95
+ if (deps.seat !== undefined && !CONTEXT_DAY_SOURCES.some(source => source === deps.seat)) {
96
+ throw new Error(`unknown --seat: ${deps.seat}`);
97
+ }
98
+ const { since: from, until, events } = readDay(since, deps);
99
+ const timeline: ContextDayTimelineEvent[] = [];
100
+ for (const event of events) {
101
+ const attribution = sourceFor(event);
102
+ if (!attribution || (deps.seat && attribution.source !== deps.seat)) continue;
103
+ const { url, source } = attribution.refs;
104
+ const link = [url, source].find(value => typeof value === 'string' && /^(?:https?:\/\/|elanous:\/\/)[^\s]+$/.test(value));
105
+ timeline.push({ at: event.ts, source: attribution.source, kind: kindFor(event.surface, event.kind) ?? 'other',
106
+ summary: event.summary ?? '', link: typeof link === 'string' ? link : null });
107
+ }
108
+ return { since: from, until, events: timeline };
109
+ }
110
+
111
+ const kstFormatter = new Intl.DateTimeFormat('en-CA', {
112
+ timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
113
+ });
114
+ function kst(at: string): string {
115
+ const parts = Object.fromEntries(kstFormatter.formatToParts(new Date(at)).map(part => [part.type, part.value]));
116
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute} KST`;
117
+ }
118
+
119
+ export function renderContextDayTimeline(report: ContextDayTimelineReport, dbPath = surfaceEventsDbPath()): string {
120
+ const lines = [`맥락 사건 ${kst(report.since)} ~ ${kst(report.until)}`];
121
+ if (!report.events.length) lines.push(`이 창에 사건 0건 — 원장 경로: ${dbPath}`);
122
+ else lines.push(...report.events.map(event => `${kst(event.at).slice(11)} · ${event.source} · ${event.kind} · ${event.summary.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, ' ')} · ${event.link ?? '—'}`));
123
+ lines.push(`${report.events.length}건 · 출처 ${new Set(report.events.map(event => event.source)).size}곳`);
124
+ return lines.join('\n');
125
+ }
126
+
74
127
  export function renderContextDay(report: ContextDayReport): string {
75
128
  return [
76
129
  `맥락 이벤트 ${report.since} ~ ${report.until} (UTC)`,
@@ -0,0 +1,33 @@
1
+ import type { ContextNowAnswer, ContextNowDeps } from './context-now.js';
2
+
3
+ const CONTEXT_FIRST_GAP_MS = 6 * 60 * 60 * 1000;
4
+ const CONTEXT_UNREADABLE = '맥락 못 읽음';
5
+
6
+ /** Track every allowed utterance, consuming a pending summary only on a normal turn. */
7
+ export function createContextFirstGate({ now, gapMs = CONTEXT_FIRST_GAP_MS }: { now: () => number; gapMs?: number }) {
8
+ const lastUtteranceAt = new Map<string, number>();
9
+ const pending = new Set<string>();
10
+ return {
11
+ note(key: string): void {
12
+ const at = now();
13
+ const previous = lastUtteranceAt.get(key);
14
+ lastUtteranceAt.set(key, at);
15
+ if (previous === undefined || at - previous >= gapMs) pending.add(key);
16
+ },
17
+ take(key: string): boolean {
18
+ return pending.delete(key);
19
+ },
20
+ };
21
+ }
22
+
23
+ export function renderContextFirst(
24
+ read: (options: {}, deps?: ContextNowDeps) => ContextNowAnswer,
25
+ deps: ContextNowDeps | undefined,
26
+ render: (answer: ContextNowAnswer) => string,
27
+ ): string {
28
+ try {
29
+ return render(read({}, deps));
30
+ } catch {
31
+ return CONTEXT_UNREADABLE;
32
+ }
33
+ }
@@ -65,7 +65,9 @@ import { detectCard, runCardFollowup, NotACardError } from './card-followup/core
65
65
  import { localOcrText } from './telegram-card-followup.js';
66
66
  import type { runGraph } from './graph-runner/runner.js';
67
67
  import { readSlashContextNow, renderTelegramNow } from './context-bus/context-now-surfaces.js';
68
- import type { ContextNowDeps } from './context-bus/context-now.js';
68
+ import { contextNow, type ContextNowDeps } from './context-bus/context-now.js';
69
+ import { createContextFirstGate, renderContextFirst } from './context-bus/context-first.js';
70
+ import { telegramLoopsStatus, type LoopsSources } from './telegram-loops-command.js';
69
71
  import { botUnavailableSlashReply, deriveTuiSlashAvailability, type TuiSlashAvailability } from './maturity/tui-slash-availability.js';
70
72
 
71
73
  const SLASH_FOCUS_TURNS_DEFAULT = 8;
@@ -143,6 +145,9 @@ export interface DiscordSelfMessageDeps {
143
145
  log?: (msg: string) => void;
144
146
  cardFollowupDeps?: { ocrText?: (path: string) => Promise<string | null>; runGraph?: typeof runGraph; rootDir?: () => string };
145
147
  nowDeps?: ContextNowDeps;
148
+ /** Override the clock for the context-first gate in isolated message-path tests. */
149
+ contextFirstNow?: () => number;
150
+ loopsSources?: LoopsSources;
146
151
  /** Override the TUI slash projection for isolated message-path tests. */
147
152
  tuiSlashAvailability?: readonly TuiSlashAvailability[];
148
153
  }
@@ -151,6 +156,15 @@ export interface DiscordSelfMessageDeps {
151
156
  export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessageHandler {
152
157
  const log = deps.log ?? ((): void => {});
153
158
  const cfg = deps.userConfig;
159
+ const contextFirstGate = createContextFirstGate({ now: deps.contextFirstNow ?? Date.now });
160
+ const maybeSendContextFirst = async (channelId: string): Promise<void> => {
161
+ if (!contextFirstGate.take(channelId) || cfg.discord?.contextFirst === false) return;
162
+ try {
163
+ await deps.getBot()?.sendMessage(channelId, renderContextFirst(contextNow, deps.nowDeps, renderTelegramNow));
164
+ } catch (err) {
165
+ log(`discord context-first failed: ${err instanceof Error ? err.message : String(err)}`);
166
+ }
167
+ };
154
168
  // C2+ (2026-07-12 dogfood): 디스코드 첨부는 메시지 단위라 "사진 먼저,
155
169
  // !cdx 명령은 다음 메시지" 패턴에서 위임 턴이 빈손이 된다. 채널별로
156
170
  // 마지막 첨부를 기억해 두고, 첨부 없는 위임 명령이 2분 내 같은
@@ -359,6 +373,7 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
359
373
 
360
374
  return async (ctx, streamer) => {
361
375
  if (deps.channelScope && ctx.channelId !== deps.channelScope) return undefined;
376
+ contextFirstGate.note(ctx.channelId);
362
377
  // Voice text commands pass the guild gate for the voice adapter's
363
378
  // sake — never route them into the LLM turn (legacy behavior).
364
379
  if (/^\/voice-(join|leave|status)\b/.test(ctx.text.trim())) return undefined;
@@ -367,6 +382,8 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
367
382
  const args = nowCommand[1]?.trim().split(/\s+/).filter(Boolean) ?? [];
368
383
  return renderTelegramNow(readSlashContextNow(args, deps.nowDeps));
369
384
  }
385
+ if (/^[/!]loops$/.test(ctx.text.trim())) return telegramLoopsStatus([], deps.loopsSources);
386
+ if (/^[/!]loops\s+\S/.test(ctx.text.trim())) return telegramLoopsStatus(['extra'], deps.loopsSources);
370
387
  if (!ctx.text.trim() && ctx.attachments.length === 1 && ctx.attachments[0]!.contentType?.startsWith('image/')) {
371
388
  const photo = ctx.attachments[0]!;
372
389
  if (detectCard({ width: photo.width, height: photo.height, ocrText: null }).decision !== 'skip') {
@@ -408,6 +425,8 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
408
425
  const unavailable = botUnavailableSlashReply(entry, 'discord');
409
426
  if (unavailable) return unavailable;
410
427
  }
428
+ const cmd = parseDiscordAcpCommand(ctx.text);
429
+ if (!cmd && !/^[!/]/.test(ctx.text.trim())) await maybeSendContextFirst(ctx.channelId);
411
430
  log(`◀ ${ctx.userName ?? ctx.userId}: ${ctx.text.slice(0, 80)}${ctx.attachments.length ? ` (+첨부 ${ctx.attachments.length})` : ''}`);
412
431
  debug.log('discord.self.turn', 'inbound', { channelId: ctx.channelId, chars: ctx.text.length, attachments: ctx.attachments.length });
413
432
  // C2+ — 첨부 기억(사진만 먼저 올리는 패턴 지원). 명령 여부와 무관.
@@ -416,7 +435,6 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
416
435
  const delegationKey = delegationChatKey('dc', ctx.channelId);
417
436
 
418
437
  // 1) Explicit text commands — /cc·/cdx·/gem delegate, /brain exits.
419
- const cmd = parseDiscordAcpCommand(ctx.text);
420
438
  if (cmd?.kind === 'brain') {
421
439
  clearActiveDelegation(delegationKey);
422
440
  return '🧠 브레인 모드로 복귀했습니다 (active delegation 해제).';
@@ -48,6 +48,7 @@ export const ELANOUS_SLASH_COMMANDS: readonly SlashCommandSchema[] = [
48
48
  { name: 'now', description: '지금 판·칸·결정·자리·최근 맥락 보기', options: [
49
49
  { name: 'topic', description: '좁혀 볼 주제 (선택)', type: 3, required: false },
50
50
  ] },
51
+ { name: 'loops', description: '루프·크론 현황 (늦음·실패 먼저)' },
51
52
  { name: 'sessions', description: '바인딩된 세션 목록' },
52
53
  { name: 'new', description: '새 세션으로 시작 (이전 대화 보존)' },
53
54
  { name: 'attach', description: '이 채널을 다른 세션에 연결', options: [
@@ -5,8 +5,8 @@
5
5
  // send.py 와 동일 동작을 TS 로 포팅(완전 elanous 소유). sync(curl) 계약.
6
6
 
7
7
  import * as childProcess from 'node:child_process';
8
- import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync } from 'node:fs';
9
- import { dirname, join, relative } from 'node:path';
8
+ import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync, readdirSync, renameSync, openSync, closeSync, statSync, writeFileSync } from 'node:fs';
9
+ import { basename, dirname, join, relative } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { getElanousConfigDir } from '../elanous-config-dir.js';
12
12
  import { conatusEnv } from './conatus-env.js';
@@ -138,11 +138,11 @@ export function userRecentlyActive(now: Date = new Date()): boolean {
138
138
 
139
139
  /** 야간 보류 적재 (jsonl append — 크론 동시 실행에 안전). origin 있으면 함께 적재 —
140
140
  * 아침 flush 가 그 origin(발신 채널)으로 되돌려 발송(없으면 report 묶음). */
141
- function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): boolean {
141
+ export function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): boolean {
142
142
  try {
143
143
  mkdirSync(dirname(path), { recursive: true });
144
144
  const rec = { ts: new Date().toISOString(), kind, text, ...(origin ? { origin } : {}) };
145
- appendFileSync(path, JSON.stringify(rec) + '\n');
145
+ withQueueLock(path, () => appendFileSync(path, JSON.stringify(rec) + '\n'));
146
146
  console.error(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
147
147
  return true;
148
148
  } catch (e) {
@@ -198,32 +198,195 @@ function deliverToOrigin(origin: MissionOrigin, text: string, onBot?: (bot: stri
198
198
  try { return sendTelegramRaw(token, origin.chatId, text, origin.threadId); } catch { return false; }
199
199
  }
200
200
 
201
- /** 보류분 일괄 발송 — 묶음 1건(아침 폭주 방지). 발송 건수 반환.
202
- * 무음 창 밖 첫 sendOutbound 가 자동 호출 + 06:31 플러시 크론이 보장. */
203
- export function flushDeferred(path = DEFERRED_PATH): number {
201
+ /** 보류 큐 재시도 상한 env — config `outbound.deferredMaxAttempts` 가 없을 때만 읽는다. */
202
+ export const DEFERRED_MAX_ATTEMPTS_ENV = 'ELANOUS_OUTBOUND_DEFERRED_MAX_ATTEMPTS';
203
+
204
+ /** 보류 항목이 격리되기 전까지 허용하는 실패 flush 수 — config > env > 5. */
205
+ export function deferredMaxAttempts(config?: Pick<UserConfig, 'outbound'>): number {
206
+ try {
207
+ const v = (config ?? getUserConfig()).outbound?.deferredMaxAttempts;
208
+ if (typeof v === 'number' && Number.isSafeInteger(v) && v > 0) return v;
209
+ } catch { /* unreadable config: env, then default */ }
210
+ const raw = Number(process.env[DEFERRED_MAX_ATTEMPTS_ENV]?.trim() || NaN);
211
+ if (Number.isSafeInteger(raw) && raw > 0) return raw;
212
+ return 5;
213
+ }
214
+
215
+ /** 격리 파일 — 보류 큐 옆 `<name>.failed.jsonl`. */
216
+ export function deferredQuarantinePath(path: string): string {
217
+ return path.endsWith('.jsonl') ? `${path.slice(0, -'.jsonl'.length)}.failed.jsonl` : `${path}.failed`;
218
+ }
219
+
220
+ function pidAlive(pid: number): boolean {
221
+ try { process.kill(pid, 0); return true; } catch (e) { return (e as NodeJS.ErrnoException).code === 'EPERM'; }
222
+ }
223
+
224
+ /** 큐 적재·claim 이 서로를 가로지르지 않게 하는 짧은 배타 잠금(`<queue>.lock`, O_EXCL).
225
+ * 적재는 open→write→close 를 잠금 안에서 하므로 claim 의 rename 이 «열린 채 쓰기 전»인 적재를 가져가지 않는다.
226
+ * 잠금을 못 얻으면(2초 · 30초 넘은 잔여 잠금은 걷는다) 그대로 진행한다 — 알림을 잃는 것보다 낫다. */
227
+ function withQueueLock<T>(path: string, fn: () => T): T {
228
+ const lock = `${path}.lock`;
229
+ const deadline = Date.now() + 2_000;
230
+ let fd: number | null = null;
231
+ while (fd === null && Date.now() <= deadline) {
232
+ try { fd = openSync(lock, 'wx'); break; } catch (e) {
233
+ const code = (e as NodeJS.ErrnoException).code;
234
+ if (code === 'ENOENT') { try { mkdirSync(dirname(path), { recursive: true }); } catch { break; } continue; }
235
+ if (code !== 'EEXIST') break;
236
+ try { if (Date.now() - statSync(lock).mtimeMs > 30_000) { unlinkSync(lock); continue; } } catch { continue; }
237
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
238
+ }
239
+ }
240
+ if (fd === null) { try { debug.log('outbound.send', 'deferred-lock-unavailable', { lock: basename(lock) }); } catch { /* */ } }
241
+ try { return fn(); } finally {
242
+ if (fd !== null) { try { closeSync(fd); } catch { /* */ } try { unlinkSync(lock); } catch { /* */ } }
243
+ }
244
+ }
245
+
246
+ /** 이보다 오래된 claim 은 소유 pid 가 살아 있어도 회수한다(pid 재사용 대비 · 정상 flush 는 이보다 훨씬 짧다). */
247
+ const STALE_CLAIM_MS = 15 * 60_000;
248
+
249
+ let claimSeq = 0;
250
+ /** 큐를 «가져간다» — 원자적 rename 으로 이 flush 만의 파일로 옮긴다. flush 도중 적재되는 항목은 새 큐 파일에
251
+ * 쌓이므로 잃지 않는다. 죽은 flusher(또는 이 프로세스의 앞 호출)가 남긴 claim 도 같이 회수한다. */
252
+ function claimDeferred(path: string): string[] {
253
+ const claims: string[] = [];
254
+ const next = (): string => `${path}.flushing-${process.pid}-${Date.now()}-${claimSeq++}`;
255
+ const dir = dirname(path);
256
+ const prefix = `${basename(path)}.flushing-`;
257
+ try {
258
+ for (const name of readdirSync(dir)) {
259
+ if (!name.startsWith(prefix)) continue;
260
+ const m = /^(\d+)-(\d+)-/.exec(name.slice(prefix.length));
261
+ const pid = Number(m?.[1]);
262
+ const at = Number(m?.[2]);
263
+ if (!Number.isSafeInteger(pid)) continue;
264
+ // flushDeferred 는 동기라 같은 pid 의 claim 은 «지난 호출의 잔여»다. 남의 것은 죽었거나 오래됐을 때만.
265
+ const stale = Number.isFinite(at) && Date.now() - at > STALE_CLAIM_MS;
266
+ if (pid !== process.pid && !stale && pidAlive(pid)) continue;
267
+ const own = next();
268
+ try { renameSync(join(dir, name), own); claims.push(own); } catch { /* another flusher took it */ }
269
+ }
270
+ } catch { /* no directory yet */ }
271
+ withQueueLock(path, () => {
272
+ if (!existsSync(path)) return;
273
+ const own = next();
274
+ try { renameSync(path, own); claims.push(own); } catch { /* another flusher took it */ }
275
+ });
276
+ return claims;
277
+ }
278
+
279
+ /** flushDeferred 주입점 — 시험은 실제 발송 대신 sendBatch 를 준다. */
280
+ export type FlushDeferredDeps = {
281
+ sendBatch?: (text: string, kind: string, origin?: MissionOrigin) => boolean;
282
+ maxAttempts?: number;
283
+ };
284
+
285
+ /** 보류분 일괄 발송 — 묶음 단위. 배달 건수 반환.
286
+ * 무음 창 밖 첫 sendOutbound 가 자동 호출 + 06:31 플러시 크론이 보장.
287
+ * ⭐ 실패한 묶음의 항목만 큐에 남긴다(성공 묶음은 다시 보내지 않는다 · 10-07 야간 11회 재발송 사고).
288
+ * 실패가 `maxAttempts` 번 쌓인 항목은 `<name>.failed.jsonl` 로 격리한다. */
289
+ export function flushDeferred(path = DEFERRED_PATH, deps: FlushDeferredDeps = {}): number {
204
290
  ensureOutboundLogSink();
205
- const observeFlush = (count: number, lagMin: number, kinds: string[]): void => {
291
+ const observeFlush = (
292
+ count: number, lagMin: number, kinds: string[],
293
+ outcome: { sent: number; kept: number; quarantined: number },
294
+ ): void => {
206
295
  const lagWarnMin = flushLagWarnMin();
207
296
  const over = count > 0 && lagMin > lagWarnMin;
208
297
  logSend(
209
298
  'flush',
210
299
  'deferred-batch',
211
- { count, lagMin, kinds: kinds.map(safeObservationLabel), path, lagWarnMin },
300
+ { count, lagMin, kinds: kinds.map(safeObservationLabel), path, lagWarnMin, ...outcome },
212
301
  over ? { level: 'warn' } : undefined,
213
302
  );
214
303
  };
215
- if (!existsSync(path)) {
216
- observeFlush(0, 0, []);
304
+ const none = { sent: 0, kept: 0, quarantined: 0 };
305
+ const claims = claimDeferred(path);
306
+ if (claims.length === 0) {
307
+ observeFlush(0, 0, [], none);
217
308
  return 0;
218
309
  }
219
- type Item = { ts: string; kind: string; text: string; origin?: MissionOrigin };
220
- let items: Item[] = [];
221
- try {
222
- items = readFileSync(path, 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
223
- } catch { /* 손상 라인 무시 */ }
310
+ type Item = { ts: string; kind: string; text: string; origin?: MissionOrigin; attempts?: number };
311
+ const items: Item[] = [];
312
+ const corrupt: string[] = [];
313
+ const readClaims: string[] = [];
314
+ for (const claim of claims) {
315
+ let body = '';
316
+ try { body = readFileSync(claim, 'utf-8'); } catch (e) {
317
+ // 못 읽은 claim 은 지우지 않는다 — 다음 flush 가 다시 회수한다.
318
+ try { debug.log('outbound.send', 'deferred-claim-unreadable', { error: e instanceof Error ? e.message : String(e) }, { level: 'warn' }); } catch { /* */ }
319
+ continue;
320
+ }
321
+ readClaims.push(claim);
322
+ for (const line of body.split('\n')) {
323
+ if (!line.trim()) continue;
324
+ try {
325
+ const rec = JSON.parse(line) as Item;
326
+ if (rec && typeof rec === 'object' && typeof rec.text === 'string') items.push(rec);
327
+ else corrupt.push(line);
328
+ } catch { corrupt.push(line); }
329
+ }
330
+ }
331
+ const qPath = deferredQuarantinePath(path);
332
+ const settleFailed = (stage: string, e: unknown): void => {
333
+ try { debug.log('outbound.send', 'deferred-settle-failed', { stage, error: e instanceof Error ? e.message : String(e) }, { level: 'warn' }); } catch { /* */ }
334
+ };
335
+ /** 읽은 claim 을 정산한다. 격리 기록이 실패하면 그 줄은 큐로 돌린다(다음에 다시 격리).
336
+ * 큐 기록이 실패하면 claim 하나를 «남길 줄만»으로 바꿔 두어 다음 flush 가 배달분을 다시 보내지 않게 한다. */
337
+ /** 정산된 claim 을 걷는다. 삭제가 실패하면 비워 둔다 — 남은 claim 이 다음 flush 에 배달분을 다시 내지 않게. */
338
+ const retireClaim = (claim: string): void => {
339
+ try { unlinkSync(claim); return; } catch (e) { settleFailed('claim-unlink', e); }
340
+ try { writeFileSync(claim, ''); } catch (e) { settleFailed('claim-truncate', e); }
341
+ };
342
+ type Quarantine = { line: string; kind?: string; attempts?: number };
343
+ /** 반환 = 실제 정산 결과(관측은 이것으로 센다). */
344
+ const settle = (keep: string[], quarantine: Quarantine[]): { kept: number; quarantined: number } => {
345
+ let back = keep;
346
+ let quarantined = 0;
347
+ if (quarantine.length) {
348
+ try {
349
+ appendFileSync(qPath, quarantine.map(q => q.line).join('\n') + '\n');
350
+ quarantined = quarantine.length;
351
+ for (const q of quarantine) {
352
+ if (q.kind === undefined) continue;
353
+ try { debug.log('outbound.send', 'deferred-quarantined', { kind: safeObservationLabel(q.kind), attempts: q.attempts }); } catch { /* */ }
354
+ }
355
+ } catch (e) { settleFailed('quarantine', e); back = [...keep, ...quarantine.map(q => q.line)]; }
356
+ }
357
+ const result = { kept: back.length, quarantined };
358
+ if (readClaims.length === 0) return result;
359
+ let wrote = back.length === 0;
360
+ if (!wrote) {
361
+ try { withQueueLock(path, () => appendFileSync(path, back.join('\n') + '\n')); wrote = true; } catch (e) { settleFailed('queue', e); }
362
+ }
363
+ if (!wrote) {
364
+ const first = readClaims[0]!;
365
+ const tmp = join(dirname(first), `.${basename(first)}.tmp`);
366
+ try {
367
+ writeFileSync(tmp, back.join('\n') + '\n');
368
+ renameSync(tmp, first);
369
+ for (const claim of readClaims.slice(1)) retireClaim(claim);
370
+ } catch (e) { settleFailed('claim-rewrite', e); }
371
+ return result;
372
+ }
373
+ for (const claim of readClaims) retireClaim(claim);
374
+ return result;
375
+ };
376
+ const maxAttempts = deps.maxAttempts ?? deferredMaxAttempts();
377
+ const priorAttempts = (it: Item): number =>
378
+ typeof it.attempts === 'number' && Number.isSafeInteger(it.attempts) && it.attempts > 0 ? it.attempts : 0;
379
+ const quarantine: Quarantine[] = corrupt.map(line => ({ line }));
380
+ // 이미 상한에 닿은 줄(앞 flush 의 격리 기록이 실패해 큐로 돌아온 것)은 보내지 않고 다시 격리만 시도한다.
381
+ for (let i = items.length - 1; i >= 0; i--) {
382
+ const it = items[i]!;
383
+ if (priorAttempts(it) < maxAttempts) continue;
384
+ quarantine.push({ line: JSON.stringify(it), kind: it.kind, attempts: priorAttempts(it) });
385
+ items.splice(i, 1);
386
+ }
224
387
  if (items.length === 0) {
225
- try { unlinkSync(path); } catch { /* */ }
226
- observeFlush(0, 0, []);
388
+ const settled = settle([], quarantine);
389
+ observeFlush(0, 0, [], { ...none, ...settled });
227
390
  return 0;
228
391
  }
229
392
  const kst = (iso: string) => {
@@ -236,8 +399,7 @@ export function flushDeferred(path = DEFERRED_PATH): number {
236
399
  `🌙 야간 보류 알림 ${list.length}건 (00:00~06:30 KST 무음 · ⏳ 최대 ${lagMin}분 지연 — 일괄 전달)`,
237
400
  ...list.map(i => `\n── [${kst(i.ts)} · ${i.kind}] ──\n${i.text}`),
238
401
  ].join('\n');
239
- observeFlush(items.length, lagMin, [...new Set(items.map(i => i.kind))]);
240
- const sendBatch = (text: string, kind: string, origin?: MissionOrigin): boolean => {
402
+ const sendBatch = deps.sendBatch ?? ((text: string, kind: string, origin?: MissionOrigin): boolean => {
241
403
  let bot = 'unknown';
242
404
  const path = origin
243
405
  ? (deliverToOrigin(origin, text, label => { bot = label; }) ? 'origin' : false)
@@ -250,15 +412,17 @@ export function flushDeferred(path = DEFERRED_PATH): number {
250
412
  });
251
413
  } catch { /* observation must not change delivery */ }
252
414
  return path !== false;
415
+ });
416
+ const failed: Item[] = [];
417
+ const send = (batch: Item[], kind: string, origin?: MissionOrigin): void => {
418
+ if (!sendBatch(fmt(batch), kind, origin)) failed.push(...batch);
253
419
  };
254
- let delivered = 0;
255
420
  // origin 없는 매매 알림만 report 묶음. 그 외는 kind 별로 운영 봇 경계를 유지한다.
256
421
  const noOrigin = items.filter(i => !i.origin);
257
422
  const tradingBatch = noOrigin.filter(i => !isOperationalKind(i.kind));
258
- if (tradingBatch.length && sendBatch(fmt(tradingBatch), 'report')) delivered += tradingBatch.length;
423
+ if (tradingBatch.length) send(tradingBatch, 'report');
259
424
  for (const kind of new Set(noOrigin.filter(i => isOperationalKind(i.kind)).map(i => i.kind))) {
260
- const batch = noOrigin.filter(i => i.kind === kind);
261
- if (sendBatch(fmt(batch), kind)) delivered += batch.length;
425
+ send(noOrigin.filter(i => i.kind === kind), kind);
262
426
  }
263
427
  // origin 있는 것 = 발신 채널·스레드별로 묶어 서로 다른 수신자에게 섞이지 않게 발송.
264
428
  const groups = new Map<string, { origin: MissionOrigin; list: Item[] }>();
@@ -272,10 +436,19 @@ export function flushDeferred(path = DEFERRED_PATH): number {
272
436
  }
273
437
  for (const g of groups.values()) {
274
438
  const kinds = new Set(g.list.map(i => i.kind));
275
- if (sendBatch(fmt(g.list), kinds.size === 1 ? g.list[0]!.kind : 'mixed', g.origin)) delivered += g.list.length;
439
+ send(g.list, kinds.size === 1 ? g.list[0]!.kind : 'mixed', g.origin);
440
+ }
441
+ // 실패 묶음의 항목만 되돌린다(시도 수 +1). 상한에 닿은 항목은 격리 — 영원히 재시도하지 않는다.
442
+ const keep: string[] = [];
443
+ for (const it of failed) {
444
+ const attempts = priorAttempts(it) + 1;
445
+ const line = JSON.stringify({ ...it, attempts });
446
+ if (attempts >= maxAttempts) quarantine.push({ line, kind: it.kind, attempts });
447
+ else keep.push(line);
276
448
  }
277
- // 전량 배달 성공 시에만 파일 제거(부분 실패는 다음 flush 재시도 — 성공분 중복은 드문 엣지 수용).
278
- if (delivered >= items.length) { try { unlinkSync(path); } catch { /* */ } }
449
+ const settled = settle(keep, quarantine);
450
+ const delivered = items.length - failed.length;
451
+ observeFlush(items.length, lagMin, [...new Set(items.map(i => i.kind))], { sent: delivered, ...settled });
279
452
  return delivered;
280
453
  }
281
454
 
@@ -1,8 +1,23 @@
1
- import { sendOutbound, setInProcessOutbound } from './outbound-alert.js';
1
+ import { inQuietHours, kstMinutes, sendOutbound, setInProcessOutbound } from './outbound-alert.js';
2
2
  import { setUserConfigOverlay } from '../user-config.js';
3
3
 
4
4
  // Focused subprocess fixture: isolated state/config roots and a fake curl on PATH.
5
5
  setUserConfigOverlay(cfg => ({ ...cfg, telegram: { ...cfg.telegram, botToken: '123456:private-secret', homeChannel: 98765 } }));
6
6
  process.env.SEND_VIA_ELANOUS = '0';
7
+ // Quiet hours (00:00~06:30 KST) defer instead of sending, so a gate run at night saw no `sent` row.
8
+ // Shift this fixture's clock back to 23:59 KST of the previous day; the shift stays inside this process.
9
+ const quietShiftMs = inQuietHours() ? (kstMinutes() + 1) * 60_000 : 0;
10
+ if (quietShiftMs > 0) {
11
+ const RealDate = Date;
12
+ const shifted = () => RealDate.now() - quietShiftMs;
13
+ class ShiftedDate extends RealDate {
14
+ constructor(...args: unknown[]) {
15
+ if (args.length === 0) super(shifted());
16
+ else super(...(args as [string | number | Date]));
17
+ }
18
+ static override now(): number { return shifted(); }
19
+ }
20
+ globalThis.Date = ShiftedDate as DateConstructor;
21
+ }
7
22
  setInProcessOutbound(null);
8
23
  if (!sendOutbound('SECRET-BODY-DO-NOT-LOG', 'ops-alert', { channel: 'cli', surface: process.env.OUTBOUND_PROBE_SOURCE ?? 'outbound-log-probe' })) process.exitCode = 1;
@@ -2,7 +2,7 @@ import type { LLMToolSpec } from '../llm.js';
2
2
  import type { ToolRuntimeContext } from '../tool-runtime/types.js';
3
3
  import { debug } from '../debug/log.js';
4
4
  import { requestConfirmation, type ConfirmOpts, type ConfirmResult } from '../hitl/confirm.js';
5
- import { devVersion, listChecklist, summarizeChecklist, type Checklist } from '../release-loop/checklist.js';
5
+ import { devVersion, checklistDevVersion, listChecklist, summarizeChecklist, type Checklist } from '../release-loop/checklist.js';
6
6
  import { add, move, validateVersion } from '../release-loop/feature-store.js';
7
7
  import { formatKst, getSchedule, setSchedule, type ReleaseSchedule } from '../release-loop/release-schedule.js';
8
8
  import { latestGraphRun, type GraphRunState } from '../graph-runner/runner.js';
@@ -64,7 +64,7 @@ function dDay(days: number): string {
64
64
  export function dispatchReleaseStatus(args: Record<string, unknown>, deps: ReleaseToolDeps = {}): { text: string; structured: Record<string, unknown> } {
65
65
  const requested = args.version;
66
66
  if (requested !== undefined && (typeof requested !== 'string' || !requested.trim())) throw new Error('판 이름이 필요합니다');
67
- const version = requested ? null : (deps.devVersion?.() ?? devVersion());
67
+ const version = requested ? null : checklistDevVersion(deps.devVersion?.() ?? devVersion());
68
68
  const versions = requested ? [requested as string] : [version!, nextVersion(version!)];
69
69
  const now = deps.now ?? new Date();
70
70
  const run = (deps.latestRun ?? latestGraphRun)('release-loop');