elanous 0.2.15 → 0.2.17

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/chunks/{1307-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
  55. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  56. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +15 -15
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +15 -15
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +15 -15
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +15 -15
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +15 -15
  67. package/apps/pwa/out/ceo/index.html +1 -1
  68. package/apps/pwa/out/ceo/index.txt +15 -15
  69. package/apps/pwa/out/chat/index.html +1 -1
  70. package/apps/pwa/out/chat/index.txt +15 -15
  71. package/apps/pwa/out/consult/index.html +1 -1
  72. package/apps/pwa/out/consult/index.txt +15 -15
  73. package/apps/pwa/out/control/index.html +1 -1
  74. package/apps/pwa/out/control/index.txt +15 -15
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +15 -15
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +15 -15
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +15 -15
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +15 -15
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +15 -15
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +15 -15
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +15 -15
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +15 -15
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +15 -15
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +15 -15
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +15 -15
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +15 -15
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +15 -15
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +15 -15
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +15 -15
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +16 -16
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +15 -15
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +15 -15
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +15 -15
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +15 -15
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +15 -15
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +15 -15
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +15 -15
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +15 -15
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +15 -15
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +15 -15
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +15 -15
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +15 -15
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +15 -15
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +15 -15
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +15 -15
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +15 -15
  146. package/graphs/doc-gen/doc-gen.yaml +24 -0
  147. package/graphs/doc-gen/recipes.yaml +3 -0
  148. package/graphs/intake/intake-daily.yaml +9 -2
  149. package/graphs/intake/news-sources.yaml +4 -0
  150. package/graphs/intake/recipes.yaml +6 -1
  151. package/graphs/orchestrator/orchestrator.yaml +7 -0
  152. package/graphs/orchestrator/recipes.yaml +3 -0
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/__fixtures__/intake-news.xml +7 -0
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +34 -15
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/goals/goal-census.ts +343 -0
  163. package/scripts/intake-news.ts +169 -0
  164. package/scripts/measure/author-section-contribution.ts +84 -0
  165. package/scripts/release-loop/cut-branch.ts +43 -13
  166. package/scripts/release-loop/docs-land-node.ts +69 -10
  167. package/scripts/release-loop/gate-node.ts +3 -1
  168. package/scripts/release-loop/publish-node.ts +2 -2
  169. package/scripts/release-loop/release-docs.ts +134 -0
  170. package/scripts/release-loop/vault-note-node.ts +19 -2
  171. package/scripts/release-loop/version-node.ts +7 -4
  172. package/scripts/release-story/draft.ts +42 -14
  173. package/scripts/rhythm/monthly.ts +97 -0
  174. package/src/acp/client.ts +17 -2
  175. package/src/agent-mission/driver.ts +104 -11
  176. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  177. package/src/agent-mission/supervisor-metrics.ts +218 -0
  178. package/src/autopilot/track-agent-shadow.ts +2 -1
  179. package/src/autopilot/track-agent.ts +5 -3
  180. package/src/briefing/brief-items.ts +191 -36
  181. package/src/budget/usage-outcomes.ts +204 -0
  182. package/src/cli/autopilot-cli.ts +66 -0
  183. package/src/cli/brief-cli.ts +73 -0
  184. package/src/cli/decisions-cli.ts +4 -1
  185. package/src/cli/intake-cli.ts +1 -1
  186. package/src/cli/know-cli.ts +20 -0
  187. package/src/cli/log-event-names.ts +1 -0
  188. package/src/cli/logs-cli.ts +43 -1
  189. package/src/cli/pr-cli.ts +92 -13
  190. package/src/cli/provider-cli.ts +14 -0
  191. package/src/cli/release-cli.ts +11 -8
  192. package/src/cli/self-update-canary.ts +46 -0
  193. package/src/cli/self-update.ts +399 -1
  194. package/src/cli/usage-cli.ts +26 -0
  195. package/src/context-card/index.ts +45 -0
  196. package/src/context-card/task.ts +76 -0
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  198. package/src/decisions/decision-ledger.ts +10 -1
  199. package/src/discord-self-message.ts +9 -0
  200. package/src/flow-loop/split.ts +24 -0
  201. package/src/harness/harness-cli-command.ts +178 -12
  202. package/src/harness/harness-pod-dispatch.ts +36 -55
  203. package/src/harness/harness-queue.ts +86 -7
  204. package/src/harness/harness-seams.ts +13 -3
  205. package/src/harness/helper-repair.ts +139 -8
  206. package/src/harness/helper-scan.ts +13 -2
  207. package/src/harness/nested-elanous-policy.ts +133 -0
  208. package/src/hq/fence-wrapper.ts +123 -0
  209. package/src/hq/hq.ts +16 -2
  210. package/src/hq/lease.ts +5 -3
  211. package/src/hq/transfer.ts +185 -0
  212. package/src/index.ts +168 -13
  213. package/src/intake-plane/digest.ts +85 -28
  214. package/src/knowledge/know-find.ts +105 -0
  215. package/src/llm.ts +75 -19
  216. package/src/loops/orchestrator/finish-rate.ts +14 -3
  217. package/src/loops/orchestrator/tick.ts +96 -25
  218. package/src/maturity/bot-slash-catalog.ts +11 -2
  219. package/src/maturity/feature-maturity.ts +2 -2
  220. package/src/maturity/tui-slash-availability.ts +9 -0
  221. package/src/nexus/api/grid.ts +61 -0
  222. package/src/nexus/api/http-server.ts +19 -1
  223. package/src/nexus/api/task-cards-api.ts +19 -0
  224. package/src/oauth/codex.ts +82 -2
  225. package/src/oauth/overload-failover-launch.ts +75 -0
  226. package/src/oauth/overload-failover.ts +137 -0
  227. package/src/release-loop/checklist.ts +10 -5
  228. package/src/release-loop/feature-store.ts +10 -9
  229. package/src/release-loop/landing-freeze.ts +18 -10
  230. package/src/release-loop/placement.ts +17 -8
  231. package/src/seat-dispatch/seat-ask.ts +33 -6
  232. package/src/seat-loop/seat-loop.ts +172 -21
  233. package/src/self-dev/ask-launch-flow.ts +48 -8
  234. package/src/self-dev/authored-goal-trace.ts +64 -0
  235. package/src/self-dev/dev-cli.ts +19 -1
  236. package/src/self-dev/dev-pipeline.ts +6 -5
  237. package/src/self-dev/entrance-registry.ts +10 -6
  238. package/src/self-dev/old-door.ts +73 -0
  239. package/src/self-dev/run-store.ts +5 -1
  240. package/src/self-dev/run-supervisor.ts +7 -1
  241. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  242. package/src/self-implement/app-api-budget.ts +166 -0
  243. package/src/self-implement/budget-gate.ts +7 -5
  244. package/src/self-implement/follow-up-goals.ts +289 -0
  245. package/src/self-implement/frozen-merges.ts +25 -20
  246. package/src/self-implement/goal-author-clarification.ts +81 -0
  247. package/src/self-implement/goal-author-cli.ts +33 -5
  248. package/src/self-implement/goal-author.ts +107 -25
  249. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  250. package/src/self-implement/goal-document-write.ts +28 -0
  251. package/src/self-implement/heal-intake.ts +4 -0
  252. package/src/self-implement/heal-triage.ts +44 -0
  253. package/src/self-implement/host-regate.ts +3 -1
  254. package/src/self-implement/orchestrator.ts +157 -8
  255. package/src/self-implement/run-ledger.ts +4 -0
  256. package/src/self-implement/seams.ts +44 -15
  257. package/src/session-runtime/index.ts +2 -0
  258. package/src/session-runtime/retry-api.ts +87 -0
  259. package/src/session-runtime/retry-policy.ts +107 -6
  260. package/src/steward/working-backwards.ts +16 -1
  261. package/src/task-agent/actions.ts +118 -0
  262. package/src/task-agent/judge.ts +96 -0
  263. package/src/task-cards/card-cli.ts +25 -0
  264. package/src/task-cards/card-store.ts +9 -4
  265. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  266. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  267. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  268. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  269. package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
  270. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  271. package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
  272. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  273. package/src/telegram-commands.ts +6 -1
  274. package/src/telegram-loops-command.ts +21 -14
  275. package/src/telegram.ts +47 -0
  276. package/src/user-config.ts +59 -4
  277. package/src/version/packed-revision.json +1 -1
  278. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  305. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  306. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  307. package/src/launch-head/seat-goal-queue.ts +0 -198
  308. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  309. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -38,9 +38,12 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
38
38
  /** 자식 컨테이너 «요청» — 🩸 2026-09-27: limits 만 두면 k8s 가 requests=limits(cpu 4)로 잡아, 32코어 노드에
39
39
  * «자리 요청»이 88% 차서 잡이 Pending 인데 실사용은 14%였다(풀 25 자리 중 ~8 만 떴다). 상한(limits)은 그대로 두고
40
40
  * 예약만 실사용에 맞춘다. */
41
- export const POD_CHILD_REQUESTS = { cpu: '1', memory: '4Gi' } as const;
41
+ // POD-DIET (10-06 03:47~05:18 · node-b goal Pods · 89 one-minute samples · 42 Pods): per-Pod observed max median 4.6Gi ·
42
+ // p75 6.2Gi · p90 11.7Gi — 23/42 went above the old 4Gi request, so the scheduler over-packed. 6Gi ≈ p75; limits stay.
43
+ // Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
44
+ export const POD_CHILD_REQUESTS = { cpu: '1', memory: '6Gi' } as const;
42
45
  import type { PodPoolMember, PodPoolScheduler } from './pod-pool.js';
43
- import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER } from './pod-lease.js';
46
+ import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
44
47
  import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
45
48
  import { spawnSync } from 'node:child_process';
46
49
  import { createHash } from 'node:crypto';
@@ -62,7 +65,8 @@ import { goalTypeOf, type GoalType } from '../../../scripts/measure-pod-memory-b
62
65
  import { declaredGoalType, type GoalType as DeclaredGoalType } from '../../self-implement/goal-author.js';
63
66
  import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
64
67
  import { getUserConfig } from '../../user-config.js';
65
- import { extractPodFailureReason } from './pod-failure-reason.js';
68
+ import { extractPodFailureReason, podTerminalRow } from './pod-failure-reason.js';
69
+ import { OLD_DOOR_STAMP_ENV } from '../../self-dev/old-door.js';
66
70
 
67
71
  export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
68
72
 
@@ -70,7 +74,9 @@ export type Kubectl = (args: readonly string[], input?: string) => { status: num
70
74
  // still the credential source for gh and setup-git's helper. Install it privately
71
75
  // on that failure; rename keeps concurrent readers from seeing a partial token.
72
76
  const APP_GH_CONFIG_PATH = '$HOME/.config/gh';
73
- const APP_GH_AUTH_SCRIPT = 'const fs=require("node:fs"),path=require("node:path"),crypto=require("node:crypto");const token=fs.readFileSync(0,"utf8").trim();if(!/^[A-Za-z0-9_]+$/.test(token))process.exit(1);const dir=process.env.GH_CONFIG_DIR||path.join(process.env.HOME,".config/gh");fs.mkdirSync(dir,{recursive:true,mode:0o700});const dest=path.join(dir,"hosts.yml"),temp=path.join(dir,".hosts.yml."+crypto.randomUUID());try{fs.writeFileSync(temp,"github.com:\\n oauth_token: "+token+"\\n git_protocol: https\\n",{mode:0o600,flag:"wx"});fs.renameSync(temp,dest);fs.chmodSync(dest,0o600)}finally{try{fs.unlinkSync(temp)}catch{}}';
77
+ // PODCRED2 (10-05) — GitHub App installation tokens now carry `.` and `-` (390 chars on node-b); the old
78
+ // `[A-Za-z0-9_]+` check rejected every real token, so any transient `gh auth login` failure became exit 7.
79
+ export const APP_GH_AUTH_SCRIPT = 'const fs=require("node:fs"),path=require("node:path"),crypto=require("node:crypto");const token=fs.readFileSync(0,"utf8").replace(/\\r?\\n$/,"");if(!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(token))process.exit(1);const dir=process.env.GH_CONFIG_DIR||path.join(process.env.HOME,".config/gh");fs.mkdirSync(dir,{recursive:true,mode:0o700});const dest=path.join(dir,"hosts.yml"),temp=path.join(dir,".hosts.yml."+crypto.randomUUID());try{fs.writeFileSync(temp,"github.com:\\n oauth_token: "+token+"\\n git_protocol: https\\n",{mode:0o600,flag:"wx"});fs.renameSync(temp,dest);fs.chmodSync(dest,0o600)}finally{try{fs.unlinkSync(temp)}catch{}}';
74
80
  const APP_GH_AUTH_COMMAND = `bun -e '${APP_GH_AUTH_SCRIPT}'`;
75
81
  const APP_GH_EXEC = `unset GH_TOKEN GITHUB_TOKEN; GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}" exec "$@"`;
76
82
 
@@ -113,6 +119,8 @@ export interface PodSpawnOptions {
113
119
  /** Job 수명 상한(초). */
114
120
  deadlineSeconds?: number;
115
121
  pollMs?: number;
122
+ /** LAUNCH-STALL: ms a launch may wait in one stage before `launch-stalled` (test seam; default harness.launchStallMinutes · 15 min). */
123
+ launchStallMs?: number;
116
124
  kubectl?: Kubectl;
117
125
  /** Host-side regate (injected for Pod tests). */
118
126
  hostRegate?: (input: { prNumber: number; headCommit: string; repoRoot: string; goalFile?: string }) => Promise<HostRegateResult>;
@@ -137,6 +145,10 @@ export interface PodSpawnOptions {
137
145
  now?: () => number;
138
146
  sleep?: (ms: number) => Promise<void>;
139
147
  credentials?: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
148
+ /** POD-TOKEN-PREREFRESH 시험 심 — «3시간 안 만료» 계정의 호스트 선갱신(기본 refreshCodexAccountHome). */
149
+ codexPrerefresh?: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
150
+ /** 시험 심 — 이 호스트가 본부 임대 보유자인가(기본 HQ 펜스 · kubectl 주입 시험이면 false). */
151
+ hqLeaseHolder?: () => boolean | Promise<boolean>;
140
152
  grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
141
153
  /** 호스트 키 캐시(`~/.cache/<소문자 이름>`)에서 키를 읽는다(시험 주입) — env 에 없을 때. */
142
154
  readKeyCache?: (name: string) => string | undefined;
@@ -167,6 +179,58 @@ export function podJobName(spaceId: string): string {
167
179
  return `si-${slug || 'job'}-${hash}`.replace(/-+/g, '-').slice(0, 58);
168
180
  }
169
181
 
182
+ /** «3시간 안 만료» — 선갱신 대상이라는 표지(다른 자격 오류와 구분한다). */
183
+ export class PodCodexExpiringError extends Error {
184
+ constructor(readonly account: string, readonly hoursLeft: number) {
185
+ super(`openai-codex:${account} access token 이 3시간 안에 만료 — 호스트에서 먼저 갱신(컨테이너는 갱신 못 한다)`);
186
+ }
187
+ }
188
+
189
+ export type PodCredentialPrerefreshOutcome = 'refreshed' | 'still-expiring' | 'refresh-failed' | 'no-lease';
190
+
191
+ /** POD-TOKEN-PREREFRESH (10-06) — «3시간 안 만료» 계정은 거부만 하지 말고 선갱신을 한 번 시도한다.
192
+ * 🩸 05:3x team · 09:47 third: 거부만 하고 다음 계정으로 안 넘어가 그 계정으로 뽑힌 Pod 런이 전부 pod-error.
193
+ * ⛔ 갱신은 본부(HQ) 임대 보유자 한 곳에서만 — refresh 토큰은 갱신마다 회전해 두 곳이 갱신하면 로그아웃된다.
194
+ * null = 이 계정은 후보에서 뺀다(호출자가 다음 계정으로). 만료 아닌 다른 자격 오류는 그대로 던진다. */
195
+ export async function podCodexCredentialWithPrerefresh(account: string, deps: {
196
+ credentials: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
197
+ refresh: (account: string) => Promise<{ ok: true; beforeH: number | null; afterH: number | null } | { ok: false; kind: string; message: string }>;
198
+ isLeaseHolder: () => boolean | Promise<boolean>;
199
+ log?: (category: string, event: string, data: Record<string, unknown>) => void;
200
+ }): Promise<{ elanousAuth: string; codexAuth: string; ghToken: string } | null> {
201
+ const log = deps.log ?? ((category, event, data) => debug.log(category, event, data));
202
+ try { return deps.credentials(account); }
203
+ catch (error) {
204
+ if (!(error instanceof PodCodexExpiringError)) throw error;
205
+ const beforeH = error.hoursLeft;
206
+ const observe = (outcome: PodCredentialPrerefreshOutcome, afterH: number | null, reason?: string) => {
207
+ try { log('pod.credential-prerefresh', outcome, { account, beforeH, afterH, outcome, ...(reason ? { reason } : {}) }); } catch { /* observation is fail-soft */ }
208
+ };
209
+ let holder = false;
210
+ try { holder = await deps.isLeaseHolder(); } catch { holder = false; }
211
+ 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);
218
+ observe('refreshed', refreshed.afterH);
219
+ return credential;
220
+ } catch (again) {
221
+ if (!(again instanceof PodCodexExpiringError)) throw again;
222
+ observe('still-expiring', again.hoursLeft);
223
+ return null;
224
+ }
225
+ }
226
+ }
227
+
228
+ /** 기본 임대 판정 — 기존 HQ 펜스(`ledger-cli` 역할 · fail-open 없음)를 그대로 쓴다. */
229
+ async function defaultHqLeaseHolder(): Promise<boolean> {
230
+ const { hqFenceDecision } = await import('../../hq/hq.js');
231
+ return hqFenceDecision('ledger-cli').run;
232
+ }
233
+
170
234
  /** 호스트 계정 → refresh 없는 사본(elanous 저장소 ⊕ codex auth.json) ⊕ gh 토큰. */
171
235
  export function hostCredentials(account: string, storePath: string = authStorePath(), ghToken: () => string = defaultGhToken): { elanousAuth: string; codexAuth: string; ghToken: string } {
172
236
  // 경로는 해석기로(격리 게이트 · 2026-09-25) — 손으로 `~/.elanous/auth.json` 을 조립하면 test↔prod 격리가 새는 자리가 된다.
@@ -179,7 +243,7 @@ export function hostCredentials(account: string, storePath: string = authStorePa
179
243
  const codex = JSON.parse(readFileSync(join(codexHome, 'auth.json'), 'utf8')) as { tokens: Record<string, unknown> };
180
244
  const access = String(codex.tokens.access_token ?? '');
181
245
  const exp = Number(JSON.parse(Buffer.from(access.split('.')[1] ?? '', 'base64url').toString('utf8') || '{}').exp ?? 0);
182
- if (exp * 1000 - Date.now() < 3 * 3600_000) throw new Error(`openai-codex:${account} access token 이 3시간 안에 만료 — 호스트에서 먼저 갱신(컨테이너는 갱신 못 한다)`);
246
+ if (exp * 1000 - Date.now() < 3 * 3600_000) throw new PodCodexExpiringError(account, Math.round(((exp * 1000 - Date.now()) / 3_600_000) * 10) / 10);
183
247
  const elanousAuth = JSON.stringify({
184
248
  version: store.version ?? 1,
185
249
  // ⭐ P4(2026-09-26): elanous 사본도 «방금 검사한» codex 홈의 토큰으로 싣는다 — 두 저장소는 따로 갱신된다.
@@ -594,14 +658,15 @@ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'chi
594
658
  return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
595
659
  }
596
660
 
597
- export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean }): Record<string, unknown> {
661
+ export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; authorGrade?: 'full' | 'lite'; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean }): Record<string, unknown> {
598
662
  const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
599
663
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
600
- const askBaseIndex = o.goalDoc ? o.args.indexOf('--base') : -1;
664
+ const delegated = Boolean(o.goalDoc) || o.authorSentence === true;
665
+ const askBaseIndex = delegated ? o.args.indexOf('--base') : -1;
601
666
  const quotedAsk = (askBaseIndex >= 0 && o.args[askBaseIndex + 1]
602
667
  ? ` '--base' '${o.args[askBaseIndex + 1]!.replace(/'/g, `'\\''`)}'`
603
- : '') + (o.goalDoc && o.args.includes('--merge-by-host') ? ' --merge-by-host' : o.goalDoc && o.args.includes('--open-pr') && !o.args.includes('--auto-merge') ? ' --no-auto-merge' : '')
604
- + (o.goalDoc && o.args.includes('--no-supervise') ? ' --no-supervise' : '');
668
+ : '') + (delegated && o.args.includes('--merge-by-host') ? ' --merge-by-host' : delegated && o.args.includes('--open-pr') && !o.args.includes('--auto-merge') ? ' --no-auto-merge' : '')
669
+ + (delegated && o.args.includes('--no-supervise') ? ' --no-supervise' : '');
605
670
  const script = [
606
671
  'set -u',
607
672
  o.grokCredential === 'subscription'
@@ -609,7 +674,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
609
674
  : o.grokCredential === 'api_key'
610
675
  ? 'mkdir -p ~/.grok && install -m 600 /creds/grok-api-key ~/.grok/api-key && export XAI_API_KEY="$(cat ~/.grok/api-key)" && export ELANOUS_LLM_PROVIDER=grok'
611
676
  : o.codexAccounts ? podCodexAccountScript(o.codexAccounts) : `mkdir -p ~/.elanous ~/.codex && cp /creds/elanous-auth.json ~/.elanous/auth.json && cp /creds/codex-auth.json ~/.codex/auth.json && chmod 600 ~/.elanous/auth.json ~/.codex/auth.json\n${podQuotaPolicyExport()}`,
612
- o.appCredential ? `unset GH_TOKEN GITHUB_TOKEN; export GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}"; mkdir -p "$GH_CONFIG_DIR" && chmod 700 "$GH_CONFIG_DIR" && { gh auth login --with-token < /creds/gh-token >/dev/null 2>&1 || ${APP_GH_AUTH_COMMAND} < /creds/gh-token; } && chmod 600 "$GH_CONFIG_DIR/hosts.yml" && chmod 700 "$GH_CONFIG_DIR" || exit 7` : 'export GH_TOKEN="$(cat /creds/gh-token)"',
677
+ o.appCredential ? `unset GH_TOKEN GITHUB_TOKEN; export GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}"; mkdir -p "$GH_CONFIG_DIR" && chmod 700 "$GH_CONFIG_DIR" && { (umask 077; gh auth login --with-token < /creds/gh-token >"$HOME/.gh-login.log" 2>&1) || ${APP_GH_AUTH_COMMAND} < /creds/gh-token; } && chmod 600 "$GH_CONFIG_DIR/hosts.yml" && chmod 700 "$GH_CONFIG_DIR" || { echo "[pod] gh-login-failed: $(head -c 300 "$HOME/.gh-login.log" 2>/dev/null | tr '\\n' ' ' | sed -E 's/(gh[a-z]_|github_pat_)[A-Za-z0-9_.-]+/\\1***/g')"; exit 7; }` : 'export GH_TOKEN="$(cat /creds/gh-token)"',
613
678
  // 🔑 스킬 키(.env) — 이미지엔 없다. 이 런의 Secret 에서 각 스킬 폴더로 0600 복사(값은 로그에 안 나온다).
614
679
  ...(o.skillEnvs?.length ? [`for n in ${o.skillEnvs.join(' ')}; do [ -d ~/.claude/skills/$n ] && install -m 600 /creds/skillenv-$n ~/.claude/skills/$n/.env; done; echo "[pod] skill env: ${o.skillEnvs.join(',')}"`] : []),
615
680
  'git config --global user.name "elanous pod child" && git config --global user.email "noreply@anthropic.com" && gh auth setup-git',
@@ -622,9 +687,13 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
622
687
  `(while :; do { mem=$(if [ -r /sys/fs/cgroup/memory.current ]; then cat /sys/fs/cgroup/memory.current; elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then cat /sys/fs/cgroup/memory/memory.usage_in_bytes; else printf -- -; fi); top=$(ps -eo rss=,comm=,args= --sort=-rss | head -5 | awk 'function encode(s) { gsub(/%/, "%25", s); gsub(/:/, "%3A", s); gsub(/ /, "%20", s); gsub(/\\t/, "%09", s); return s } { rss=$1; name=$2; sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]*/, ""); n=split($0, a, /[[:space:]]+/); cmd="<redacted>"; if (name=="sleep" && a[2]=="30") cmd="sleep 30" (n>2 ? " <redacted>" : ""); else if (name=="bun") { cmd="bun <redacted>"; if (a[2]=="test") cmd="bun test <redacted>"; else if (a[2]=="run") cmd="bun run <redacted>"; else if ((a[2]=="x" || a[2]=="exec") && a[3]=="tsc") cmd="bun x tsc <redacted>" } else if (name=="tsc") cmd="tsc <redacted>"; else if (name=="elanous") { cmd="elanous <redacted>"; if (a[2]=="self") cmd="elanous self <redacted>"; else if (a[2]=="harness") cmd="elanous harness <redacted>" } else if (name=="node") cmd="node <redacted>"; if (name=="sleep" && cmd=="<redacted>") cmd="sleep <redacted>"; else if (cmd=="<redacted>" && name!="bash" && name!="sh") name="other"; printf " %s:%s:%s", rss, encode(name), encode(substr(cmd,1,120)) }'); printf "ELANOUS_MEM %s %s%s\\n" "$(date +%s)" "$mem" "$top"; } || true; sleep 15 || break; done) & mem_sampler_pid=$!`,
623
688
  ...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
624
689
  // 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
625
- goalPath
690
+ // ⛔ `--author-grade` 는 Pod 안 `elanous`(이미지에 깔린 판)가 모를 수 있어 넘기지 않는다 — 10-06 «unknown option» 즉사(#24445 되돌림).
691
+ // lite 는 지금 로컬 저작 전용이다. Pod 저작은 설정·기본(full)을 따른다.
692
+ 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{}'`
694
+ : goalPath
626
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{}'`
627
- : `elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
696
+ : `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
628
697
  'cat /tmp/si.out',
629
698
  '[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
630
699
  // A long run's log export can pass the 5MB artifact limit; skipping it whole left no window into why a
@@ -856,31 +925,72 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
856
925
  let releaseAdmission: ((() => void) & { applied?: (job: string, context: string, namespace: string) => void; observed?: () => void }) | undefined;
857
926
  try {
858
927
  // Every attempt, including an OOM retry, must acquire a pool slot before applying a Job.
928
+ const stallMs = options.launchStallMs ?? (getUserConfig().harness?.launchStallMinutes ?? 15) * 60_000;
929
+ // LAUNCH-STALL (10-06): a stage that waits past the cap says so once — today Pods sat 22 min to 5 h with no event.
930
+ const stalledStages = new Set<string>();
931
+ // pool-slot reason = what THIS launch saw on its last empty tryAcquire; admission reason = the pool-wide lease measurement (one pool, one reading).
932
+ const stageReason: { 'pool-slot'?: string | null } = {};
933
+ const unfitContexts = new Set<string>();
934
+ const watchStall = (stage: 'admission' | 'pool-slot'): (() => void) => {
935
+ const started = Date.now();
936
+ const timer = setTimeout(() => {
937
+ if (stalledStages.has(stage)) return;
938
+ stalledStages.add(stage);
939
+ const pool = options.pool as { waitReason?: () => string | null } | undefined;
940
+ debug.log('harness.launch', 'launch-stalled', { spaceId: input.spaceId, runId: childRunId, stage, waitedSec: Math.round((Date.now() - started) / 1000),
941
+ reason: (stage === 'pool-slot' ? stageReason['pool-slot'] : (typeof pool?.waitReason === 'function' ? pool.waitReason() : null)) ?? 'not-measured-yet' }, { level: 'warn' });
942
+ }, stallMs);
943
+ (timer as { unref?: () => void }).unref?.();
944
+ return () => clearTimeout(timer);
945
+ };
946
+ // Members that can never hold this Job's memory limit are skipped with a reason; all unfit → fail fast instead of waiting forever.
947
+ const unfitFailure = async (): Promise<SelfImplementJobDone | null> => {
948
+ const pool = options.pool as { unfitMembers?: (b: number) => Promise<Array<{ context: string; allocatableMemoryBytes: number }>>; members?: readonly PodPoolMember[] } | undefined;
949
+ if (!pool || typeof pool.unfitMembers !== 'function' || !Array.isArray(pool.members)) return null;
950
+ const limitBytes = memoryQuantityBytes(memoryLimit);
951
+ if (limitBytes === null) return null;
952
+ const unfit = await pool.unfitMembers(limitBytes);
953
+ for (const u of unfit) {
954
+ debug.log('harness.launch', 'launch-member-skipped', { spaceId: input.spaceId, runId: childRunId, member: u.context, reason: 'memory-never-fits', allocatableMemoryBytes: u.allocatableMemoryBytes, memoryLimit }, { level: 'warn' });
955
+ }
956
+ unfitContexts.clear();
957
+ for (const u of unfit) unfitContexts.add(u.context);
958
+ if (!unfit.length || !pool.members.every((m) => unfitContexts.has(m.context))) return null;
959
+ const detail = unfit.map((u) => `${u.context} ${Math.round(u.allocatableMemoryBytes / 2 ** 30 * 10) / 10}GiB`).join(' · ');
960
+ return { exitCode: 1, output: '', error: { code: 'pod-pool-unfit', message: `모든 풀 멤버의 할당 가능 메모리가 이 Job 의 한도 ${memoryLimit} 보다 작다 — 영원히 배치될 수 없다(${detail}) · --pod-memory 를 낮추거나 다른 풀로` } };
961
+ };
859
962
  const acquireSlot = async (): Promise<PodPoolMember | null> => {
860
963
  if (!options.pool) return null;
861
- for (;;) {
862
- if (input.signal?.aborted) return null;
863
- const acquired = await options.pool.tryAcquire();
864
- if (acquired) {
865
- debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
866
- return acquired;
964
+ const stop = watchStall('pool-slot');
965
+ try {
966
+ for (;;) {
967
+ if (input.signal?.aborted) return null;
968
+ // The reason comes back inside this call, so a concurrent launch cannot overwrite it.
969
+ const acquired = await options.pool.tryAcquire(unfitContexts, (reason) => { stageReason['pool-slot'] = reason; });
970
+ if (acquired) {
971
+ debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
972
+ return acquired;
973
+ }
974
+ await sleep(options.pollMs ?? 15_000);
867
975
  }
868
- await sleep(options.pollMs ?? 15_000);
869
- }
976
+ } finally { stop(); }
870
977
  };
871
978
  const acquireAdmission = async (): Promise<boolean> => {
872
979
  if (!options.pool || typeof options.pool.acquireAdmission !== 'function') return true;
980
+ const stop = watchStall('admission');
873
981
  try { releaseAdmission = await options.pool.acquireAdmission(input.signal, after); return true; }
874
982
  catch (error) {
875
983
  // A predecessor closed without merging is not a cancellation: surface it so the queue owner is notified.
876
984
  if (error instanceof Error && error.message.startsWith('pod lease predecessor blocked')) admissionBlocked = error.message;
877
985
  return false;
878
- }
986
+ } finally { stop(); }
879
987
  };
880
988
  let admissionBlocked: string | undefined;
881
989
  const admissionFailure = (): SelfImplementJobDone => admissionBlocked
882
990
  ? { exitCode: 1, output: '', error: { code: 'pod-predecessor-blocked', message: admissionBlocked } }
883
991
  : { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
992
+ const unfitDone = await unfitFailure();
993
+ if (unfitDone) return unfitDone;
884
994
  if (!await acquireAdmission()) return admissionFailure();
885
995
  let member: PodPoolMember | null = await acquireSlot();
886
996
  if (options.pool && !member) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
@@ -932,25 +1042,40 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
932
1042
  for (;;) {
933
1043
  try {
934
1044
  const grok = options.provider === 'grok';
935
- const account = retryAccount ?? (grok ? 'grok' : options.accountBroker?.() ?? options.account ?? 'team');
936
- debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account, brokered: Boolean(options.accountBroker) });
937
- const codexAccounts = grok || !options.accountBroker || !options.rotationAccounts ? undefined : [account, ...options.rotationAccounts.filter((candidate) => candidate !== account && candidate !== failedAccount)];
938
- if (codexAccounts && !options.rotationAccounts!.includes(account)) throw new Error(`pod: 배분 계정 ${account} 이 회전 계획에 없다`);
939
- if (codexAccounts && new Set(options.rotationAccounts).size !== options.rotationAccounts!.length) throw new Error('pod: 유효하고 서로 다른 codex 계정이 필요하다');
1045
+ const plannedAccount = retryAccount ?? (grok ? 'grok' : options.accountBroker?.() ?? options.account ?? 'team');
1046
+ debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account: plannedAccount, brokered: Boolean(options.accountBroker) });
1047
+ const plannedCodexAccounts = grok || !options.accountBroker || !options.rotationAccounts ? undefined : [plannedAccount, ...options.rotationAccounts.filter((candidate) => candidate !== plannedAccount && candidate !== failedAccount)];
1048
+ if (plannedCodexAccounts && !options.rotationAccounts!.includes(plannedAccount)) throw new Error(`pod: 배분 계정 ${plannedAccount} 이 회전 계획에 없다`);
1049
+ if (plannedCodexAccounts && new Set(options.rotationAccounts).size !== options.rotationAccounts!.length) throw new Error('pod: 유효하고 서로 다른 codex 계정이 필요하다');
940
1050
  const accountCredentials = options.credentials ?? hostCredentials;
941
- if (codexAccounts) podCodexAccountScript(codexAccounts);
942
- const codexCredentials = codexAccounts?.map((candidate) => {
943
- const credential = accountCredentials(candidate);
1051
+ if (plannedCodexAccounts) podCodexAccountScript(plannedCodexAccounts);
1052
+ // POD-TOKEN-PREREFRESH: «3시간 안 만료» 후보는 선갱신 → 실패면 후보에서 빼고 다음 계정으로.
1053
+ const prerefreshDeps = {
1054
+ credentials: accountCredentials,
1055
+ refresh: options.codexPrerefresh ?? (async (name: string) => (await import('../../oauth/codex.js')).refreshCodexAccountHome(name)),
1056
+ isLeaseHolder: options.hqLeaseHolder ?? (options.kubectl ? () => false : defaultHqLeaseHolder), // kubectl 주입(=시험)이면 본부 임대를 묻지 않는다
1057
+ };
1058
+ const candidates = grok ? [] : plannedCodexAccounts ?? [plannedAccount];
1059
+ const usable: Array<{ name: string; credential: ReturnType<typeof accountCredentials> }> = [];
1060
+ for (const candidate of candidates) {
1061
+ const credential = await podCodexCredentialWithPrerefresh(candidate, prerefreshDeps);
1062
+ if (credential) usable.push({ name: candidate, credential });
1063
+ }
1064
+ if (!grok && usable.length === 0) throw new Error(`openai-codex 후보(${candidates.join(', ')}) access token 이 전부 3시간 안에 만료 — 선갱신도 못 했다(본부 임대 없음 또는 갱신 실패 · pod.credential-prerefresh 를 보라)`);
1065
+ const account = grok ? plannedAccount : usable[0]!.name;
1066
+ if (account !== plannedAccount) debug.log('self-implement.pod', 'account-skipped-expiring', { spaceId: input.spaceId, from: plannedAccount, to: account });
1067
+ const codexAccounts = plannedCodexAccounts ? usable.map((entry) => entry.name) : undefined;
1068
+ const codexCredentials = codexAccounts ? usable.map(({ name: candidate, credential }) => {
944
1069
  const elanous = JSON.parse(credential.elanousAuth) as { providers?: Record<string, { tokens?: { refreshToken?: unknown } }> };
945
1070
  const codex = JSON.parse(credential.codexAuth) as { tokens?: { refresh_token?: unknown } };
946
1071
  if (!elanous.providers?.['openai-codex']?.tokens || elanous.providers['openai-codex'].tokens.refreshToken || !codex.tokens || codex.tokens.refresh_token) {
947
1072
  throw new Error(`pod: ${candidate} 자격에 refresh 토큰이 있거나 인증 파일이 없다`);
948
1073
  }
949
1074
  return credential;
950
- });
1075
+ }) : undefined;
951
1076
  const creds = grok
952
1077
  ? (options.grokCredentials ?? (() => hostGrokCredentials({ env, apiKeyOptIn: options.grokApiKeyOptIn })))()
953
- : codexCredentials?.[0] ?? accountCredentials(account);
1078
+ : codexCredentials?.[0] ?? usable[0]!.credential;
954
1079
  if (grok && !('grokAuth' in creds && creds.grokAuth) && !('grokApiKey' in creds && creds.grokApiKey && options.grokApiKeyOptIn === true)) {
955
1080
  throw new Error('grok: 구독 자격 없음 · API 키 opt-in 꺼짐 또는 키 없음');
956
1081
  }
@@ -1202,12 +1327,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1202
1327
  ? String(detail.until) : undefined;
1203
1328
  const armEnv = Object.fromEntries(Object.entries(options.armEnv ?? {}).filter(([key]) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL' && key !== POD_GITHUB_CREDENTIAL_TOKEN_ENV && key !== POD_GITHUB_CREDENTIAL_URL_ENV && (!hostRefresh || !['GH_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR'].includes(key))));
1204
1329
  const seat = env.ELANOUS_HARNESS_SEAT;
1205
- const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil || seat
1330
+ const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil || seat || env.ELANOUS_POD_AUTHOR_ON_POD === '1'
1206
1331
  ? {
1207
1332
  ...armEnv,
1208
1333
  ...(detailUntil ? { ELANOUS_LIVE_DETAIL_UNTIL: detailUntil } : {}),
1209
1334
  ...(env.ELANOUS_DISPATCH_RECORDED === '1' ? { ELANOUS_DISPATCH_RECORDED: '1' } : {}),
1210
1335
  ...(seat === 'OP' || seat === 'TC' || seat === 'MK' || seat === 'UX' ? { ELANOUS_HARNESS_SEAT: seat } : {}),
1336
+ ...(env.ELANOUS_POD_AUTHOR_ON_POD === '1'
1337
+ ? { ELANOUS_POD_AUTHOR_ON_POD: '1', ...(env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'flag' || env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'config' || env.ELANOUS_POD_AUTHOR_GRADE_SOURCE === 'default'
1338
+ ? { ELANOUS_POD_AUTHOR_GRADE_SOURCE: env.ELANOUS_POD_AUTHOR_GRADE_SOURCE, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { ELANOUS_POD_AUTHOR_GRADE: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}) } : {}),
1211
1339
  ...(grounding ? { [GROUNDING_URL_ENV]: groundingUrl! } : {}),
1212
1340
  ...(credentialRelay ? { [POD_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GROK_PATH}` } : {}),
1213
1341
  ...(githubRelay ? { [POD_GITHUB_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GITHUB_PATH}` } : {}),
@@ -1224,7 +1352,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1224
1352
  debug.log('self-implement.pod', 'memory-request', { spaceId: input.spaceId, job: name,
1225
1353
  tier: oomRetried ? retryTier : memoryTier, reason: oomRetried ? 'OOMKilled' : memoryReason,
1226
1354
  memoryLimit, memoryRequest });
1227
- const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
1355
+ const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
1228
1356
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
1229
1357
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
1230
1358
  releaseAdmission?.applied?.(name, context, namespace);
@@ -1468,6 +1596,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1468
1596
  if (member) { options.pool!.release(member); member = null; }
1469
1597
  releaseAdmission?.();
1470
1598
  releaseAdmission = undefined;
1599
+ const unfitRetry = await unfitFailure();
1600
+ if (unfitRetry) return unfitRetry;
1471
1601
  if (!await acquireAdmission()) return admissionFailure();
1472
1602
  member = await acquireSlot();
1473
1603
  if (options.pool && !member) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before a pool slot opened' } };
@@ -1593,12 +1723,13 @@ function lastPodChildFailure(logs: string): { stage: string; error: string } | n
1593
1723
  try { result = JSON.parse(line); } catch { continue; }
1594
1724
  if (!result || typeof result !== 'object' || Array.isArray(result)) continue;
1595
1725
  const row = result as Record<string, unknown>;
1596
- if (typeof row.stage !== 'string' || typeof row.ok !== 'boolean') continue;
1597
- if (row.ok !== false || typeof row.error !== 'string') return null;
1598
- const lines = row.error.replace(/\/home\/[^/\s]+\//g, '~/').split(/\r\n|\n|\r/);
1726
+ const terminal = podTerminalRow(row);
1727
+ if (!terminal) continue;
1728
+ if (terminal.ok !== false || typeof terminal.error !== 'string') return null;
1729
+ const lines = terminal.error.replace(/\/home\/[^/\s]+\//g, '~/').split(/\r\n|\n|\r/);
1599
1730
  const first = lines[0]!.slice(0, 240);
1600
1731
  const last = lines.at(-1)!.slice(0, 240);
1601
- return { stage: row.stage, error: first === last ? first : `${first}\n${last}` };
1732
+ return { stage: terminal.stage, error: first === last ? first : `${first}\n${last}` };
1602
1733
  }
1603
1734
  return null;
1604
1735
  }
@@ -19,6 +19,7 @@
19
19
  * Cf. PLAN-parallel-self-dev-orchestrator-2026-07-21.
20
20
  */
21
21
  import { lstatSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
22
+ import { oldDoorInternalEnv } from '../../self-dev/old-door.js';
22
23
  import { selfDevRunsDir } from '../../self-dev/run-store.js';
23
24
  import { debug } from '../../debug/log.js';
24
25
  import { join, resolve } from 'node:path';
@@ -356,6 +357,7 @@ export function defaultSelfImplementSpawn(): SelfImplementJobSpawn {
356
357
  ...childNestEnv(),
357
358
  ...harnessSpaceEnv('self-implement', input.spaceId),
358
359
  ...executorRoleEnv(), // ⭐ 세포 role 분화 — 자식은 executor(실행자)로 자기인지.
360
+ ...oldDoorInternalEnv('self-implement'),
359
361
  },
360
362
  stdio: ['ignore', 'pipe', 'pipe'],
361
363
  });
@@ -233,6 +233,11 @@ export function parseTelegramSlash(text: string, commands: TgSlashCommand[]): Tg
233
233
  * caller (handleIncoming) can send it directly without going through
234
234
  * dispatchTelegramSlash when it already has the parse result. */
235
235
  export function buildUnknownSlashReply(name: string, commands: TgSlashCommand[]): string {
236
+ const unsupported = buildBotSlashCatalog({
237
+ surface: 'telegram', coreCommands: SLASH_COMMANDS, maturity: FEATURE_MATURITY,
238
+ handledCommands: commands,
239
+ }).unsupportedReply(name);
240
+ if (unsupported) return unsupported;
236
241
  const names = commands.map(c => `/${c.name}`).join(' ');
237
242
  return `Unknown command: /${name}\nAvailable: ${names}`;
238
243
  }
@@ -327,7 +332,7 @@ export function defaultTelegramCommands(
327
332
  {
328
333
  name: 'loops',
329
334
  description: '루프·크론 현황 — 늦음·실패를 먼저 보여줍니다',
330
- handler: async (args) => telegramLoopsStatus(args),
335
+ handler: (args) => telegramLoopsStatus(args),
331
336
  },
332
337
  {
333
338
  name: 'status',
@@ -1,6 +1,6 @@
1
1
  import { dashboardLoops } from './domains/dashboard-data.js';
2
2
  import { readSchedulesInventory } from './nexus/api/schedules-read.js';
3
- import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH, type LoopRow } from './loops/status-rows.js';
3
+ import { loadLoopRows, LOOP_SCHEDULES_PATH, LOOPS_PATH } from './loops/status-rows.js';
4
4
 
5
5
  interface LoopsSources {
6
6
  schedules: () => unknown;
@@ -25,20 +25,27 @@ export async function telegramLoopsStatus(args: string[], sources: LoopsSources
25
25
  if (path === LOOPS_PATH) return sources.loops();
26
26
  throw new Error('unknown inventory');
27
27
  }, sources.now());
28
- if (!rows.length) return '등록된 루프·크론이 없습니다.';
29
- const counts = (['늦음', '실패', '살아 있음', '꺼짐', '판정 불가'] as const)
30
- .map(verdict => `${verdict} ${rows.filter(row => row.verdict === verdict).length}`);
31
- const priority: Record<LoopRow['verdict'], number> = { '늦음': 0, '실패': 1, '살아 있음': 2, '꺼짐': 3, '판정 불가': 4 };
32
- const lines = rows.slice().sort((a, b) => priority[a.verdict] - priority[b.verdict]).map(row =>
33
- `• ${row.verdict === '늦음' ? '🔴' : row.verdict === '실패' ? '❌' : row.verdict === '살아 있음' ? '🟢' : '⚪'} ${row.verdict} · ${clean(row.name)} (${clean(row.layer)}) · ${clean(row.mode)} · 마지막 ${row.lastRun ?? '기록 없음'} · ${clean(row.id)}`);
34
- let reply = ['루프·크론 현황', counts.join(' · ')].join('\n');
35
- for (const [index, line] of lines.entries()) {
36
- // Telegram caps a message at 4096 characters; leave space for an explicit omission count.
37
- if (reply.length + line.length + 35 > 4000) return `${reply}\n… ${lines.length - index}개 더 (메시지 길이 제한)`;
38
- reply += `\n${line}`;
28
+ if (!rows.length) return '모두 정상';
29
+ const problems = rows.filter(row => row.verdict === '늦음' || row.verdict === '실패');
30
+ const ordered = [
31
+ ...problems.filter(row => row.verdict === '늦음'),
32
+ ...problems.filter(row => row.verdict === '실패'),
33
+ ];
34
+ const summary = (['살아 있음', '꺼짐', '판정 불가'] as const)
35
+ .map(verdict => `${verdict} ${rows.filter(row => row.verdict === verdict).length}`).join(' · ');
36
+ const lines = ['루프·크론 현황'];
37
+ let shown = 0;
38
+ for (const row of ordered.slice(0, 10)) {
39
+ const line = `• ${row.verdict === '늦음' ? '🔴' : '❌'} ${row.verdict} · ${clean(row.name)} (${clean(row.layer)}) · ${clean(row.mode)} · 마지막 ${row.lastRun ?? '기록 없음'} · ${clean(row.id)}`;
40
+ // Leave room for the summary and an omission count under Telegram's 4096-character cap.
41
+ if (lines.join('\n').length + line.length + summary.length + 60 > 4000) break;
42
+ lines.push(line);
43
+ shown++;
39
44
  }
40
- return reply;
45
+ if (shown < ordered.length) lines.push(`… ${ordered.length - shown}개 더`);
46
+ lines.push(summary);
47
+ return lines.join('\n');
41
48
  } catch {
42
- return '루프 현황을 읽지 못했습니다. 일부 레지스트리 조회가 실패했습니다. 데몬 연결을 확인하세요.';
49
+ return '루프 현황 못 읽음';
43
50
  }
44
51
  }
package/src/telegram.ts CHANGED
@@ -80,6 +80,8 @@ import type { TranscriptionEngine, FieldAudio } from './field-note/transcribe.js
80
80
  import { maybeHandleCardPhoto, type CardPhotoDeps } from './telegram-card-followup.js';
81
81
  import { attachTelegramProjectButtons } from './telegram-project-command.js';
82
82
  import { attachTelegramFabricPlan } from './telegram-fabric-plan.js';
83
+ import { contextNow as readContextNow, type ContextNowDeps } from './context-bus/context-now.js';
84
+ import { renderTelegramNow } from './context-bus/context-now-surfaces.js';
83
85
 
84
86
  export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
85
87
 
@@ -233,6 +235,9 @@ export interface TelegramBotOpts {
233
235
  /** Test seam for the business-card graph and its private run root. */
234
236
  cardFollowupDeps?: Pick<CardPhotoDeps, 'runGraph' | 'rootDir' | 'ocrText'>;
235
237
  fabricPlanDeps?: import('./telegram-fabric-plan.js').TelegramFabricPlanDeps;
238
+ /** 시험 seam — `/v1/context/now` 와 같은 `contextNow` 읽기. 던지면 «맥락 못 읽음». */
239
+ readContextNow?: typeof readContextNow;
240
+ contextNowDeps?: ContextNowDeps;
236
241
  }
237
242
 
238
243
  /** Surface-unification v2 (FU-2) — Telegram trigger event passed to
@@ -264,6 +269,9 @@ const EMPTY_ALLOWLIST_NOTICE_GAP_MS = 10 * 60 * 1000;
264
269
  // Tuned above a normal chat answer but below a real delegation (which
265
270
  // pays a multi-second session load before any work).
266
271
  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 = '맥락 못 읽음';
267
275
 
268
276
  interface ApiResponse<T> {
269
277
  ok: boolean;
@@ -355,6 +363,12 @@ export class TelegramBot {
355
363
  private readonly cardFollowupDeps: Pick<CardPhotoDeps, 'runGraph' | 'rootDir' | 'ocrText'>;
356
364
  private readonly seatWorkDeps: TelegramSeatWorkDeps;
357
365
  private readonly handleFabricPlan: (ctx: TgIncoming) => Promise<boolean>;
366
+ private readonly readContextNow: typeof readContextNow;
367
+ private readonly contextNowDeps?: ContextNowDeps;
368
+ /** 대화 키(chat+thread) → 그 대화의 마지막 사용자 발화 시각(요약 여부와 무관하게 매 발화마다 갱신). */
369
+ private readonly lastUtteranceAt = new Map<string, number>();
370
+ /** 첫 말이거나 6시간 이상 쉰 뒤라 요약이 «밀려 있는» 대화 키 — 다음 일반 답이 한 번 소비한다. */
371
+ private readonly contextFirstPending = new Set<string>();
358
372
  private seatAskTimer: ReturnType<typeof setInterval> | null = null;
359
373
  /** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
360
374
  private readonly fieldGroups = new Map<string, {
@@ -394,6 +408,8 @@ export class TelegramBot {
394
408
  this.fieldNoteDecodeAudio = opts.fieldNoteDecodeAudio;
395
409
  this.cardFollowupDeps = opts.cardFollowupDeps ?? {};
396
410
  this.handleFabricPlan = attachTelegramFabricPlan(this, opts.fabricPlanDeps);
411
+ this.readContextNow = opts.readContextNow ?? readContextNow;
412
+ this.contextNowDeps = opts.contextNowDeps;
397
413
  this.seatWorkDeps = { ...opts.seatWorkDeps, askDeps: { ...opts.seatWorkDeps?.askDeps,
398
414
  channel: 'telegram', botId: this.botId,
399
415
  send: opts.seatWorkDeps?.askDeps?.send ?? (async (origin, text) => {
@@ -414,6 +430,34 @@ export class TelegramBot {
414
430
  return this.allowedUsers.size > 0 && this.allowedUsers.has(userId);
415
431
  }
416
432
 
433
+ /** 허용된 모든 발화(명령 포함)마다 부른다 — 쉼은 마지막 발화부터 재고, 요약 전송과는 따로 기록한다. */
434
+ 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);
440
+ }
441
+
442
+ /** 처음이거나 6시간 이상 쉰 대화의 첫 일반 답 앞에 context-now 요약 한 메시지를 보낸다. 실패해도 본 답은 막지 않는다. */
443
+ private async maybeSendContextFirst(ctx: TgIncoming): Promise<void> {
444
+ const key = `${ctx.chatId}:${ctx.threadId ?? ''}`;
445
+ if (!this.contextFirstPending.delete(key)) return;
446
+ const cfg = this.slashContext?.userConfig.telegram;
447
+ 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
+ try {
455
+ await this.sendMessage(ctx.chatId, text, { replyTo: ctx.messageId, threadId: ctx.threadId });
456
+ } catch (err) {
457
+ this.log(`telegram context-first failed: ${err instanceof Error ? err.message : String(err)}`);
458
+ }
459
+ }
460
+
417
461
  private async refuseMessage(ctx: TgIncoming): Promise<void> {
418
462
  const empty = this.allowedUsers.size === 0;
419
463
  debug.log('telegram.owner-gate', 'refused', {
@@ -1545,6 +1589,7 @@ export class TelegramBot {
1545
1589
  await this.refuseMessage(ctx);
1546
1590
  return;
1547
1591
  }
1592
+ this.noteUtterance(ctx);
1548
1593
  if (await this.handleFabricPlan(ctx)) return;
1549
1594
  // 현장 업로드 — `#현장` 캡션(또는 태그된 앨범의 나머지)의 사진·영상은 LLM 대신 현장 폴더로.
1550
1595
  if (await this.tryHandleFieldUpload(ctx)) return;
@@ -1973,6 +2018,8 @@ export class TelegramBot {
1973
2018
  debug.log('telegram.deliver', 'reaction', { chatId: ctx.chatId, phase: 'start', emoji: '👀', on: reactionsOn });
1974
2019
  if (reactionsOn && ctx.messageId != null) void this.setMessageReaction(ctx.chatId, ctx.messageId, '👀');
1975
2020
 
2021
+ await this.maybeSendContextFirst(ctx);
2022
+
1976
2023
  // Post a placeholder NOW so the user sees acknowledgement while
1977
2024
  // the LLM is thinking. The same message is then edited in-place
1978
2025
  // as tokens stream in — far better UX than dead air followed by