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
@@ -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');
@@ -3,6 +3,7 @@ import { decideBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInp
3
3
  import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from '../harness/harness-substrate-default.js';
4
4
  import { podMemoryLimitFor } from '../task-orchestrator/surfaces/self-implement-pod.js';
5
5
  import { recallMemoryContext } from '../agent-substrate/execution/memory-context.js';
6
+ import { CONTEXT_CARD_SCREEN_CHARS } from '../context-card/index.js';
6
7
 
7
8
  export interface GateResult<T> {
8
9
  decision: T;
@@ -169,6 +170,7 @@ export async function relationGate(
169
170
  export interface MemoryGateDeps {
170
171
  recall: (query: string) => Promise<string>;
171
172
  priorAbandonment: (goalId: string) => Promise<string | undefined> | string | undefined;
173
+ priorMustFix?: (goalId: string) => readonly string[];
172
174
  }
173
175
 
174
176
  export interface MemoryDecision {
@@ -183,15 +185,22 @@ export async function memoryGate(
183
185
  card: Pick<TaskCard, 'goalId' | 'title'>,
184
186
  deps: MemoryGateDeps = { recall: recallMemoryContext, priorAbandonment: () => undefined },
185
187
  ): Promise<GateResult<MemoryDecision>> {
186
- const [context, priorAbandonment] = await Promise.all([
188
+ const [recall, priorAbandonment] = await Promise.all([
187
189
  deps.recall(card.title), deps.priorAbandonment(card.goalId),
188
190
  ]);
191
+ // must-fix 와 recall 은 서로 독립이다 — recall 이 비어도 앞선 must-fix 는 맥락에 남는다.
192
+ const mustFix = deps.priorMustFix?.(card.goalId) ?? [];
193
+ const mustFixBlock = mustFix.length ? `앞선 must-fix:\n${mustFix.map((item) => `- ${item}`).join('\n')}` : '';
194
+ const combined = [mustFixBlock, recall].filter(Boolean).join('\n');
195
+ const codePoints = [...combined];
196
+ const truncated = codePoints.length > CONTEXT_CARD_SCREEN_CHARS;
197
+ const context = truncated ? codePoints.slice(0, CONTEXT_CARD_SCREEN_CHARS).join('') : combined;
189
198
  const decision: MemoryDecision = {
190
199
  action: 'record', context, fragmentIds: [],
191
200
  ...(priorAbandonment === undefined ? {} : { priorAbandonment }),
192
201
  };
193
202
  return {
194
203
  decision,
195
- explanation: `memory=${context ? 'recalled' : 'none or unavailable'}; prior abandonment=${priorAbandonment ?? 'unknown'}; fragment IDs unavailable from recall API`,
204
+ explanation: `memory=${recall ? 'recalled' : 'none or unavailable'}; prior abandonment=${priorAbandonment ?? 'unknown'}; fragment IDs unavailable from recall API${truncated ? `; truncated; chars=${[...context].length}/${CONTEXT_CARD_SCREEN_CHARS}` : ''}`,
196
205
  };
197
206
  }
@@ -1,23 +1,27 @@
1
1
  import type { TaskCard } from '../task-cards/card-store.js';
2
2
 
3
- export interface SplitCell { id: string; title: string; seat?: string; host?: string }
3
+ export interface SplitCell { id: string; title: string; kind?: string; seat?: string; host?: string }
4
4
 
5
5
  /** FLOW1: only explicitly assigned work may enter a seat's queue. The same proposal is used in shadow and live. */
6
6
  export function splitCard(card: TaskCard, _opts: { shadow?: boolean } = {}): SplitCell[] {
7
7
  const intake = [...card.sections].reverse().find(section => section.key.startsWith('intake:wish:'));
8
8
  let text = card.title;
9
+ let kind: string | undefined;
9
10
  if (intake) {
10
11
  let body: unknown;
11
12
  try { body = JSON.parse(intake.content); } catch { body = intake.content; }
12
13
  if (typeof body === 'string') text = body;
13
- else if (body && typeof body === 'object' && typeof (body as { text?: unknown }).text === 'string') text = (body as { text: string }).text;
14
+ else if (body && typeof body === 'object') {
15
+ if (typeof (body as { text?: unknown }).text === 'string') text = (body as { text: string }).text;
16
+ if (typeof (body as { kind?: unknown }).kind === 'string') kind = (body as { kind: string }).kind;
17
+ }
14
18
  }
15
19
  const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
16
20
  const cells: SplitCell[] = lines.map((line, index) => {
17
21
  const match = /^(?:[-*]|\d+[.)])?\s*\[(OP|MK|TC|UX)\]\s*(.+)$/u.exec(line);
18
- if (match) return { id: `${card.id}-${index + 1}`, title: match[2]!.trim(), seat: match[1]! };
22
+ if (match) return { id: `${card.id}-${index + 1}`, title: match[2]!.trim(), seat: match[1]!, ...(kind !== undefined ? { kind } : {}) };
19
23
  // 담당 없는 행도 칸으로 남긴다 — seat 가 없으니 대기열엔 못 들어가고 tick 이 unplaced-no-owner 로 센다.
20
- return { id: `${card.id}-${index + 1}`, title: line.replace(/^(?:[-*]|\d+[.)])\s*/u, '') };
24
+ return { id: `${card.id}-${index + 1}`, title: line.replace(/^(?:[-*]|\d+[.)])\s*/u, ''), ...(kind !== undefined ? { kind } : {}) };
21
25
  });
22
26
  // 의뢰 본문이 있으면 담당이 하나도 없어도 행마다 칸으로 남긴다. 본문 없이 제목뿐일 때만 카드 한 칸.
23
27
  return intake || cells.some(cell => cell.seat) ? cells : [{ id: card.id, title: card.title }];
@@ -20,6 +20,9 @@ import { debug } from '../debug/log.js';
20
20
  import { getUserConfig } from '../user-config.js';
21
21
 
22
22
  const GIT_TIMEOUT_MS = 30_000;
23
+ /** 얕은 클론(Pod `git clone --depth 50` · docker/harness/job.yaml)에서 base 를 가져올 깊이 — 클론과 같은 값.
24
+ * ⛔ 깊이 없이 얕은 저장소로 다른 브랜치를 fetch 하면 그 브랜치의 «전 이력»을 받는다(실측 193s vs 4s · 30s 에 SIGTERM). */
25
+ const SHALLOW_BASE_FETCH_DEPTH = 50;
23
26
  const WORKTREE_ADD_TIMEOUT_MS = 5 * 60_000;
24
27
 
25
28
  export interface WorktreeAddDeps {
@@ -439,14 +442,31 @@ export function syncBaseWithRemote(
439
442
  // 원격이 "그런 브랜치 없다" 고 답했다 ⇒ 축약 hex 여도 모호하지 않다.
440
443
  if (!hasRemote) return asSha(false) ?? { checkout: base, freshness: 'local-only' };
441
444
 
445
+ // ⭐ 얕은 저장소면 깊이를 묶는다 — 아니면 그 브랜치의 전 이력을 받느라 GIT_TIMEOUT_MS 를 넘긴다
446
+ // (Pod 클론은 `--depth 50` · 비-main base 런이 전부 여기서 SIGTERM 으로 죽었다). 받은 tip 에서 갈리므로
447
+ // 이후 merge-base(HEAD ↔ 그 tip)는 tip 자체라 깊이 50 으로 족하다. 얕지 않으면 종전 인자 그대로다.
448
+ // probe 를 못 읽으면 종전 경로로 간다(값은 'unknown' 으로 관측에 남긴다).
449
+ const shallowProbe = git('rev-parse', '--is-shallow-repository');
450
+ const shallowAnswer = shallowProbe.status === 0 ? (shallowProbe.stdout || '').trim() : '';
451
+ const shallow: boolean | 'unknown' = shallowAnswer === 'true' ? true : shallowAnswer === 'false' ? false : 'unknown';
452
+ const depth = shallow === true ? SHALLOW_BASE_FETCH_DEPTH : null;
453
+ const fetchArgs = depth === null ? ['fetch', 'origin', wanted] : ['fetch', `--depth=${depth}`, 'origin', wanted];
454
+ const fetchStartedAt = Date.now();
442
455
  // Only the no-runner fallback joins the shared retry seam; injected runners remain
443
456
  // an exact test/caller seam with their prior command count.
444
457
  const fetched = runner
445
- ? git('fetch', 'origin', wanted)
446
- : runGitCommand(repoRoot, ['fetch', 'origin', wanted], { encoding: 'utf8', timeout: GIT_TIMEOUT_MS });
458
+ ? git(...fetchArgs)
459
+ : runGitCommand(repoRoot, fetchArgs, { encoding: 'utf8', timeout: GIT_TIMEOUT_MS });
460
+ const fetchMs = Date.now() - fetchStartedAt;
461
+ debug.log('git-fs.worktree', 'base.fetch', { base, shallow, depth, ms: fetchMs, ok: fetched.status === 0, signal: fetched.signal ?? null }, fetched.status === 0 ? undefined : { level: 'warn' });
447
462
  if (fetched.status !== 0) {
448
463
  // origin 에 있는 것을 확인했는데 못 가져왔다 ⇒ 낡은 로컬로 진행하면 조용히 틀린다.
449
- throw new Error(`git worktree base sync failed — origin/${remoteBranch} exists but fetch failed: status=${fetched.status}, signal=${fetched.signal ?? 'none'}, stdout=${JSON.stringify((fetched.stdout || '').trim().slice(0, 200))}, stderr=${JSON.stringify((fetched.stderr || '').trim().slice(0, 200))}`);
464
+ // spawnSync 의 timeout 은 SIGTERM 으로 끝난다 — 「실패」와 「시간 초과」를 문면에서 가른다.
465
+ const timedOut = fetched.status === null && fetched.signal === 'SIGTERM';
466
+ const what = timedOut
467
+ ? `fetch timed out after ${GIT_TIMEOUT_MS / 1000}s (shallow=${shallow}${depth === null ? '' : `, depth=${depth}`})`
468
+ : 'fetch failed';
469
+ throw new Error(`git worktree base sync failed — origin/${remoteBranch} exists but ${what}: status=${fetched.status}, signal=${fetched.signal ?? 'none'}, stdout=${JSON.stringify((fetched.stdout || '').trim().slice(0, 200))}, stderr=${JSON.stringify((fetched.stderr || '').trim().slice(0, 200))}`);
450
470
  }
451
471
  // ⭐ **가져온 것**에서 갈린다(리뷰 must-fix ②) — ls-remote 로 먼저 읽은 SHA 를 쓰면 그 사이 원격이
452
472
  // 전진했을 때 "원격 tip" 계약이 깨진다. FETCH_HEAD 가 방금 받은 tip 이다.