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
@@ -0,0 +1,235 @@
1
+ /**
2
+ * TASK-AGENT 과제 넘기기 문 — `elanous task hand "<한 줄>"` (RFC-task-agent-any-task-to-completion §A9 표 1행 · R3).
3
+ *
4
+ * - 과제 카드를 실행부와 «같은» 상태 파일(`effectiveInstanceRoot()/task-agent-actions.json`)의 `tasks` 칸에 적는다.
5
+ * 종전 칸(landingDay·landingsToday·failureCounts)은 그대로 두고 덧붙이기만 한다(하위 호환 — 실행부 쓰기는 `...disk` 로 보존).
6
+ * - 첫 수는 «launch» = `harness say [--seat <자리>] --substrate pod --merge-by-host "<원문>"`.
7
+ * - 기본은 SHADOW: 카드와 명령만 남기고 아무것도 띄우지 않는다. `--live` 일 때만 주입된 launcher 로 띄운다.
8
+ * - 관측: `debug.log('task-agent', 'handed', {taskId, seat, checklistId, mode})`.
9
+ */
10
+ import { randomBytes } from 'node:crypto';
11
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { debug } from '../debug/log.js';
14
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
15
+ import { withFileLockSync } from '../storage/file-lock.js';
16
+
17
+ export const TASK_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
18
+ export type TaskSeat = typeof TASK_SEATS[number];
19
+ /** 과제 카드 id 접두 — `task show <id>` 가 이 접두로 TOX 태스크와 갈린다. */
20
+ export const TASK_CARD_PREFIX = 'ta-';
21
+
22
+ export interface TaskCardEvent { at: string; event: string; detail?: string }
23
+ export interface TaskCardMove { kind: 'launch' | 'wait'; command?: string[]; reason: string }
24
+ export interface TaskCard {
25
+ id: string;
26
+ text: string;
27
+ seat?: TaskSeat;
28
+ checklistId?: string;
29
+ createdAt: string;
30
+ /** launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사. */
31
+ status: 'handed' | 'launched' | 'launch-failed';
32
+ history: TaskCardEvent[];
33
+ /** 미션 카드만 — 조각 카드 id(순서대로) · 쪼갠 출처(`mission.ts` splitMission). */
34
+ pieces?: string[];
35
+ splitSource?: string;
36
+ /** 조각 카드만 — 소속 미션 카드 id · 먼저 착지해야 하는 조각 id(선후). */
37
+ mission?: string;
38
+ after?: string[];
39
+ /** 조각 카드만 — 착지 근거(② `tasks advance --pr|--goal`): 이 조각 런이 연 PR 번호 · 골 id(16자 · 런 원장 `pr-opened` 로 PR 을 찾는다). */
40
+ pr?: number;
41
+ goalId?: string;
42
+ /** 조각 카드만 — ② 가 선행 착지 뒤 넘겼다(한 번만 · 같은 잠금 안에서 먼저 적는다). */
43
+ handed?: { at: string; mode: 'shadow' | 'live'; claim?: string; error?: string };
44
+ /** 미션 카드만 — 모든 조각 착지 → 칸 green «제안»(체크리스트는 손대지 않는다 · OP/주인이 뒤집는다). */
45
+ greenProposal?: { at: string; checklistId: string | null; evidence: Record<string, string> };
46
+ }
47
+
48
+ interface TaskAgentStateFile { tasks?: Record<string, TaskCard>; [key: string]: unknown }
49
+
50
+ export function taskAgentStatePath(): string {
51
+ return join(effectiveInstanceRoot(), 'task-agent-actions.json');
52
+ }
53
+
54
+ /** 상태 파일을 읽을 수 없거나(ENOENT 제외) 해석할 수 없다 — 덮어쓰기 전에 멈춘다. */
55
+ export class TaskAgentStateError extends Error {
56
+ constructor(path: string, cause: unknown) {
57
+ super(`task-agent 상태 파일을 읽을 수 없다 — 덮어쓰지 않고 멈춘다: ${path} (${cause instanceof Error ? cause.message : String(cause)})`);
58
+ this.name = 'TaskAgentStateError';
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 실행부·과제 넘기기 문이 함께 쓰는 상태 파일(`task-agent-actions.json`)의 엄격한 읽기.
64
+ * 파일이 «없음»(ENOENT)만 빈 상태다. 그 밖의 읽기·해석 오류(손상된 JSON · 객체 아님 · 권한)는
65
+ * 던진다 — 빈 상태로 읽고 다시 쓰면 failureCounts·착지 기록·과제 카드가 지워진다.
66
+ */
67
+ export function readTaskAgentState<T extends object = TaskAgentStateFile>(path: string): T {
68
+ let raw: string;
69
+ try { raw = readFileSync(path, 'utf8'); } catch (error) {
70
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return {} as T;
71
+ return stateError(path, error);
72
+ }
73
+ let parsed: unknown;
74
+ try { parsed = JSON.parse(raw); } catch (error) { return stateError(path, error); }
75
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return stateError(path, new Error('최상위가 객체가 아니다'));
76
+ return parsed as T;
77
+ }
78
+
79
+ function stateError(path: string, cause: unknown): never {
80
+ const error = new TaskAgentStateError(path, cause);
81
+ try { debug.log('task-agent', 'state-unreadable', { path, reason: error.message }); } catch { /* fail-soft */ }
82
+ throw error;
83
+ }
84
+
85
+ function readState(path: string): TaskAgentStateFile {
86
+ return readTaskAgentState<TaskAgentStateFile>(path);
87
+ }
88
+
89
+ /** 카드 여럿을 한 잠금 안에서 적는다(미션 ⊕ 조각) — 종전 칸은 `...disk` 로 보존. */
90
+ export function writeTaskCards(path: string, cards: readonly TaskCard[]): void {
91
+ withFileLockSync(`${path}.lock`, () => {
92
+ const disk = readState(path);
93
+ mkdirSync(dirname(path), { recursive: true });
94
+ const tasks = { ...(disk.tasks ?? {}) };
95
+ for (const card of cards) tasks[card.id] = card;
96
+ writeFileSync(path, JSON.stringify({ ...disk, tasks }));
97
+ });
98
+ }
99
+
100
+ /** 카드 하나를 잠금 안에서 읽고 고쳐 쓴다 — `update` 가 undefined 를 돌려주면 쓰지 않는다. 쓴(또는 그대로인) 카드를 돌려준다. */
101
+ export function updateTaskCard(path: string, id: string, update: (card: TaskCard | undefined, tasks: Readonly<Record<string, TaskCard>>) => TaskCard | undefined): TaskCard | undefined {
102
+ return withFileLockSync(`${path}.lock`, () => {
103
+ const disk = readState(path);
104
+ const current = disk.tasks?.[id];
105
+ const next = update(current, disk.tasks ?? {});
106
+ if (!next) return current;
107
+ mkdirSync(dirname(path), { recursive: true });
108
+ writeFileSync(path, JSON.stringify({ ...disk, tasks: { ...(disk.tasks ?? {}), [id]: next } }));
109
+ return next;
110
+ });
111
+ }
112
+
113
+ export function readTaskCard(id: string, path = taskAgentStatePath()): TaskCard | undefined {
114
+ return readState(path).tasks?.[id];
115
+ }
116
+
117
+ /** 첫 수의 elanous 인자(진입점·--config-dir 앞붙임은 launcher 몫). */
118
+ export function launchArgs(card: Pick<TaskCard, 'text' | 'seat'>): string[] {
119
+ return ['harness', 'say', ...(card.seat ? ['--seat', card.seat] : []), '--substrate', 'pod', '--merge-by-host', card.text];
120
+ }
121
+
122
+ /**
123
+ * `landed` = 착지한 조각 id(② 감지가 채운다). 안 주면 «모른다» — 선행은 미착지로 보고(fail-closed)
124
+ * 이유에 «착지 미확인»을 단다(착지했다/안 했다를 단정하지 않는다).
125
+ * 미션 카드는 스스로 발사하지 않는다 · 아직 안 뜬 조각의 선행이 덜 착지했으면 `wait after <id>`.
126
+ */
127
+ export function nextMoveFor(card: TaskCard, landed?: ReadonlySet<string>): TaskCardMove {
128
+ if (card.pieces) return { kind: 'wait', reason: `미션 — 조각 ${card.pieces.length}장이 선후대로 발사된다` };
129
+ const unmet = (card.after ?? []).filter((id) => !landed?.has(id));
130
+ if (card.status !== 'launched' && unmet.length > 0) return { kind: 'wait', reason: `after ${unmet.join(', ')}${landed ? '' : ' · 착지 미확인'}` };
131
+ if (card.status === 'handed') return { kind: 'launch', command: launchArgs(card), reason: '넘겨받은 과제 — 첫 발사' };
132
+ if (card.status === 'launch-failed') return { kind: 'launch', command: launchArgs(card), reason: '직전 발사 실패 — 다시 발사' };
133
+ return { kind: 'wait', reason: '발사됨 — 런 멈춤을 슈퍼바이저 그림자 판단(task-agent.shadow-move)이 받는다' };
134
+ }
135
+
136
+ export function shellQuote(arg: string): string {
137
+ return /^[A-Za-z0-9_./:=@%+-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
138
+ }
139
+
140
+ export type TaskLauncher = (args: string[]) => void | Promise<void>;
141
+
142
+ export interface HandTaskOptions {
143
+ text: string;
144
+ seat?: TaskSeat;
145
+ checklistId?: string;
146
+ live?: boolean;
147
+ statePath?: string;
148
+ /** `--live` 일 때만 불린다. */
149
+ launcher?: TaskLauncher;
150
+ now?: () => Date;
151
+ id?: string;
152
+ /** 미션 조각으로 넘길 때 — 카드에 소속 미션·선후를 싣는다. */
153
+ mission?: string;
154
+ after?: string[];
155
+ /** 착지한 조각 id — `after` 가 모두 여기 있어야 발사한다(② 의 자리). */
156
+ landed?: ReadonlySet<string>;
157
+ /** 카드에 그대로 싣는 칸(② 의 넘김 표지·착지 근거) — handTask 가 카드를 새로 적을 때 지워지지 않게. */
158
+ cardFields?: Pick<TaskCard, 'pr' | 'goalId' | 'handed'>;
159
+ /**
160
+ * 있으면 카드 쓰기마다 잠금 안에서 디스크 카드를 보고 참일 때만 쓴다(② 의 claim 소유 확인).
161
+ * 거짓이면 쓰지 않고 `TaskCardSupersededError` — 다른 넘김이 그 카드를 가져갔다(발사 전이면 발사하지 않는다).
162
+ */
163
+ writeGuard?: (current: TaskCard | undefined) => boolean;
164
+ }
165
+
166
+ /** `writeGuard` 가 거짓 — 다른 넘김이 카드를 가져갔다. 카드는 건드리지 않았다. */
167
+ export class TaskCardSupersededError extends Error {
168
+ constructor(id: string) {
169
+ super(`과제 카드를 다른 넘김이 가져갔다 — 쓰지 않았다: ${id}`);
170
+ this.name = 'TaskCardSupersededError';
171
+ }
172
+ }
173
+
174
+ /** `--live` 발사가 실패했다 — 카드는 launch-failed 로 남았다. */
175
+ export class TaskLaunchError extends Error {
176
+ constructor(card: TaskCard, reason: string) {
177
+ super(`발사 실패 (${card.id} → launch-failed): ${reason}`);
178
+ this.name = 'TaskLaunchError';
179
+ }
180
+ }
181
+
182
+ export interface HandTaskResult { card: TaskCard; move: TaskCardMove; mode: 'shadow' | 'live'; launched: boolean }
183
+
184
+ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
185
+ const text = opts.text.trim();
186
+ if (!text) throw new Error('과제 한 줄이 비어 있다');
187
+ if (opts.seat !== undefined && !TASK_SEATS.includes(opts.seat)) throw new Error(`자리는 ${TASK_SEATS.join('|')} 중 하나다: ${opts.seat}`);
188
+ const now = (opts.now ?? (() => new Date()))();
189
+ const id = opts.id ?? `${TASK_CARD_PREFIX}${now.toISOString().slice(0, 10).replaceAll('-', '')}-${randomBytes(3).toString('hex')}`;
190
+ const mode = opts.live ? 'live' : 'shadow';
191
+ const path = opts.statePath ?? taskAgentStatePath();
192
+ const card: TaskCard = {
193
+ id, text,
194
+ ...(opts.seat ? { seat: opts.seat } : {}),
195
+ ...(opts.checklistId ? { checklistId: opts.checklistId } : {}),
196
+ createdAt: now.toISOString(),
197
+ status: 'handed',
198
+ history: [],
199
+ ...(opts.mission ? { mission: opts.mission, after: [...(opts.after ?? [])] } : {}),
200
+ ...(opts.cardFields ?? {}),
201
+ };
202
+ const move = nextMoveFor(card, opts.landed);
203
+ if (move.kind !== 'launch') throw new Error(`발사할 수 없는 과제다 — ${move.reason}`);
204
+ const guard = opts.writeGuard;
205
+ const writeCard = (target: string, next: TaskCard): void => {
206
+ if (!guard) { writeTaskCards(target, [next]); return; }
207
+ const owned = { ok: false };
208
+ // 착지 근거(pr·goalId)는 `recordPieceRef` 소유 — claim 뒤에 적힌 최신 근거를 디스크에서 이어 받는다.
209
+ updateTaskCard(target, next.id, (current) => {
210
+ if (!guard(current)) return undefined;
211
+ owned.ok = true;
212
+ // next 의 옛 근거는 버리고 디스크의 근거로 «교체»한다 — 정정된 근거 옆에 옛 근거가 되살아나지 않게.
213
+ const { pr: _stalePr, goalId: _staleGoal, ...base } = next;
214
+ return { ...base, ...(current?.pr !== undefined ? { pr: current.pr } : {}), ...(current?.goalId !== undefined ? { goalId: current.goalId } : {}) };
215
+ });
216
+ if (!owned.ok) throw new TaskCardSupersededError(next.id);
217
+ };
218
+ writeCard(path, card);
219
+ try { debug.log('task-agent', 'handed', { taskId: id, seat: card.seat ?? null, checklistId: card.checklistId ?? null, mode }); } catch { /* fail-soft */ }
220
+ if (mode === 'shadow') return { card, move, mode, launched: false };
221
+ if (!opts.launcher) throw new Error('--live 인데 launcher 가 없다');
222
+ try {
223
+ await opts.launcher(move.command!);
224
+ } catch (error) {
225
+ const reason = error instanceof Error ? error.message : String(error);
226
+ const failed: TaskCard = { ...card, status: 'launch-failed', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch-failed', detail: reason }] };
227
+ writeCard(path, failed);
228
+ try { debug.log('task-agent', 'launch-failed', { taskId: id, seat: card.seat ?? null, reason }); } catch { /* fail-soft */ }
229
+ throw new TaskLaunchError(failed, reason);
230
+ }
231
+ const launched: TaskCard = { ...card, status: 'launched', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch', detail: move.command!.join(' ') }] };
232
+ writeCard(path, launched);
233
+ try { debug.log('task-agent', 'launched', { taskId: id, seat: card.seat ?? null }); } catch { /* fail-soft */ }
234
+ return { card: launched, move, mode, launched: true };
235
+ }
@@ -33,6 +33,8 @@ export function extractPodFailureReason(input: {
33
33
  containerReason?: string | null;
34
34
  jobReason?: string;
35
35
  deadlineSeconds?: number;
36
+ /** POD-NORESULT: the stage and PR from the child's parsed result row (disposition) — used only when no log line gives a readable reason. */
37
+ result?: { stage?: string | null; prUrl?: string | null; prNumber?: number | null } | null;
36
38
  }): string {
37
39
  if (input.jobReason === 'DeadlineExceeded') {
38
40
  return `DeadlineExceeded: Job 수명 상한 ${input.deadlineSeconds ?? 'unknown'}초 초과`;
@@ -72,6 +74,14 @@ export function extractPodFailureReason(input: {
72
74
  const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
73
75
  const reason = errorLine?.text.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
74
76
  if (reason) return reason;
77
+ // POD-NORESULT: an unconverged child (exit 2 → BackoffLimitExceeded under backoffLimit 0) often leaves a result row
78
+ // with a stage and PR but no error text — report that instead of «unreadable».
79
+ const stage = input.result?.stage?.trim();
80
+ if (stage) {
81
+ const prNumber = input.result?.prNumber;
82
+ const pr = Number.isSafeInteger(prNumber) && prNumber! > 0 ? `PR #${prNumber}` : input.result?.prUrl ? `PR ${input.result.prUrl}` : '';
83
+ return pr ? `수확 가능(${stage}) · ${pr}` : `수렴 못 함(단계 ${stage})`;
84
+ }
75
85
  const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
76
86
  return `사유 못 읽음: ${why.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240)}`;
77
87
  }
@@ -33,6 +33,13 @@ export const POD_CONTROL_INBOX_DIR = '/tmp/elanous-control.inbox';
33
33
  /** Newest pod log bytes returned when the export passes the 5MB artifact limit (headroom under it). */
34
34
  export const POD_LOGS_KEEP_BYTES = 4_500_000;
35
35
 
36
+ /** Re-emit the child's `{kind:"self", ok, result}` line as one flat `{...result, ok}` line at the end of /tmp/si.out.
37
+ * POD-NORESULT ⓑ (10-07 · 28 failed rc=2 Jobs read back): every child printed its result line, but one
38
+ * (si-task-71f68d57f6b7, image 0.2.15) had stderr lines (`[graph] collect fail`) after it, so the old
39
+ * «last line only» check skipped the flatten and the host kept `ok:false` with no stage or PR number.
40
+ * Scan back to the newest child result line instead; still append exactly one line. */
41
+ const POD_RESULT_FLATTEN = `bun -e 'const fs=require("fs");const p="/tmp/si.out";const lines=fs.readFileSync(p,"utf8").trimEnd().split("\\n");for(let i=lines.length-1;i>=0;i--){const l=lines[i].trim();if(!l.startsWith("{"))continue;let o;try{o=JSON.parse(l)}catch{continue}if(!o||o.kind!=="self")continue;if(o.result&&typeof o.result==="object"&&!Array.isArray(o.result))fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n");break}'`;
42
+
36
43
  export const POD_JOB_DEADLINE_SECONDS = 10_800;
37
44
 
38
45
  /** 자식 컨테이너 «요청» — 🩸 2026-09-27: limits 만 두면 k8s 가 requests=limits(cpu 4)로 잡아, 32코어 노드에
@@ -147,6 +154,8 @@ export interface PodSpawnOptions {
147
154
  credentials?: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
148
155
  /** POD-TOKEN-PREREFRESH 시험 심 — «3시간 안 만료» 계정의 호스트 선갱신(기본 refreshCodexAccountHome). */
149
156
  codexPrerefresh?: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
157
+ /** PREREFRESH-LOCK 시험 심 — 계정별 갱신 잠금 파일 경로(기본 = 그 계정 codexHome 옆). */
158
+ codexPrerefreshLockPath?: (account: string) => string | null;
150
159
  /** 시험 심 — 이 호스트가 본부 임대 보유자인가(기본 HQ 펜스 · kubectl 주입 시험이면 false). */
151
160
  hqLeaseHolder?: () => boolean | Promise<boolean>;
152
161
  grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
@@ -186,17 +195,29 @@ export class PodCodexExpiringError extends Error {
186
195
  }
187
196
  }
188
197
 
189
- export type PodCredentialPrerefreshOutcome = 'refreshed' | 'still-expiring' | 'refresh-failed' | 'no-lease';
198
+ export type PodCredentialPrerefreshOutcome =
199
+ | 'refreshed' | 'still-expiring' | 'refresh-failed' | 'no-lease'
200
+ // PREREFRESH-LOCK (10-06): a peer refreshed it while we waited for the lock · a failed refresh's one re-check.
201
+ | 'refresh-skipped-already-fresh' | 'refresh-failed-recheck-ok' | 'refresh-failed-recheck-failed'
202
+ // 갱신을 «시도하지 않았다» — 잠금 자리를 못 구했거나 잠금을 못 잡았다(갱신 실패와 가른다 · reason = no-lock-path|lock-timeout|lock-error).
203
+ | 'lock-unavailable';
190
204
 
191
205
  /** POD-TOKEN-PREREFRESH (10-06) — «3시간 안 만료» 계정은 거부만 하지 말고 선갱신을 한 번 시도한다.
192
206
  * 🩸 05:3x team · 09:47 third: 거부만 하고 다음 계정으로 안 넘어가 그 계정으로 뽑힌 Pod 런이 전부 pod-error.
193
207
  * ⛔ 갱신은 본부(HQ) 임대 보유자 한 곳에서만 — refresh 토큰은 갱신마다 회전해 두 곳이 갱신하면 로그아웃된다.
208
+ * ⭐ PREREFRESH-LOCK (10-06 · #24449 사후 리뷰): 같은 호스트의 두 런이 같은 계정을 «동시에» 고르면 갱신이 둘이 돌아
209
+ * 뒤엣것이 회전된 refresh 토큰으로 실패하고 그 계정을 버렸다. ⇒ 계정별 파일 잠금 안에서 ⑴ 먼저 다시 읽어
210
+ * 이미 신선하면 갱신하지 않고 ⑵ 갱신이 실패해도 한 번 다시 읽어(동료가 갱신했을 수 있다) 신선하면 쓴다.
194
211
  * null = 이 계정은 후보에서 뺀다(호출자가 다음 계정으로). 만료 아닌 다른 자격 오류는 그대로 던진다. */
195
212
  export async function podCodexCredentialWithPrerefresh(account: string, deps: {
196
213
  credentials: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
197
214
  refresh: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
198
215
  isLeaseHolder: () => boolean | Promise<boolean>;
199
216
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
217
+ /** 계정별 갱신 잠금 파일(기본 = 그 계정 codexHome 옆 · `codexAccountRefreshLockPath`). null = 잠금 없음. */
218
+ lockPath?: (account: string) => string | null;
219
+ /** 잠금 대기 조정(시험 심) — 기본 CODEX_REFRESH_LOCK_OPTS. */
220
+ lockOpts?: { staleMs?: number; retryBusyMs?: number; maxTries?: number };
200
221
  }): Promise<{ elanousAuth: string; codexAuth: string; ghToken: string } | null> {
201
222
  const log = deps.log ?? ((category, event, data) => debug.log(category, event, data));
202
223
  try { return deps.credentials(account); }
@@ -209,17 +230,56 @@ export async function podCodexCredentialWithPrerefresh(account: string, deps: {
209
230
  let holder = false;
210
231
  try { holder = await deps.isLeaseHolder(); } catch { holder = false; }
211
232
  if (!holder) { observe('no-lease', null); return null; }
212
- let refreshed: Awaited<ReturnType<typeof deps.refresh>>;
213
- try { refreshed = await deps.refresh(account); }
214
- catch { observe('refresh-failed', null, 'threw'); return null; }
215
- if (!refreshed.ok) { observe('refresh-failed', null, refreshed.kind); return null; }
216
- try {
217
- const credential = deps.credentials(account);
233
+ /** One re-read: fresh credential, null when still expiring. Other credential errors throw. */
234
+ const reread = (): { elanousAuth: string; codexAuth: string; ghToken: string } | { expiringH: number } => {
235
+ try { return deps.credentials(account); }
236
+ catch (again) {
237
+ if (!(again instanceof PodCodexExpiringError)) throw again;
238
+ return { expiringH: again.hoursLeft };
239
+ }
240
+ };
241
+ const codex = await import('../../oauth/codex.js');
242
+ let lockPath: string | null = null;
243
+ let lockUnknown = false;
244
+ try { lockPath = (deps.lockPath ?? ((name: string) => codex.codexAccountRefreshLockPath(name)))(account); } catch { lockUnknown = true; }
245
+ // 이름 있는 계정인데 잠금 자리를 못 구하면 잠금 없이 갱신하지 않는다 — 막으려던 중복 갱신이 그 자리에서 재발한다.
246
+ // 갱신은 건너뛰고 한 번만 다시 읽는다(동료가 이미 갱신했으면 그대로 쓴다).
247
+ if (lockUnknown || (lockPath === null && account !== (await import('../../oauth/codex-account.js')).DEFAULT_CODEX_ACCOUNT)) {
248
+ const late = reread();
249
+ if (!('expiringH' in late)) { observe('refresh-skipped-already-fresh', null, 'no-lock-path'); return late; }
250
+ observe('lock-unavailable', null, 'no-lock-path');
251
+ return null;
252
+ }
253
+ const critical = async () => {
254
+ // ⑴ 잠금을 기다리는 사이 동료 런이 갱신했으면 그대로 쓴다 — 두 번째 갱신이 회전된 토큰으로 실패하는 자리.
255
+ const first = reread();
256
+ if (!('expiringH' in first)) { observe('refresh-skipped-already-fresh', null); return first; }
257
+ let refreshed: Awaited<ReturnType<typeof deps.refresh>> | null = null;
258
+ let failReason = 'threw';
259
+ try { refreshed = await deps.refresh(account); }
260
+ catch { refreshed = null; }
261
+ if (refreshed && !refreshed.ok) failReason = refreshed.kind;
262
+ if (!refreshed || !refreshed.ok) {
263
+ observe('refresh-failed', null, failReason);
264
+ // ⑵ 버리기 전에 한 번 다시 읽는다 — 다른 호스트 경로(사람 `account refresh`)가 방금 갱신했을 수 있다.
265
+ let recheck: ReturnType<typeof reread>;
266
+ try { recheck = reread(); } catch { observe('refresh-failed-recheck-failed', null, 'recheck-error'); return null; }
267
+ if (!('expiringH' in recheck)) { observe('refresh-failed-recheck-ok', null); return recheck; }
268
+ observe('refresh-failed-recheck-failed', recheck.expiringH);
269
+ return null;
270
+ }
271
+ const after = reread();
272
+ if ('expiringH' in after) { observe('still-expiring', after.expiringH); return null; }
218
273
  observe('refreshed', refreshed.afterH);
219
- return credential;
220
- } catch (again) {
221
- if (!(again instanceof PodCodexExpiringError)) throw again;
222
- observe('still-expiring', again.hoursLeft);
274
+ return after;
275
+ };
276
+ try { return await codex.withCodexAccountRefreshLock(lockPath, critical, deps.lockOpts ?? codex.CODEX_REFRESH_LOCK_OPTS); }
277
+ catch (lockError) {
278
+ if (!(lockError instanceof codex.CodexRefreshLockUnavailableError)) throw lockError;
279
+ // 잠금을 못 잡았다(대기 초과 · 잠금 파일 생성 실패) — 갱신은 하지 않고 한 번만 다시 읽는다.
280
+ const late = reread();
281
+ if (!('expiringH' in late)) { observe('refresh-skipped-already-fresh', null, lockError.reason); return late; }
282
+ observe('lock-unavailable', null, lockError.reason);
223
283
  return null;
224
284
  }
225
285
  }
@@ -688,11 +748,11 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
688
748
  ...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
689
749
  // 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
690
750
  // ⛔ `--author-grade` 는 Pod 안 `elanous`(이미지에 깔린 판)가 모를 수 있어 넘기지 않는다 — 10-06 «unknown option» 즉사(#24445 되돌림).
691
- // lite 는 지금 로컬 저작 전용이다. Pod 저작은 설정·기본(full)을 따른다.
751
+ // AUTHOR-LITE2-POD — 등급은 환경변수 `ELANOUS_AUTHOR_GRADE` 로 싣는다(옛 Pod elanous 는 모르는 환경변수를 무시하고 full 로 돈다).
692
752
  o.authorSentence
693
- ? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; bun -e 'const fs=require("fs");const p="/tmp/si.out";const s=fs.readFileSync(p,"utf8");const line=s.trimEnd().split("\\n").at(-1);try{const o=JSON.parse(line);if(o.kind==="self"&&o.result&&typeof o.result==="object")fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n")}catch{}'`
753
+ ? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); ${o.authorGrade === 'lite' || o.authorGrade === 'full' ? `ELANOUS_AUTHOR_GRADE=${o.authorGrade} ` : ''}elanous harness say --substrate local --json${quotedAsk} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; ${POD_RESULT_FLATTEN}`
694
754
  : goalPath
695
- ? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; bun -e 'const fs=require("fs");const p="/tmp/si.out";const s=fs.readFileSync(p,"utf8");const line=s.trimEnd().split("\\n").at(-1);try{const o=JSON.parse(line);if(o.kind==="self"&&o.result&&typeof o.result==="object")fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n")}catch{}'`
755
+ ? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; ${POD_RESULT_FLATTEN}`
696
756
  : `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
697
757
  'cat /tmp/si.out',
698
758
  '[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
@@ -1054,6 +1114,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1054
1114
  credentials: accountCredentials,
1055
1115
  refresh: options.codexPrerefresh ?? (async (name: string) => (await import('../../oauth/codex.js')).refreshCodexAccountHome(name)),
1056
1116
  isLeaseHolder: options.hqLeaseHolder ?? (options.kubectl ? () => false : defaultHqLeaseHolder), // kubectl 주입(=시험)이면 본부 임대를 묻지 않는다
1117
+ // PREREFRESH-LOCK: 시험(kubectl 주입)은 정본 스토어를 읽어 잠금 경로를 풀지 않는다 — 임시 디렉터리의 계정별 잠금.
1118
+ // 잠금 자리는 주입 여부와 무관하게 계정 홈 옆 하나 — 시험은 codexPrerefreshLockPath 로 명시한다(같은 계정이 서로 다른 잠금을 쓰지 않게).
1119
+ ...(options.codexPrerefreshLockPath ? { lockPath: options.codexPrerefreshLockPath } : {}),
1057
1120
  };
1058
1121
  const candidates = grok ? [] : plannedCodexAccounts ?? [plannedAccount];
1059
1122
  const usable: Array<{ name: string; credential: ReturnType<typeof accountCredentials> }> = [];
@@ -1552,12 +1615,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1552
1615
  }
1553
1616
  const releaseHold = regate.failures[0]?.step === 'release-path-hold';
1554
1617
  const frozen = regate.status === 'frozen';
1618
+ // FREEZE-POD: the host is the only merger of a Pod run, so its freeze check here is the one that holds it.
1619
+ if (frozen) debug.log('self-implement.pod', 'merge-blocked-by-freeze', { job: name, runId: liveChildRunId, pr: prNumber ?? null, reason: 'landing-freeze' });
1555
1620
  disposition = { ...disposition, stage: frozen || releaseHold ? 'pr-opened' : regate.passed ? 'merged' : 'host-regate-failed', merged: regate.passed && !frozen, hostRegate: regate, ok: regate.passed || releaseHold || frozen };
1556
1621
  }
1557
1622
  const childFailure = state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
1558
1623
  ? lastPodChildFailure(logs) : null;
1559
1624
  let noResultDiagnostic: string | null = null;
1560
- if (state === 'failed' && containerExitCode !== null && containerExitCode !== 0 && !disposition && !oomKilled && failedReason !== 'DeadlineExceeded' && failedReason !== 'BackoffLimitExceeded') {
1625
+ // POD-NORESULT: the Job runs with backoffLimit 0, so any non-zero child exit is «BackoffLimitExceeded» — not a reason to skip the ledger.
1626
+ if (state === 'failed' && containerExitCode !== null && containerExitCode !== 0 && !disposition && !oomKilled && failedReason !== 'DeadlineExceeded') {
1561
1627
  try { noResultDiagnostic = podNoResultDiagnostic(readFileSync(runLedgerPath(liveChildRunId, ledgerDir), 'utf8')); }
1562
1628
  catch { /* ledger not returned: do not invent a stage */ }
1563
1629
  noResultDiagnostic ??= 'child terminal result missing; last ledger stage=unknown; round=unknown; mustFix=unknown';
@@ -1568,7 +1634,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1568
1634
  }
1569
1635
  debug.log('self-implement.pod', 'job-finished', { job: name, ...(member ? { context: member.context } : {}), state, containerReason, stage: disposition?.stage ?? null, prUrl: disposition?.prUrl ?? null, childRunId: liveChildRunId, ledgerCompleteness,
1570
1636
  ...(state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
1571
- ? { ...(childFailure ? { childStage: childFailure.stage } : {}), childError: childFailure?.error ?? 'no-result-line' } : {}) },
1637
+ ? { ...(childFailure ? { childStage: childFailure.stage } : disposition?.stage ? { childStage: disposition.stage } : {}),
1638
+ // POD-NORESULT: a parsed result row without error text is not a missing result line (UX 10-07 01:01: 28 of 28 exit-2 Jobs printed one).
1639
+ childError: childFailure?.error ?? (disposition?.stage ? 'result-without-error' : 'no-result-line') } : {}) },
1572
1640
  state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded' ? { compact: { stringMax: 500 } } : undefined);
1573
1641
  if (oomKilled && reattach && !oomRetried && (!reattachedMemoryLimit || memoryGi(reattachedMemoryLimit) === null)) {
1574
1642
  return { exitCode: 1, output: '', error: { code: 'pod-memory-unavailable', message: `Job ${name} 실제 메모리 한도를 확인할 수 없다` } };
@@ -1639,7 +1707,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1639
1707
  : disposition;
1640
1708
  const podExitCode = state === 'complete' && disposition?.stage !== 'host-regate-failed' && disposition?.ok !== false ? 0 : 1;
1641
1709
  // SCHED-CONTRACT: a failed child must say why outside the Pod, not «no error diagnostic».
1642
- const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds }) : undefined;
1710
+ const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds, result: disposition ? { stage: disposition.stage, prUrl: disposition.prUrl ?? null, prNumber: disposition.prNumber ?? null } : null }) : undefined;
1643
1711
  if (podReason) debug.log('self-implement.pod', 'failure-reason', { job: name, reason: podReason });
1644
1712
  return {
1645
1713
  exitCode: podExitCode,
@@ -1651,7 +1719,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1651
1719
  ? { error: { code: 'pod-gh-login-failed', message: `Job ${name} GitHub 앱 로그인 실패 (container=${containerReason ?? 'unknown'}/7, attempt=${ghLoginRetried ? 2 : 1}) — 새 토큰으로 ${ghLoginRetried ? '1회 재시도했으나 다시 실패' : '재시도 불가'}` } }
1652
1720
  : oomKilled
1653
1721
  ? { error: { code: 'pod-oom-killed', message: `Job ${name} failed (OOMKilled/${containerExitCode ?? 'unknown'}, memoryLimit=${memoryLimit})${oomRetried || (memoryGi(reattachedMemoryLimit ?? memoryLimit) ?? 0) >= (memoryGi(podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit) ?? Infinity) ? ` — OOM · ${oomRetried ? retryTier : 'high'} 에서도` : ''}\n${formatLastMemSample(samples.at(-1))}` } }
1654
- : { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
1722
+ : { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : disposition?.stage ? ` — childStage=${disposition.stage} · reason=${podReason}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
1655
1723
  : podReason && !childFailure?.error ? { error: { code: 'pod-child-failed', message: podReason } } : {}),
1656
1724
  ...(finishedDisposition ? { disposition: finishedDisposition } : {}),
1657
1725
  };
@@ -2,7 +2,7 @@ import { dashboardLoops } from './domains/dashboard-data.js';
2
2
  import { readSchedulesInventory } from './nexus/api/schedules-read.js';
3
3
  import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH } from './loops/status-rows.js';
4
4
 
5
- interface LoopsSources {
5
+ export interface LoopsSources {
6
6
  schedules: () => unknown;
7
7
  loops: () => unknown;
8
8
  now: () => number;
package/src/telegram.ts CHANGED
@@ -82,6 +82,7 @@ import { attachTelegramProjectButtons } from './telegram-project-command.js';
82
82
  import { attachTelegramFabricPlan } from './telegram-fabric-plan.js';
83
83
  import { contextNow as readContextNow, type ContextNowDeps } from './context-bus/context-now.js';
84
84
  import { renderTelegramNow } from './context-bus/context-now-surfaces.js';
85
+ import { createContextFirstGate, renderContextFirst } from './context-bus/context-first.js';
85
86
 
86
87
  export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
87
88
 
@@ -269,9 +270,6 @@ const EMPTY_ALLOWLIST_NOTICE_GAP_MS = 10 * 60 * 1000;
269
270
  // Tuned above a normal chat answer but below a real delegation (which
270
271
  // pays a multi-second session load before any work).
271
272
  const NOTIFY_AS_NEW_MSG_THRESHOLD_MS = 20_000;
272
- /** 같은 대화에서 이 간격 안이면 context-now 요약을 다시 보내지 않는다. */
273
- const CONTEXT_FIRST_GAP_MS = 6 * 60 * 60 * 1000;
274
- const CONTEXT_UNREADABLE = '맥락 못 읽음';
275
273
 
276
274
  interface ApiResponse<T> {
277
275
  ok: boolean;
@@ -365,10 +363,7 @@ export class TelegramBot {
365
363
  private readonly handleFabricPlan: (ctx: TgIncoming) => Promise<boolean>;
366
364
  private readonly readContextNow: typeof readContextNow;
367
365
  private readonly contextNowDeps?: ContextNowDeps;
368
- /** 대화 키(chat+thread) → 그 대화의 마지막 사용자 발화 시각(요약 여부와 무관하게 매 발화마다 갱신). */
369
- private readonly lastUtteranceAt = new Map<string, number>();
370
- /** 첫 말이거나 6시간 이상 쉰 뒤라 요약이 «밀려 있는» 대화 키 — 다음 일반 답이 한 번 소비한다. */
371
- private readonly contextFirstPending = new Set<string>();
366
+ private readonly contextFirstGate = createContextFirstGate({ now: () => this.nowImpl() });
372
367
  private seatAskTimer: ReturnType<typeof setInterval> | null = null;
373
368
  /** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
374
369
  private readonly fieldGroups = new Map<string, {
@@ -432,27 +427,17 @@ export class TelegramBot {
432
427
 
433
428
  /** 허용된 모든 발화(명령 포함)마다 부른다 — 쉼은 마지막 발화부터 재고, 요약 전송과는 따로 기록한다. */
434
429
  private noteUtterance(ctx: TgIncoming): void {
435
- const key = `${ctx.chatId}:${ctx.threadId ?? ''}`;
436
- const now = this.nowImpl();
437
- const previous = this.lastUtteranceAt.get(key);
438
- this.lastUtteranceAt.set(key, now);
439
- if (previous === undefined || now - previous >= CONTEXT_FIRST_GAP_MS) this.contextFirstPending.add(key);
430
+ this.contextFirstGate.note(`${ctx.chatId}:${ctx.threadId ?? ''}`);
440
431
  }
441
432
 
442
433
  /** 처음이거나 6시간 이상 쉰 대화의 첫 일반 답 앞에 context-now 요약 한 메시지를 보낸다. 실패해도 본 답은 막지 않는다. */
443
434
  private async maybeSendContextFirst(ctx: TgIncoming): Promise<void> {
444
435
  const key = `${ctx.chatId}:${ctx.threadId ?? ''}`;
445
- if (!this.contextFirstPending.delete(key)) return;
436
+ if (!this.contextFirstGate.take(key)) return;
446
437
  const cfg = this.slashContext?.userConfig.telegram;
447
438
  if (cfg?.contextFirst === false) return;
448
- let text = CONTEXT_UNREADABLE;
449
- try {
450
- text = renderTelegramNow(this.readContextNow({}, this.contextNowDeps));
451
- } catch {
452
- text = CONTEXT_UNREADABLE;
453
- }
454
439
  try {
455
- await this.sendMessage(ctx.chatId, text, { replyTo: ctx.messageId, threadId: ctx.threadId });
440
+ await this.sendMessage(ctx.chatId, renderContextFirst(this.readContextNow, this.contextNowDeps, renderTelegramNow), { replyTo: ctx.messageId, threadId: ctx.threadId });
456
441
  } catch (err) {
457
442
  this.log(`telegram context-first failed: ${err instanceof Error ? err.message : String(err)}`);
458
443
  }