elanous 0.2.14 → 0.2.16

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 (350) 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-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  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 +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -15,10 +15,10 @@ import { podSkillsDigest, readSkillEnvFiles, resolvePodSkills } from './pod-skil
15
15
  import { podSourceScript, type PodSource } from './pod-source-receive.js';
16
16
  import { GROUNDING_TOKEN_ENV, GROUNDING_URL_ENV, mintGroundingToken, revokeGroundingRun, type GroundingTokenScope } from '../../grounding/token.js';
17
17
  import { POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH, POD_GITHUB_CREDENTIAL_TOKEN_ENV, POD_GITHUB_CREDENTIAL_URL_ENV, installationRepositories } from '../../nexus/api/pod-credential-api.js';
18
- import { githubInstallationCredential } from '../../auth/github-app-token.js';
18
+ import { coalescedInstallationCredential } from '../../auth/github-app-token.js';
19
19
  import { POD_CREDENTIAL_TOKEN_ENV, POD_CREDENTIAL_URL_ENV } from '../../grok/credential.js';
20
20
  import { collectPodLedgers, createPodLedgerFollower } from './pod-ledger-collect.js';
21
- import { collectPodArtifacts } from './pod-artifact-return.js';
21
+ import { collectPodArtifacts, podNoResultDiagnostic } from './pod-artifact-return.js';
22
22
  import { isValidAccountName, resolveCodexAccount } from '../../oauth/codex-account.js';
23
23
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
24
24
  import { controlInboxEnv } from '../../harness/control-inbox.js';
@@ -38,7 +38,10 @@ 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
46
  import { measurePoolLease, recommendConcurrency, POD_HOST_LEASE_ANNOTATION, LEASE_KUBECTL_MAX_BUFFER } from './pod-lease.js';
44
47
  import { ACTUAL_SUBSTRATE_ENV, RUN_CONTRACT_ENV, carryRunContract, completionFloorFor } from '../../self-implement/graph-run-contract.js';
@@ -59,8 +62,11 @@ import { LLM_TIER_MAP_BY_PROVIDER, lookupLlmTierSpec, type LlmTierProvider } fro
59
62
  import { parseSelfImplementJson, type SelfImplementJobDone, type SelfImplementJobSpawn } from './self-implement.js';
60
63
  import { codexQuotaPolicyFromConfig } from '../../oauth/codex-account-store.js';
61
64
  import { goalTypeOf, type GoalType } from '../../../scripts/measure-pod-memory-by-goal.js';
65
+ import { declaredGoalType, type GoalType as DeclaredGoalType } from '../../self-implement/goal-author.js';
62
66
  import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
63
67
  import { getUserConfig } from '../../user-config.js';
68
+ import { extractPodFailureReason } from './pod-failure-reason.js';
69
+ import { OLD_DOOR_STAMP_ENV } from '../../self-dev/old-door.js';
64
70
 
65
71
  export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
66
72
 
@@ -68,7 +74,9 @@ export type Kubectl = (args: readonly string[], input?: string) => { status: num
68
74
  // still the credential source for gh and setup-git's helper. Install it privately
69
75
  // on that failure; rename keeps concurrent readers from seeing a partial token.
70
76
  const APP_GH_CONFIG_PATH = '$HOME/.config/gh';
71
- 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{}}';
72
80
  const APP_GH_AUTH_COMMAND = `bun -e '${APP_GH_AUTH_SCRIPT}'`;
73
81
  const APP_GH_EXEC = `unset GH_TOKEN GITHUB_TOKEN; GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}" exec "$@"`;
74
82
 
@@ -83,6 +91,11 @@ export interface PodSpawnOptions {
83
91
  rotationAccounts?: readonly string[];
84
92
  /** 미지정이면 종전 Codex Job. */
85
93
  provider?: 'openai-codex' | 'grok';
94
+ /** Child model/effort chosen at launch (`--child-llm-model/--child-llm-effort`) — carried into the Pod child. */
95
+ childModel?: string;
96
+ childEffort?: string;
97
+ /** True when the child provider was named at launch — the child gets explicit flags even for codex. */
98
+ childProviderExplicit?: boolean;
86
99
  /** 🔐 API 키 과금 허용은 명시 true 뿐. */
87
100
  grokApiKeyOptIn?: boolean;
88
101
  namespace?: string;
@@ -108,7 +121,7 @@ export interface PodSpawnOptions {
108
121
  pollMs?: number;
109
122
  kubectl?: Kubectl;
110
123
  /** Host-side regate (injected for Pod tests). */
111
- hostRegate?: (input: { prNumber: number; headCommit: string; repoRoot: string }) => Promise<HostRegateResult>;
124
+ hostRegate?: (input: { prNumber: number; headCommit: string; repoRoot: string; goalFile?: string }) => Promise<HostRegateResult>;
112
125
  /** PR comment for host-regate failures that never reached runHostRegate (injectable for tests). */
113
126
  ghComment?: (prNumber: number, body: string) => void;
114
127
  /** 🔑 Pod 필수 스킬의 키(.env)를 이 런의 Secret 으로 넘긴다 — 명시 opt-in(유료 크레딧을 쓴다). */
@@ -123,7 +136,8 @@ export interface PodSpawnOptions {
123
136
  mintGrounding?: (claims: { runId: string; job: string; ttlMs: number; scope?: GroundingTokenScope; repository?: string }) => Promise<{ token: string; exp: number }>;
124
137
  revokeGrounding?: (runId: string) => void;
125
138
  /** Test seam for the host's repository-scoped GitHub App installation token. */
126
- githubInstallation?: (repository: string) => { token: string; expires_at?: string; expiresAt?: string | number } | null;
139
+ /** `fresh` = do not reuse a coalesced token (the exit-7 retry must not get the token that just failed back). */
140
+ githubInstallation?: (repository: string, opts?: { fresh?: boolean }) => { token: string; expires_at?: string; expiresAt?: string | number } | null;
127
141
  /** GitHub installation repository lookup; required to prove the scoped token matches owner/name. */
128
142
  githubRepositories?: (token: string) => Promise<readonly string[] | null>;
129
143
  now?: () => number;
@@ -184,6 +198,17 @@ export function hostCredentials(account: string, storePath: string = authStorePa
184
198
 
185
199
  /** A Pod gets a refresh-less access copy, so the copy must outlive the Job: same rule as the Codex path (3 h). */
186
200
  export const POD_GROK_MIN_VALIDITY_MS = 3 * 3600_000;
201
+ /** PODCRED1 (10-05): an App installation token lives ~60 min and the host renews it in the Pod at the ten-minute
202
+ * boundary; below this remaining life at launch the token is observed (`github-app-short-lived`). */
203
+ export const POD_APP_TOKEN_MIN_START_MS = 20 * 60_000;
204
+ /** Exit 7 (in-Pod GitHub login failed) is retried once after this spread backoff — bursts were simultaneous launches. */
205
+ export const POD_GH_LOGIN_RETRY_BACKOFF_MS = 60_000;
206
+ /** Base backoff plus a 0-59 s spread derived from the job name, so jobs that failed together retry apart. */
207
+ export function podGhLoginRetryBackoffMs(job: string): number {
208
+ let spread = 0;
209
+ for (const ch of job) spread = (spread * 31 + ch.charCodeAt(0)) % 60;
210
+ return POD_GH_LOGIN_RETRY_BACKOFF_MS + spread * 1000;
211
+ }
187
212
 
188
213
  /** resolveGrokCredential 과 동일한 출처를 읽되 Pod 로는 refresh 없는 access 사본만 보낸다.
189
214
  * POD4 (10-01): the copy used to ship whatever the host had — an AUTH1 shard copied a token that had expired
@@ -453,11 +478,11 @@ export function podRunLabels(o: { runId?: string; parentRunId?: string }): Recor
453
478
 
454
479
  /** Pod 메모리 «등급» — 대표 09-27: PWA Pod 골이 16Gi 에서 OOM 으로 죽는다 ⇒ «high» 등급을 옵션으로 고른다.
455
480
  * 등급 = `standard`(`ELANOUS_POD_MEMORY` 또는 16Gi) · `high`(`ELANOUS_POD_MEMORY_HIGH` 또는 32Gi · standard 보다 작아지지 않는다).
456
- * 고르는 순서(앞이 이긴다): ① 발사 옵션 `--pod-memory <등급>`(→ `ELANOUS_POD_MEMORY_TIER`) ② 조각 문면 한 줄 `Pod 메모리: high|standard` ③ 조각의 `apps/pwa/` ④ 부모 골 본문의 같은 두 규칙 ⑤ standard.
481
+ * 고르는 순서(앞이 이긴다): ① 발사 옵션 `--pod-memory <등급>`(→ `ELANOUS_POD_MEMORY_TIER`) ② 조각 문면 `Pod 메모리: lite|standard|high` ③ 조각의 `apps/pwa/` ④ 부모의 명시 메모리 줄·PWA 경로 ⑤ 실측 권고 ⑥ 권고 없는 research/document 는 lite ⑦ standard.
457
482
  * node-b 노드 할당 가능 약 251Gi. */
458
483
  export const POD_MEMORY_DEFAULT = '16Gi';
459
484
  export const POD_MEMORY_HIGH_DEFAULT = '32Gi';
460
- /** POD7 — `lite`(`ELANOUS_POD_MEMORY_LITE` 또는 2Gi): 명시할 때만(인자·문면 줄) · 자동 판정은 없다(성공 런 피크 기록 0 — 설계 §②). OOM 이면 POD9 가 high 로 한 번 다시 띄운다. */
485
+ /** POD7 — `lite`(`ELANOUS_POD_MEMORY_LITE` 또는 2Gi): research/document 기본값; OOM 시 standard 로 한 단계 승급한다. */
461
486
  export const POD_MEMORY_LITE_DEFAULT = '2Gi';
462
487
 
463
488
  /** Reserve from measured per-run peaks, never from the advisory limit tier. Sparse or missing measurements keep the old reservation. */
@@ -475,7 +500,7 @@ export function podMemoryRequestFor(kind: GoalType | null, advice?: PodMemoryAdv
475
500
  }
476
501
  export const POD_MEMORY_TIERS = ['lite', 'standard', 'high'] as const;
477
502
  export type PodMemoryTier = (typeof POD_MEMORY_TIERS)[number];
478
- export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto' | 'advise';
503
+ export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto' | 'advise' | 'goal-type-auto';
479
504
  const PWA_PATH = /(?:^|[\s`'"(,·])apps\/pwa\//m;
480
505
  const GOAL_LINE = /^[ \t]*(?:Pod 메모리|pod-memory)[ \t]*:[ \t]*(lite|standard|high)[ \t]*\r?$/im;
481
506
 
@@ -511,6 +536,10 @@ export function parsePodMemoryTier(v: string | undefined): PodMemoryTier | null
511
536
  return t && (POD_MEMORY_TIERS as readonly string[]).includes(t) ? (t as PodMemoryTier) : null;
512
537
  }
513
538
 
539
+ function parsePodGoalType(value: string | undefined): DeclaredGoalType | null {
540
+ return value === 'implement' || value === 'research' || value === 'document' || value === 'operate' ? value : null;
541
+ }
542
+
514
543
  function memoryGi(v: string): number | null {
515
544
  const m = /^(\d+(?:\.\d+)?)(Gi|Mi)$/.exec(v);
516
545
  if (!m) return null;
@@ -521,9 +550,13 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
521
550
  const option = parsePodMemoryTier(env.ELANOUS_POD_MEMORY_TIER);
522
551
  const line = parsePodMemoryTier(GOAL_LINE.exec(feature)?.[1]);
523
552
  const parentLine = !option && !line ? parsePodMemoryTier(GOAL_LINE.exec(parentGoal ?? '')?.[1]) : null;
553
+ const declared = parsePodGoalType(env.ELANOUS_POD_GOAL_TYPE)
554
+ ?? declaredGoalType(feature) ?? declaredGoalType(parentGoal ?? '');
555
+ // 선언 «document» 는 실측 분류의 «docs» 다 — 낱말 판정(goalTypeOf)이 «document» 를 못 읽어 권고가 안 이어지던 구멍
524
556
  const goalType = advice && !option && !line && !parentLine
525
557
  ? (broadPodTestWarning(feature) ? 'test' : goalTypeOf(feature))
526
558
  ?? (parentGoal ? (broadPodTestWarning(parentGoal) ? 'test' : goalTypeOf(parentGoal)) : null)
559
+ ?? (declared === 'document' ? 'docs' : null)
527
560
  : null;
528
561
  const recommendation = advice?.byGoalType.find((entry) => entry.goalType === goalType && entry.evidence.runs > 0)?.recommended;
529
562
  const [tier, source]: [PodMemoryTier, PodMemorySource] = option ? [option, 'option']
@@ -531,8 +564,11 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
531
564
  // 조각 자신의 PWA 경로가 부모 줄·실측 권고보다 먼저다 — 부모 «standard» 가 PWA 조각을 16Gi 로 내리면 OOM(POD7 · #23569 순서 회귀)
532
565
  : goalTouchesPwa(feature) ? ['high', 'pwa-auto']
533
566
  : parentLine ? [parentLine, 'parent-goal-line']
534
- : recommendation ? [recommendation, 'advise']
535
567
  : parentGoal && goalTouchesPwa(parentGoal) ? ['high', 'parent-pwa-auto']
568
+ // 실측이 high(OOM 증거)를 권하면 선언 종류의 자동 lite 보다 먼저다 — 문서 골도 OOM 이 났으면 다시 2Gi 로 띄우지 않는다
569
+ : recommendation === 'high' ? ['high', 'advise']
570
+ : declared === 'research' || declared === 'document' ? ['lite', 'goal-type-auto']
571
+ : recommendation ? [recommendation, 'advise']
536
572
  : ['standard', 'default'];
537
573
  if (tier === 'lite') return { limit: env.ELANOUS_POD_MEMORY_LITE?.trim() || POD_MEMORY_LITE_DEFAULT, tier, source };
538
574
  const base = env.ELANOUS_POD_MEMORY?.trim() || POD_MEMORY_DEFAULT;
@@ -542,14 +578,37 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
542
578
  return { limit: a !== null && b !== null && a > b ? base : high, tier, source };
543
579
  }
544
580
 
545
- 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> {
581
+ /** A named child provider decides the Pod provider (10-05 PODPROVIDER) — grok needs a usable credential, other names are refused. */
582
+ export function podNamedChildProvider(named: string | undefined, credential: { grokSubscription: boolean; grokApiKey: boolean }): { provider?: 'openai-codex' | 'grok'; refuse?: string } {
583
+ if (!named) return {};
584
+ if (named === 'openai-codex' || named === 'codex') return { provider: 'openai-codex' };
585
+ if (named === 'grok') return credential.grokSubscription || credential.grokApiKey ? { provider: 'grok' } : { refuse: 'pod: Pod 에 grok 자격 없음 — 구독 자격이 쓸 수 없거나(만료 임박·갱신 불가) API 키 과금 동의(harness.pod.grokApiKeyOptIn)가 없다 · codex 로 조용히 돌리지 않는다' };
586
+ return { refuse: `pod: Pod 자식 provider 는 openai-codex|grok 만 — 받음 ${named}` };
587
+ }
588
+
589
+ /** Fallback credentials for the codex plan — a launch that named openai-codex never falls back to grok (post-review must-fix). */
590
+ export function podFallbackCredentials(named: 'openai-codex' | 'grok' | undefined, fallback: { grokSubscription: boolean; grokApiKey: boolean }): { grokSubscription: boolean; grokApiKey: boolean } {
591
+ return named === 'openai-codex' ? { grokSubscription: false, grokApiKey: false } : fallback;
592
+ }
593
+
594
+ /** Child LLM flags for the Pod child — grok always names its model; codex only when the launch named it (10-05 PODPROVIDER). */
595
+ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'childModel' | 'childEffort' | 'childProviderExplicit'>): string[] {
596
+ const provider = options.provider === 'grok' ? 'grok' : options.childProviderExplicit ? 'openai-codex' : undefined;
597
+ if (!provider) return [];
598
+ const model = options.childModel?.trim() || (provider === 'grok' ? defaultGrokModel().id : undefined);
599
+ // `self implement` registers no --child-llm-effort — passing it kills the Pod at argument parsing, so effort stays host-only.
600
+ return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
601
+ }
602
+
603
+ 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; 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> {
546
604
  const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
547
605
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
548
- const askBaseIndex = o.goalDoc ? o.args.indexOf('--base') : -1;
606
+ const delegated = Boolean(o.goalDoc) || o.authorSentence === true;
607
+ const askBaseIndex = delegated ? o.args.indexOf('--base') : -1;
549
608
  const quotedAsk = (askBaseIndex >= 0 && o.args[askBaseIndex + 1]
550
609
  ? ` '--base' '${o.args[askBaseIndex + 1]!.replace(/'/g, `'\\''`)}'`
551
- : '') + (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' : '')
552
- + (o.goalDoc && o.args.includes('--no-supervise') ? ' --no-supervise' : '');
610
+ : '') + (delegated && o.args.includes('--merge-by-host') ? ' --merge-by-host' : delegated && o.args.includes('--open-pr') && !o.args.includes('--auto-merge') ? ' --no-auto-merge' : '')
611
+ + (delegated && o.args.includes('--no-supervise') ? ' --no-supervise' : '');
553
612
  const script = [
554
613
  'set -u',
555
614
  o.grokCredential === 'subscription'
@@ -557,7 +616,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
557
616
  : o.grokCredential === 'api_key'
558
617
  ? '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'
559
618
  : 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()}`,
560
- 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)"',
619
+ 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)"',
561
620
  // 🔑 스킬 키(.env) — 이미지엔 없다. 이 런의 Secret 에서 각 스킬 폴더로 0600 복사(값은 로그에 안 나온다).
562
621
  ...(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(',')}"`] : []),
563
622
  'git config --global user.name "elanous pod child" && git config --global user.email "noreply@anthropic.com" && gh auth setup-git',
@@ -570,9 +629,11 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
570
629
  `(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=$!`,
571
630
  ...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
572
631
  // 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
573
- goalPath
632
+ o.authorSentence
633
+ ? `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{}'`
634
+ : goalPath
574
635
  ? `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{}'`
575
- : `elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
636
+ : `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1; rc=$?`,
576
637
  'cat /tmp/si.out',
577
638
  '[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
578
639
  // A long run's log export can pass the 5MB artifact limit; skipping it whole left no window into why a
@@ -735,7 +796,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
735
796
  catch (error) { debug.log('self-implement.pod', 'memory-advice-unavailable', { spaceId: input.spaceId, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
736
797
  const { limit: selectedMemoryLimit, tier: memoryTier, source: memorySource } = podMemoryLimitFor(input.feature, env, parentGoal, useAdvice && !explicitMemory ? advice : undefined);
737
798
  const requestKind = broadPodTestWarning(input.feature) ? 'test' : goalTypeOf(input.feature)
738
- ?? (parentGoal ? (broadPodTestWarning(parentGoal) ? 'test' : goalTypeOf(parentGoal)) : null);
799
+ ?? (parentGoal ? (broadPodTestWarning(parentGoal) ? 'test' : goalTypeOf(parentGoal)) : null)
800
+ ?? ((parsePodGoalType(env.ELANOUS_POD_GOAL_TYPE) ?? declaredGoalType(input.feature) ?? declaredGoalType(parentGoal ?? '')) === 'document' ? 'docs' : null);
739
801
  for (const [where, text] of [['feature', input.feature], ['parent-goal', parentGoal]] as const) {
740
802
  if (text && text.split(/\r?\n/u).some((line) => /Pod 메모리/u.test(line) && !GOAL_LINE.test(line))) {
741
803
  const reason = '한 줄 단독 `Pod 메모리: high|standard` 형식이 아님';
@@ -749,9 +811,14 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
749
811
  const after = input.after ?? podPredecessorFor(input.feature, env, parentGoal);
750
812
  if (after !== undefined) debug.log('self-implement.pod', 'predecessor', { spaceId: input.spaceId, after, source: input.after !== undefined ? 'input' : 'goal-or-option' });
751
813
  let oomRetried = false;
814
+ let retryTier: PodMemoryTier = memoryTier === 'lite' ? 'standard' : 'high';
752
815
  let retryFromChildRunId: string | undefined;
753
816
  let retryFromMemoryLimit: string | undefined;
754
- debug.log('self-implement.pod', 'memory-limit', { spaceId: input.spaceId, goalType: requestKind, memoryLimit, tier: memoryTier, source: memorySource,
817
+ const selectedGoalType = parsePodGoalType(env.ELANOUS_POD_GOAL_TYPE) ?? declaredGoalType(input.feature) ?? declaredGoalType(parentGoal ?? '');
818
+ const memoryReason = memorySource === 'option' && ['explicit-option', 'goal-type-default', 'existing-tier'].includes(env.ELANOUS_POD_MEMORY_REASON ?? '')
819
+ ? env.ELANOUS_POD_MEMORY_REASON!
820
+ : memorySource === 'goal-type-auto' ? `declared goal type ${selectedGoalType}` : memorySource;
821
+ debug.log('self-implement.pod', 'memory-limit', { spaceId: input.spaceId, goalType: requestKind, declaredGoalType: selectedGoalType, memoryLimit, tier: memoryTier, source: memorySource, reason: memoryReason,
755
822
  ...(memorySource.startsWith('parent-') ? { inheritedFrom } : {}), ...(parentGoalUnreadable ? { parentGoal: 'unreadable' } : {}) });
756
823
  const childRunId = mintRunId();
757
824
  const executionKey = createHash('sha256').update(JSON.stringify({ spaceId: input.spaceId, feature: input.feature, base: input.base, autoMerge: input.autoMerge, autoReview: input.autoReview, openPr: input.openPr, draft: input.draft })).digest('hex');
@@ -769,6 +836,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
769
836
  if (typeof intent.data.to !== 'string' || memoryGi(intent.data.to) === null) throw new Error('invalid OOM retry memory limit');
770
837
  oomRetried = true;
771
838
  memoryLimit = intent.data.to;
839
+ retryTier = parsePodMemoryTier(typeof intent.data.tier === 'string' ? intent.data.tier : undefined)
840
+ ?? ((memoryGi(intent.data.to) ?? 0) >= (memoryGi(podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit) ?? Infinity) ? 'high' : 'standard');
772
841
  retryFromChildRunId = typeof intent.data.fromChildRunId === 'string' ? intent.data.fromChildRunId : undefined;
773
842
  retryFromMemoryLimit = typeof intent.data.from === 'string' ? intent.data.from : undefined;
774
843
  }
@@ -783,7 +852,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
783
852
  ...(input.draft === false ? ['--no-draft'] : []),
784
853
  ...(hostSupervised ? ['--no-supervise'] : []),
785
854
  ...(options.extraArgs ?? []),
786
- ...(options.provider === 'grok' ? ['--child-llm-provider', 'grok', '--child-llm-model', defaultGrokModel().id] : []),
855
+ ...podChildLlmArgs(options),
787
856
  ];
788
857
  debug.log('pod.self-implement', 'child-supervise', { hostSupervised, childSupervise: !hostSupervised });
789
858
  const done = (async (): Promise<SelfImplementJobDone> => {
@@ -800,7 +869,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
800
869
  if (!options.pool) return null;
801
870
  for (;;) {
802
871
  if (input.signal?.aborted) return null;
803
- const acquired = options.pool.tryAcquire();
872
+ const acquired = await options.pool.tryAcquire();
804
873
  if (acquired) {
805
874
  debug.log('self-implement.pod', 'pool-slot', { spaceId: input.spaceId, context: acquired.context, inflight: options.pool.snapshot() });
806
875
  return acquired;
@@ -868,6 +937,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
868
937
  let retrySamples: ReturnType<typeof parseMemSamples> = [];
869
938
  let failedAccount: string | undefined;
870
939
  let usageLimitRetried = false;
940
+ let ghLoginRetried = false;
871
941
  for (;;) {
872
942
  try {
873
943
  const grok = options.provider === 'grok';
@@ -930,10 +1000,10 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
930
1000
  debug.log('self-implement.pod', 'credential-relay-skipped', { reason: 'no-host-url' });
931
1001
  }
932
1002
  const now = options.now ?? Date.now;
933
- const issueVerifiedAppCredential = async (): Promise<{ token: string; expiresAt: number } | null> => {
1003
+ const issueVerifiedAppCredential = async (fresh = false): Promise<{ token: string; expiresAt: number } | null> => {
934
1004
  if (!repository) return null;
935
1005
  // This is the same repository-scoped, contents+pull_requests installation request on launch and refresh.
936
- const candidate = (options.githubInstallation ?? ((repo) => githubInstallationCredential({ scope: { repository: repo.split('/')[1]! } })))(repository);
1006
+ const candidate = (options.githubInstallation ?? ((repo, o) => coalescedInstallationCredential({ repository: repo.split('/')[1]! }, { fresh: o?.fresh })))(repository, { fresh });
937
1007
  if (!candidate?.token) return null;
938
1008
  const expiry = candidate.expires_at ?? ('expiresAt' in candidate ? candidate.expiresAt : undefined);
939
1009
  const expiresAt = typeof expiry === 'number' ? expiry : Date.parse(expiry ?? '');
@@ -943,8 +1013,13 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
943
1013
  ? { token: candidate.token, expiresAt } : null;
944
1014
  };
945
1015
  let appCredential: Awaited<ReturnType<typeof issueVerifiedAppCredential>> = null;
946
- try { appCredential = await issueVerifiedAppCredential(); }
1016
+ try { appCredential = await issueVerifiedAppCredential(ghLoginRetried); }
947
1017
  catch { debug.log('self-implement.pod', 'github-app-unavailable', { job: name, reason: 'mint-or-verification-failed' }); }
1018
+ // PODCRED1: a short token is still shipped — the host renews it in the Pod on the first live poll — but it is
1019
+ // observed, so an exit-7 burst can be checked against the remaining life the Pods actually started with.
1020
+ if (appCredential && appCredential.expiresAt - now() < POD_APP_TOKEN_MIN_START_MS) {
1021
+ debug.log('self-implement.pod', 'github-app-short-lived', { job: name, expiresAt: new Date(appCredential.expiresAt).toISOString(), requiredSeconds: POD_APP_TOKEN_MIN_START_MS / 1000 });
1022
+ }
948
1023
  const githubRelay = appCredential && groundingUrl && repository
949
1024
  ? await mint({ runId: groundingRunId, job: name, ttlMs: groundingTtlMs, scope: 'gh-credential', repository })
950
1025
  : null;
@@ -1042,7 +1117,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1042
1117
  // 🩸 종전엔 여기서 delete 부터 해서, 재개가 원격에서 멀쩡히 돌던 Job 을 죽였다. 실패로 끝난 Job 만 지우고 다시 만든다.
1043
1118
  // ☸️ 노드 레지스트리에 이 판이 있으면 그것을 pull 한다(델타 · 대표 2026-09-26) — 라벨 판정은 로컬 이미지 이름으로 한다.
1044
1119
  const jobImage = member?.imageRef ?? image;
1045
- const launchRunId = retryAccount || oomRetried ? mintRunId() : childRunId;
1120
+ const launchRunId = retryAccount || oomRetried || ghLoginRetried ? mintRunId() : childRunId;
1046
1121
  let liveChildRunId = launchRunId;
1047
1122
  let reattachedMemoryLimit: string | undefined;
1048
1123
  let existingExecutionKey: string | undefined;
@@ -1083,7 +1158,10 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1083
1158
  }
1084
1159
  if (failedExisting) {
1085
1160
  const pods = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${name}`, '-o', 'jsonpath={range .items[*]}{.metadata.creationTimestamp}{"\\t"}{range .status.containerStatuses[?(@.name=="child")]}{.state.terminated.reason}{"\\t"}{.state.terminated.exitCode}{end}{"\\n"}{end}']);
1086
- const failedOom = pods.status === 0 && pods.stdout.split('\n').some((line) => { const fields = line.split('\t'); return fields[1] === 'OOMKilled' && fields[2]?.trim() === '137'; });
1161
+ const latestPod = pods.status === 0 ? pods.stdout.trimEnd().split('\n').map((line) => line.split('\t'))
1162
+ .filter(([created]) => created && !Number.isNaN(Date.parse(created)))
1163
+ .sort((a, b) => Date.parse(b[0]!) - Date.parse(a[0]!))[0] : undefined;
1164
+ const failedOom = latestPod?.[1]?.trim() === 'OOMKilled' && latestPod[2]?.trim() === '137';
1087
1165
  // Once attempt 2 exists, consume its terminal result even if it failed for a non-OOM reason.
1088
1166
  // Only the old, lower-memory attempt may be removed to make room for high.
1089
1167
  if (failedOom || oomRetried) reattach = !oomRetried || (memoryGi(reattachedMemoryLimit ?? '') ?? 0) >= (memoryGi(memoryLimit) ?? Infinity);
@@ -1096,21 +1174,22 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1096
1174
  debug.log('self-implement.pod', 'job-reattach', { job: name, conditions: existingConditions.join(' ') || 'running', spaceId: input.spaceId });
1097
1175
  }
1098
1176
  if (!hostRefresh && ghTokenExpiresAt === null && !githubRelay) debug.log('self-implement.pod', 'gh-token-app-skipped', { job: name, reason: 'no-verified-app-or-refresh-path' });
1177
+ let appliedMemoryRequest: string | undefined;
1099
1178
  if (!reattach) {
1100
1179
  if (oomRetried && hasExistingJob && !failedExisting && memoryGi(reattachedMemoryLimit ?? '') === null) {
1101
1180
  return { exitCode: 1, output: '', error: { code: 'pod-memory-unavailable', message: `Job ${name} 실제 메모리 한도를 확인할 수 없다` } };
1102
1181
  }
1103
1182
  if (oomRetried && !retryAccount) {
1104
- const high = podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit;
1105
- if ((memoryGi(memoryLimit) ?? 0) < (memoryGi(high) ?? Infinity)) throw new Error(`pod: OOM 재시도 한도가 high 와 다름: ${memoryLimit}`);
1183
+ const retryLimit = podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: retryTier }).limit;
1184
+ if ((memoryGi(memoryLimit) ?? 0) < (memoryGi(retryLimit) ?? Infinity)) throw new Error(`pod: OOM 재시도 한도가 ${retryTier} 와 다름: ${memoryLimit}`);
1106
1185
  const leaseMembers = member ? [member] : [{ context, capacity: 1, k3dCluster: '' }];
1107
1186
  const lease = recommendConcurrency(measurePoolLease(leaseMembers, { kubectl: (args) => baseKubectl(args) }), {
1108
- capacity: leaseMembers.reduce((sum, item) => sum + item.capacity, 0), perGoalMemory: high, accounts: 0, perAccount: 0,
1187
+ capacity: leaseMembers.reduce((sum, item) => sum + item.capacity, 0), perGoalMemory: retryLimit, accounts: 0, perAccount: 0,
1109
1188
  });
1110
1189
  if (lease.recommended === null || lease.placeableSlots === null || lease.placeableSlots < 1 || lease.memorySlots === null || lease.memorySlots < 1) {
1111
- return { exitCode: 1, output: '', error: { code: 'pod-lease', message: `OOM 재시도 high 자리 판정 실패: ${lease.reason ?? lease.limitedBy ?? 'no capacity'}\n${formatLastMemSample(retrySamples.at(-1))}` } };
1190
+ return { exitCode: 1, output: '', error: { code: 'pod-lease', message: `OOM 재시도 ${retryTier} 자리 판정 실패: ${lease.reason ?? lease.limitedBy ?? 'no capacity'}\n${formatLastMemSample(retrySamples.at(-1))}` } };
1112
1191
  }
1113
- debug.log('self-implement.pod', 'oom-retry-lease', { job: name, context, memoryLimit: high, recommended: lease.recommended });
1192
+ debug.log('self-implement.pod', 'oom-retry-lease', { job: name, context, memoryLimit: retryLimit, tier: retryTier, recommended: lease.recommended });
1114
1193
  }
1115
1194
  if (failedExisting && !reattach && !oomRetried && collectFullLogs('failed-job-logs-unavailable').status === 'incomplete') {
1116
1195
  return { exitCode: 1, output: '', error: { code: 'pod-collection-incomplete', message: `Job ${name} recovery incomplete — old Job retained` } };
@@ -1146,8 +1225,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1146
1225
  const measuredRequest = podMemoryRequestFor(requestKind, advice);
1147
1226
  const requestGi = memoryGi(measuredRequest);
1148
1227
  const limitGi = memoryGi(memoryLimit);
1149
- const memoryRequest = requestGi !== null && limitGi !== null && requestGi > limitGi ? memoryLimit : measuredRequest;
1150
- 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 를 부르지 않는다
1228
+ const documentDefaultLite = !oomRetried && selectedGoalType === 'document' && memoryTier === 'lite'
1229
+ && (memorySource === 'goal-type-auto' || memoryReason === 'goal-type-default');
1230
+ const memoryRequest = documentDefaultLite ? memoryLimit
1231
+ : requestGi !== null && limitGi !== null && requestGi > limitGi ? memoryLimit : measuredRequest;
1232
+ appliedMemoryRequest = memoryRequest;
1233
+ debug.log('self-implement.pod', 'memory-request', { spaceId: input.spaceId, job: name,
1234
+ tier: oomRetried ? retryTier : memoryTier, reason: oomRetried ? 'OOMKilled' : memoryReason,
1235
+ memoryLimit, memoryRequest });
1236
+ 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 } : {}), 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 를 부르지 않는다
1151
1237
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
1152
1238
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
1153
1239
  releaseAdmission?.applied?.(name, context, namespace);
@@ -1183,7 +1269,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1183
1269
  }
1184
1270
  try {
1185
1271
  appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-child-run', data: { childRunId: liveChildRunId, job: name, attempt: oomRetried ? 2 : 1, ...(goalFile ? { goalFile } : {}) } }, ledgerDir);
1186
- if (oomRetried && retryFromChildRunId) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry', data: { attempt: 2, from: retryFromMemoryLimit ?? selectedMemoryLimit, to: memoryLimit, job: name, childRunId: liveChildRunId, fromChildRunId: retryFromChildRunId } }, ledgerDir);
1272
+ appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-memory-selected', data: { job: name, childRunId: liveChildRunId, attempt: oomRetried ? 2 : 1, limit: memoryLimit, ...(appliedMemoryRequest ? { request: appliedMemoryRequest } : {}), tier: oomRetried ? retryTier : memoryTier, source: oomRetried ? 'oom-retry' : memorySource, reason: oomRetried ? 'OOMKilled' : memoryReason, declaredGoalType: selectedGoalType } }, ledgerDir);
1273
+ if (oomRetried && retryFromChildRunId) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry', data: { attempt: 2, from: retryFromMemoryLimit ?? selectedMemoryLimit, to: memoryLimit, tier: retryTier, reason: 'OOMKilled', job: name, childRunId: liveChildRunId, fromChildRunId: retryFromChildRunId } }, ledgerDir);
1187
1274
  } catch (error) {
1188
1275
  const reason = error instanceof Error ? error.message : String(error);
1189
1276
  debug.log('self-implement.pod', 'parent-ledger-unavailable', { job: name, reason, attempt: oomRetried ? 2 : 1 });
@@ -1293,6 +1380,21 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1293
1380
  const parsed = parseSelfImplementJson(logs);
1294
1381
  const terminal = parseSelfImplementJson(logs.trimEnd().split('\n').at(-1) ?? '');
1295
1382
  const nextAccount = codexAccounts?.find((candidate) => candidate !== account);
1383
+ if (state === 'failed' && containerExitCode === 7 && !input.signal?.aborted) {
1384
+ debug.log('self-implement.pod', 'gh-login-failed', { job: name, attempt: ghLoginRetried ? 2 : 1, tokenExpiresInSec: ghTokenExpiresAt === null ? null : Math.floor((ghTokenExpiresAt - now()) / 1000) });
1385
+ if (!ghLoginRetried) {
1386
+ const deleted = kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found', '--wait=true']);
1387
+ if (deleted.status !== 0) throw new Error(`pod: GitHub 로그인 실패 Job 삭제 실패: ${deleted.stderr.trim()}`);
1388
+ // Bursts came from simultaneous launches (10-05 12:49Z · 5 deaths in 2 s) — wait, spread by job name, then
1389
+ // retry once with a newly issued token so the retries do not collide again.
1390
+ const backoffMs = podGhLoginRetryBackoffMs(name);
1391
+ debug.log('self-implement.pod', 'gh-login-retry-backoff', { job: name, backoffMs });
1392
+ await sleep(backoffMs);
1393
+ if (input.signal?.aborted) return { exitCode: null, output: '', error: { code: 'aborted', message: 'aborted before GitHub login retry' } };
1394
+ ghLoginRetried = true;
1395
+ continue;
1396
+ }
1397
+ }
1296
1398
  if (!usageLimitRetried && !input.signal?.aborted && state !== 'aborted' && terminal?.ok === false
1297
1399
  && /\b429\b/.test(terminal.error ?? '') && /\busage_limit_reached\b/.test(terminal.error ?? '') && nextAccount) {
1298
1400
  debug.log('self-implement.pod', 'usage-limit-retry', { job: name, spaceId: input.spaceId, from: account, to: nextAccount });
@@ -1316,7 +1418,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1316
1418
  try {
1317
1419
  const hostRoot = findGitDir(process.cwd())?.root;
1318
1420
  if (!hostRoot) throw new Error('host repository unavailable');
1319
- regate = await (options.hostRegate ?? runHostRegate)({ prNumber, headCommit, repoRoot: hostRoot });
1421
+ regate = await (options.hostRegate ?? runHostRegate)({ prNumber, headCommit, repoRoot: hostRoot, ...(goalFile ? { goalFile } : {}) });
1320
1422
  } catch (error) {
1321
1423
  debug.log('harness.host-regate', 'unmeasured', { pr: prNumber, files: [], os: process.platform, failedStep: 'host-regate' });
1322
1424
  regate = { passed: false, failures: [{ step: 'host-regate', detail: error instanceof Error ? error.message : String(error) }], os: process.platform };
@@ -1335,7 +1437,16 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1335
1437
  }
1336
1438
  const childFailure = state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
1337
1439
  ? lastPodChildFailure(logs) : null;
1440
+ let noResultDiagnostic: string | null = null;
1441
+ if (state === 'failed' && containerExitCode !== null && containerExitCode !== 0 && !disposition && !oomKilled && failedReason !== 'DeadlineExceeded' && failedReason !== 'BackoffLimitExceeded') {
1442
+ try { noResultDiagnostic = podNoResultDiagnostic(readFileSync(runLedgerPath(liveChildRunId, ledgerDir), 'utf8')); }
1443
+ catch { /* ledger not returned: do not invent a stage */ }
1444
+ noResultDiagnostic ??= 'child terminal result missing; last ledger stage=unknown; round=unknown; mustFix=unknown';
1445
+ }
1338
1446
  if (childFailure) debug.log('self-implement.pod', 'child-error', { job: name, stage: childFailure.stage, error: childFailure.error }, { compact: { stringMax: 500 } });
1447
+ if (noResultDiagnostic && !noResultDiagnostic.includes('stage=unknown')) {
1448
+ debug.log('self-implement.pod', 'no-result-ledger-diagnostic', { job: name, childRunId: liveChildRunId, diagnostic: noResultDiagnostic }, { compact: { stringMax: 500 } });
1449
+ }
1339
1450
  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,
1340
1451
  ...(state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
1341
1452
  ? { ...(childFailure ? { childStage: childFailure.stage } : {}), childError: childFailure?.error ?? 'no-result-line' } : {}) },
@@ -1344,22 +1455,25 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1344
1455
  return { exitCode: 1, output: '', error: { code: 'pod-memory-unavailable', message: `Job ${name} 실제 메모리 한도를 확인할 수 없다` } };
1345
1456
  }
1346
1457
  if (oomKilled && failedReason !== 'DeadlineExceeded' && !input.signal?.aborted && !oomRetried) {
1347
- const high = podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit;
1348
1458
  const actual = memoryGi(reattachedMemoryLimit ?? memoryLimit);
1459
+ const standardLimit = podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'standard' }).limit;
1460
+ const nextTier: PodMemoryTier = actual !== null && actual < (memoryGi(standardLimit) ?? 0) ? 'standard' : 'high';
1461
+ const nextLimit = podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: nextTier }).limit;
1349
1462
  if (actual === null) {
1350
1463
  return { exitCode: 1, output: '', error: { code: 'pod-memory-unavailable', message: `Job ${name} 실제 메모리 한도를 판정할 수 없다: ${memoryLimit}` } };
1351
1464
  }
1352
- if (actual < (memoryGi(high) ?? actual)) {
1465
+ if (actual < (memoryGi(nextLimit) ?? actual)) {
1353
1466
  const from = reattachedMemoryLimit ?? memoryLimit;
1354
- appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry-intent', data: { attempt: 2, from, to: high, job: name, executionKey, fromChildRunId: liveChildRunId } }, ledgerDir);
1355
- debug.log('self-implement.pod', 'oom-retry', { from, to: high, job: name });
1467
+ appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry-intent', data: { attempt: 2, from, to: nextLimit, tier: nextTier, reason: 'OOMKilled', job: name, executionKey, fromChildRunId: liveChildRunId } }, ledgerDir);
1468
+ debug.log('self-implement.pod', 'oom-retry', { from, to: nextLimit, tier: nextTier, reason: 'OOMKilled', job: name });
1356
1469
  const deleted = kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found', '--wait=true']);
1357
1470
  if (deleted.status !== 0) throw new Error(`pod: OOM Job 삭제 실패: ${deleted.stderr.trim()}`);
1358
1471
  oomRetried = true;
1359
1472
  retryFromChildRunId = liveChildRunId;
1360
1473
  retryFromMemoryLimit = from;
1361
1474
  retrySamples = samples;
1362
- memoryLimit = high;
1475
+ memoryLimit = nextLimit;
1476
+ retryTier = nextTier;
1363
1477
  if (member) { options.pool!.release(member); member = null; }
1364
1478
  releaseAdmission?.();
1365
1479
  releaseAdmission = undefined;
@@ -1378,7 +1492,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1378
1492
  const record: GoalExecutionRecord = {
1379
1493
  runId: liveChildRunId,
1380
1494
  stage: (state === 'aborted' || !disposition ? 'aborted' : disposition.stage) as GoalExecutionRecord['stage'],
1381
- outcome: (!disposition ? 'pod-no-result' : state === 'complete' && disposition.merged ? 'merged' : state === 'complete' && disposition.ok ? 'completed' : 'abandoned') as GoalExecutionRecord['outcome'],
1495
+ outcome: (!disposition ? noResultDiagnostic ? 'abandoned' : 'pod-no-result' : state === 'complete' && disposition.merged ? 'merged' : state === 'complete' && disposition.ok ? 'completed' : 'abandoned') as GoalExecutionRecord['outcome'],
1382
1496
  ok: state === 'complete' && (disposition?.ok ?? false),
1383
1497
  ...(disposition?.prNumber !== undefined ? { prNumber: disposition.prNumber } : {}),
1384
1498
  completedAt: new Date().toISOString(),
@@ -1402,16 +1516,22 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1402
1516
  const finishedDisposition = deadlineExceeded && disposition
1403
1517
  ? { ...disposition, failureClassification: 'run-deadline-exceeded' as const }
1404
1518
  : disposition;
1519
+ const podExitCode = state === 'complete' && disposition?.stage !== 'host-regate-failed' && disposition?.ok !== false ? 0 : 1;
1520
+ // SCHED-CONTRACT: a failed child must say why outside the Pod, not «no error diagnostic».
1521
+ const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds }) : undefined;
1522
+ if (podReason) debug.log('self-implement.pod', 'failure-reason', { job: name, reason: podReason });
1405
1523
  return {
1406
- exitCode: state === 'complete' && disposition?.stage !== 'host-regate-failed' && disposition?.ok !== false ? 0 : 1,
1524
+ exitCode: podExitCode,
1407
1525
  output: tail,
1408
1526
  ...(deadlineExceeded
1409
1527
  ? { error: { code: 'pod-deadline-exceeded', message: `Job ${name} 이 수명 상한 ${deadlineSeconds}초에 닿았다` } }
1410
1528
  : state === 'failed'
1411
- ? oomKilled
1412
- ? { 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 · high 에서도' : ''}\n${formatLastMemSample(samples.at(-1))}` } }
1413
- : { 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'}` } }
1414
- : {}),
1529
+ ? containerExitCode === 7
1530
+ ? { error: { code: 'pod-gh-login-failed', message: `Job ${name} GitHub 앱 로그인 실패 (container=${containerReason ?? 'unknown'}/7, attempt=${ghLoginRetried ? 2 : 1}) — 새 토큰으로 ${ghLoginRetried ? '1회 재시도했으나 다시 실패' : '재시도 불가'}` } }
1531
+ : oomKilled
1532
+ ? { 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))}` } }
1533
+ : { 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}` : ''}` } }
1534
+ : podReason && !childFailure?.error ? { error: { code: 'pod-child-failed', message: podReason } } : {}),
1415
1535
  ...(finishedDisposition ? { disposition: finishedDisposition } : {}),
1416
1536
  };
1417
1537
  } catch (err) {
@@ -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';
@@ -49,6 +50,7 @@ export interface SelfImplementDisposition {
49
50
  ok?: boolean;
50
51
  worktreePath?: string;
51
52
  branch?: string;
53
+ harvestable?: true;
52
54
  prUrl?: string;
53
55
  prNumber?: number;
54
56
  /** Head SHA checked by the Pod before host regate. */
@@ -84,6 +86,7 @@ export interface SelfImplementDisposition {
84
86
  reviewVerdict?: 'pass' | 'warn' | 'fail';
85
87
  /** 마지막 리뷰의 must-fix 수(`review.mustFix.length`). 같은 조건. */
86
88
  reviewMustFixCount?: number;
89
+ mustFix?: string[];
87
90
  }
88
91
 
89
92
  /**
@@ -265,7 +268,7 @@ export function createSelfImplementAdapter(opts: SelfImplementAdapterOptions) {
265
268
  : r.output.length === 0
266
269
  ? `${exitMessage} (child produced no output)`
267
270
  : r.disposition
268
- ? `${exitMessage} (child terminal result contained no error diagnostic)`
271
+ ? `${exitMessage} (child stage=${r.disposition.stage ?? 'unknown'}; ${r.disposition.detail ?? `mustFix=${JSON.stringify(r.disposition.mustFix ?? 'unknown')}`})`
269
272
  : `${exitMessage} (child output did not contain a parseable terminal JSON diagnostic)`;
270
273
  return {
271
274
  ...exec,
@@ -354,6 +357,7 @@ export function defaultSelfImplementSpawn(): SelfImplementJobSpawn {
354
357
  ...childNestEnv(),
355
358
  ...harnessSpaceEnv('self-implement', input.spaceId),
356
359
  ...executorRoleEnv(), // ⭐ 세포 role 분화 — 자식은 executor(실행자)로 자기인지.
360
+ ...oldDoorInternalEnv('self-implement'),
357
361
  },
358
362
  stdio: ['ignore', 'pipe', 'pipe'],
359
363
  });
@@ -461,6 +465,7 @@ export function parseSelfImplementJson(stdout: string): SelfImplementDisposition
461
465
  ...(typeof o.ok === 'boolean' ? { ok: o.ok } : {}),
462
466
  ...(typeof o.worktreePath === 'string' ? { worktreePath: o.worktreePath } : {}),
463
467
  ...(typeof o.branch === 'string' ? { branch: o.branch } : {}),
468
+ ...(o.harvestable === true && typeof o.branch === 'string' && o.branch.trim() ? { harvestable: true } : {}),
464
469
  ...(typeof o.prUrl === 'string' ? { prUrl: o.prUrl } : {}),
465
470
  ...(typeof o.prNumber === 'number' ? { prNumber: o.prNumber } : {}),
466
471
  ...(typeof o.checkedHeadCommit === 'string' ? { checkedHeadCommit: o.checkedHeadCommit } : {}),
@@ -483,6 +488,7 @@ export function parseSelfImplementJson(stdout: string): SelfImplementDisposition
483
488
  ? { providerErrors: o.providerErrors as SelfImplementDisposition['providerErrors'] } : {}),
484
489
  ...(typeof o.runId === 'string' && o.runId.trim() ? { runId: o.runId, childRunId: o.runId } : {}),
485
490
  ...reviewGateFields(o),
491
+ ...(Array.isArray(o.mustFix) && o.mustFix.every((item) => typeof item === 'string') ? { mustFix: o.mustFix as string[] } : {}),
486
492
  };
487
493
  reportUnmappedDispositionFields(o, mapped);
488
494
  return mapped;
@@ -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
  }