elanous 0.2.15 → 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 (254) 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-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -364,7 +364,61 @@ export function decideProviderFallback(input: {
364
364
  return { action: 'advance', nextProvider, category, attempts };
365
365
  }
366
366
 
367
- export type RetryAction = 'retry' | 'abort' | 'ask-user' | 'auto-undo';
367
+ /** Consecutive overload/5xx/rate-limit outcomes that switch the child off codex
368
+ * before the same provider spends its remaining retries. Other errors (400)
369
+ * never count. Recovery is this many later non-overload outcomes. */
370
+ export const OVERLOAD_FAILOVER_STREAK = 3;
371
+
372
+ export type LlmCallOutcomeKind = 'ok' | 'overloaded' | '5xx' | 'rate-limit' | 'other';
373
+
374
+ /** One finished LLM call. Launch failover reads only this shape. */
375
+ export interface LlmCallOutcome {
376
+ readonly provider: string;
377
+ readonly status: number;
378
+ readonly kind: LlmCallOutcomeKind;
379
+ }
380
+
381
+ const CODEX_CALL_PROVIDERS = new Set(['codex', 'openai-codex']);
382
+
383
+ export function llmCallOutcomeKind(status: number, message = ''): LlmCallOutcomeKind {
384
+ if (status >= 200 && status < 300) return 'ok';
385
+ const msg = message.toLowerCase();
386
+ if (status === 429 || msg.includes('rate limit') || msg.includes('too many requests')) return 'rate-limit';
387
+ if (
388
+ status === 500 || status === 502 || status === 503 || status === 504 ||
389
+ msg.includes('overloaded') || msg.includes('server_busy') || msg.includes('server_error')
390
+ ) {
391
+ return status >= 500 ? '5xx' : 'overloaded';
392
+ }
393
+ if (msg.includes('overloaded')) return 'overloaded';
394
+ return 'other';
395
+ }
396
+
397
+ export function isOverloadOutcomeKind(kind: LlmCallOutcomeKind): boolean {
398
+ return kind === 'overloaded' || kind === '5xx' || kind === 'rate-limit';
399
+ }
400
+
401
+ /** True when the newest `limit` codex outcomes are all overload-class.
402
+ * A non-codex row or a non-overload row breaks the streak. Empty input is false. */
403
+ export function codexOverloadStreakReached(
404
+ outcomes: readonly LlmCallOutcome[],
405
+ limit = OVERLOAD_FAILOVER_STREAK,
406
+ ): boolean {
407
+ if (limit < 1 || outcomes.length < limit) return false;
408
+ const tail = outcomes.slice(-limit);
409
+ return tail.every((row) => CODEX_CALL_PROVIDERS.has(row.provider) && isOverloadOutcomeKind(row.kind));
410
+ }
411
+
412
+ /** True when the newest `limit` outcomes are successful (any provider). */
413
+ export function overloadRecovered(
414
+ outcomes: readonly LlmCallOutcome[],
415
+ limit = OVERLOAD_FAILOVER_STREAK,
416
+ ): boolean {
417
+ if (limit < 1 || outcomes.length < limit) return false;
418
+ return outcomes.slice(-limit).every((row) => row.kind === 'ok');
419
+ }
420
+
421
+ export type RetryAction = 'retry' | 'abort' | 'ask-user' | 'auto-undo' | 'switch-provider';
368
422
 
369
423
  export interface RetryDecision {
370
424
  action: RetryAction;
@@ -417,6 +471,16 @@ export interface RetryContext {
417
471
  /** Optional: retry-after header string from a 429 response (ms or
418
472
  * seconds or HTTP date). decideRetry parses it. */
419
473
  retryAfter?: string;
474
+ /** Configured fallbacks still unused. When > 0, an overload-class
475
+ * failure that has already repeated `overloadStreak` times switches
476
+ * provider instead of spending another retry on the same one.
477
+ * Omitted or 0 keeps the historical retry-until-exhausted behaviour. */
478
+ remainingFallbacks?: number;
479
+ /** Consecutive overload-class failures already seen for this call,
480
+ * including the one under decision. Default threshold is
481
+ * OVERLOAD_FAILOVER_STREAK. */
482
+ overloadStreak?: number;
483
+ overloadStreakLimit?: number;
420
484
  }
421
485
 
422
486
  /** Classify an error into a RetryCategory without making an action
@@ -519,10 +583,42 @@ export function classifyError(err: unknown): RetryCategory {
519
583
  return 'unknown';
520
584
  }
521
585
 
586
+ function isOverloadSwitchCandidate(msg: string, code: string | null): boolean {
587
+ return (
588
+ code === '429' || code === '500' || code === '502' || code === '503' || code === '504' ||
589
+ msg.includes('rate limit') || msg.includes('too many requests') ||
590
+ msg.includes('overloaded') || msg.includes('server_busy') || msg.includes('server_error')
591
+ );
592
+ }
593
+
594
+ function overloadSwitchDecision(ctx: RetryContext): RetryDecision | undefined {
595
+ const remaining = ctx.remainingFallbacks ?? 0;
596
+ if (remaining <= 0) return undefined;
597
+ const limit = ctx.overloadStreakLimit ?? OVERLOAD_FAILOVER_STREAK;
598
+ const streak = ctx.overloadStreak ?? 1;
599
+ if (streak < limit) return undefined;
600
+ return {
601
+ action: 'switch-provider',
602
+ category: 'overloaded',
603
+ reason: `overload streak ${streak} with ${remaining} fallback(s) left; switch before spending retries`,
604
+ };
605
+ }
606
+
522
607
  /** Pure verdict. Caller sleeps + retries or aborts per the returned
523
608
  * action. Exponential backoff baseline: 500ms * 2^attempt, clamped
524
609
  * to [500, 30_000]. */
525
610
  export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
611
+ const msg = extractMessage(err).toLowerCase();
612
+ const code = extractErrorCode(err);
613
+
614
+ // Overload with a fallback still unused switches before the doom
615
+ // window (k === 3) turns the same 503 into auto-undo. A 400 is not
616
+ // a candidate. Doom still wins for every other repeated failure.
617
+ if (isOverloadSwitchCandidate(msg, code)) {
618
+ const switched = overloadSwitchDecision(ctx);
619
+ if (switched) return switched;
620
+ }
621
+
526
622
  // Doom-loop always wins — even a technically-retryable error
527
623
  // becomes auto-undo after N identical failures. The caller may
528
624
  // still degrade to ask-user when plan mode is active or when the
@@ -535,9 +631,6 @@ export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
535
631
  };
536
632
  }
537
633
 
538
- const msg = extractMessage(err).toLowerCase();
539
- const code = extractErrorCode(err);
540
-
541
634
  // Context window exhausted — never retry (the next call will also
542
635
  // be over-sized). Abort and surface the error to the caller; upstream
543
636
  // can trigger compaction.
@@ -589,8 +682,16 @@ export function decideRetry(err: unknown, ctx: RetryContext): RetryDecision {
589
682
  };
590
683
  }
591
684
 
592
- // Overloaded / server busy.
593
- if (msg.includes('overloaded') || msg.includes('server_busy') || code === '503') {
685
+ // Overloaded / server busy / 5xx.
686
+ if (
687
+ msg.includes('overloaded') ||
688
+ msg.includes('server_busy') ||
689
+ msg.includes('server_error') ||
690
+ code === '500' ||
691
+ code === '502' ||
692
+ code === '503' ||
693
+ code === '504'
694
+ ) {
594
695
  return {
595
696
  action: 'retry',
596
697
  delayMs: backoffMs(ctx.attempt),
@@ -57,7 +57,22 @@ export async function recordWorkingBackwardsOnCards(
57
57
  debug.log('steward.working-backwards', 'skipped-launched-before-draft', { issueId: issue.identifier });
58
58
  continue;
59
59
  }
60
- const draft = await draftWorkingBackwards(issue, ask);
60
+ // An invalid draft for the same issue text is not re-asked every tick — each ask is a 70-100 s LLM call (10-05 ELA-29 timed the schedule stage out 3×).
61
+ const invalidKey = `hitl:draft-invalid-${createHash('sha256').update(JSON.stringify({ title: issue.title, body: issue.body ?? '' })).digest('hex').slice(0, 16)}`;
62
+ if (card.sections.some(section => section.key === invalidKey)) {
63
+ debug.log('steward.working-backwards', 'skipped-known-invalid', { issueId: issue.identifier });
64
+ throw new InvalidWorkingBackwardsDraft();
65
+ }
66
+ let draft: WorkingBackwardsDraft;
67
+ try { draft = await draftWorkingBackwards(issue, ask); }
68
+ catch (error) {
69
+ if (error instanceof InvalidWorkingBackwardsDraft) {
70
+ // The memo is best-effort — failing to write it must not replace the invalid-draft error the caller routes to hitl.
71
+ try { store.appendSection(card.id, { key: invalidKey, owner: 'steward', content: 'working-backwards draft invalid for this issue text — not re-asked until the issue changes' }); }
72
+ catch (memoError) { debug.log('steward.working-backwards', 'invalid-memo-failed', { issueId: issue.identifier, error: memoError instanceof Error ? memoError.message : String(memoError) }); }
73
+ }
74
+ throw error;
75
+ }
61
76
  for (const [section, content] of [['prfaq', draft.prfaq], ['manual', draft.manual]] as const) {
62
77
  const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;
63
78
  if (!card.sections.some(item => item.key === key)) store.appendSection(card.id, { key, owner: 'steward', content });
@@ -46,6 +46,31 @@ export function registerCardCommand(program: Command, deps: CardCliDeps = {}): C
46
46
  }
47
47
  });
48
48
 
49
+ card.command('close <id>')
50
+ .description('Close a task card with a reason (owner operation)')
51
+ .requiredOption('--reason <text>', 'One-line close reason')
52
+ .action((id: string, options: { reason: string }) => {
53
+ const store = createStore();
54
+ try {
55
+ const reason = options.reason.trim();
56
+ if (!reason || /[\r\n]/.test(options.reason)) throw new Error('Close reason must be one line');
57
+ const existing = store.getCard(id);
58
+ if (!existing) {
59
+ process.stderr.write(`Card not found: ${id}\n`);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ if (existing.status === 'closed') {
64
+ write(`이미 닫힘: ${id}\n`);
65
+ return;
66
+ }
67
+ store.closeCard(id, reason);
68
+ write(`닫힘: ${id}\n`);
69
+ } finally {
70
+ store.close();
71
+ }
72
+ });
73
+
49
74
  card.command('list')
50
75
  .description('List task cards')
51
76
  .option('--open', 'Only show open cards')
@@ -32,7 +32,7 @@ export interface AppendSectionInput {
32
32
  type CardEvent =
33
33
  | { type: 'created'; id: string; goalId: string; title: string; createdAt: string }
34
34
  | { type: 'section'; key: string; owner: string; content: string; createdAt: string }
35
- | { type: 'closed'; createdAt: string };
35
+ | { type: 'closed'; createdAt: string; reason?: string };
36
36
 
37
37
  /** RFC §3 (내부 문서 `RFC-execution-and-landing-loop-agents-parallel-first-2026-09-29`) — each card section has one owner.
38
38
  * A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
@@ -197,13 +197,15 @@ export class CardStore {
197
197
  }).immediate();
198
198
  }
199
199
 
200
- closeCard(id: string): TaskCard {
200
+ closeCard(id: string, reason?: string): TaskCard {
201
+ if (reason !== undefined && !reason.trim()) throw new Error('Close reason is required');
201
202
  return this.db.transaction(() => {
202
203
  this.reconcile();
203
204
  const card = this.readCard(id);
204
205
  if (!card) throw new Error(`Card not found: ${id}`);
205
206
  if (card.status === 'closed') return card;
206
- this.append(id, { type: 'closed', createdAt: new Date().toISOString() });
207
+ this.append(id, { type: 'closed', createdAt: new Date().toISOString(),
208
+ ...(reason === undefined ? {} : { reason: redactSecrets(reason.trim()) }) });
207
209
  return { ...card, status: 'closed' as const };
208
210
  }).immediate();
209
211
  }
@@ -3,6 +3,7 @@ import { gunzipSync } from 'node:zlib';
3
3
  import { debug } from '../../debug/log.js';
4
4
  import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
5
5
  import { ledgerLineToLogEvent } from './pod-ledger-events.js';
6
+ import { defaultPodReemit } from './pod-ledger-prod-sink.js';
6
7
 
7
8
  type Ledger = { runId: string; jsonl: string };
8
9
  type Incomplete = { runId: string; reason: string };
@@ -94,7 +95,7 @@ export function parsePodLedgerChunks(logs: string): ParseResult {
94
95
  * except a ledger that THIS run's live follower created (`replace`): that one is a partial copy and the final is complete. */
95
96
  export function collectPodLedgers(
96
97
  logs: string,
97
- { dir = runLedgerDir(), log = (c, e, d) => debug.log(c, e, d), emit = (c, e, d) => debug.log(c, e, d), replace = new Set<string>() }: {
98
+ { dir = runLedgerDir(), log = defaultPodReemit(), emit = defaultPodReemit(), replace = new Set<string>() }: {
98
99
  dir?: string;
99
100
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
100
101
  emit?: Emit;
@@ -146,8 +147,8 @@ export function createPodLedgerFollower(opts: {
146
147
  onStall?: (message: string) => void;
147
148
  }): { poll(): void; readonly owned: boolean } {
148
149
  const dir = opts.dir ?? runLedgerDir();
149
- const log = opts.log ?? ((c, e, d) => debug.log(c, e, d));
150
- const emit: Emit = opts.emit ?? ((c, e, d) => debug.log(c, e, d));
150
+ const log = opts.log ?? defaultPodReemit();
151
+ const emit: Emit = opts.emit ?? defaultPodReemit();
151
152
  const now = opts.now ?? Date.now;
152
153
  const stallMinutes = opts.stallMinutes ?? 30;
153
154
  if (!Number.isFinite(stallMinutes) || stallMinutes <= 0) throw new RangeError('stallMinutes must be positive');
@@ -0,0 +1,75 @@
1
+ import { debug } from '../../debug/log.js';
2
+ import { LogStore, logsDbPath } from '../../mss/logging/log-store.js';
3
+
4
+ type Emit = (category: string, event: string, data: Record<string, unknown>) => void;
5
+ type Store = Pick<LogStore, 'insertBatch'>;
6
+
7
+ /** 발사한 부모(`harness say --config-dir …`)가 정한 집의 logs.db — 부모가 자식 `self orchestrate` 에 env 로 넘긴다. */
8
+ export const POD_REEMIT_LOGS_DB_ENV = 'ELANOUS_POD_REEMIT_LOGS_DB';
9
+ export function launchHomeLogsDb(env: NodeJS.ProcessEnv = process.env): string | undefined {
10
+ return env[POD_REEMIT_LOGS_DB_ENV]?.trim() || undefined;
11
+ }
12
+
13
+ let cached: { path: string; store: Store } | null = null;
14
+ let announced = '';
15
+
16
+ /**
17
+ * POD-OBS(10-06 · 0.2.16 P0 · OP 사양): Pod 자식 원장 «재방출»만 부모가 발사 때 정한 집(A)의 logs.db 로 보낸다.
18
+ * 자식 orchestrate 는 트리 파생 test 우주(B)에서 돌아, 지금은 재방출이 B 에만 쌓였다(24h `ledger-collected` 운영 2 vs 다른 우주 250).
19
+ * 자식 우주 원장·격리는 그대로 둔다. A 를 못 열면 조용히 버리지 않고 B 로 떨어뜨리며 경고한다.
20
+ * env 가 없거나(옛 발사) A == B 이거나 시험 프로세스면 null → 호출측이 지금처럼 B 에만 쓴다.
21
+ */
22
+ export function operationalReemit(opts: {
23
+ currentDb?: string;
24
+ operationalDb?: string;
25
+ open?: (path: string) => Store;
26
+ testProcess?: boolean;
27
+ fallback?: Emit;
28
+ } = {}): Emit | null {
29
+ const operationalDb = opts.operationalDb ?? launchHomeLogsDb();
30
+ if (!operationalDb) return null;
31
+ const currentDb = opts.currentDb ?? logsDbPath();
32
+ const testProcess = opts.testProcess ?? (process.env.NODE_ENV === 'test' || !!process.env.ELANOUS_TEST_HOME);
33
+ if (testProcess && !opts.open) return null;
34
+ if (currentDb === operationalDb) return null;
35
+ const open = opts.open ?? ((path: string) => new LogStore(path, { instance: 'prod' }));
36
+ const fallback: Emit = opts.fallback ?? ((c, e, d) => debug.log(c, e, d));
37
+ const write = (category: string, event: string, data: Record<string, unknown>): void => {
38
+ if (!cached || cached.path !== operationalDb || opts.open) cached = { path: operationalDb, store: open(operationalDb) };
39
+ cached.store.insertBatch([{ rec: { ts: new Date().toISOString(), category, event, data }, surface: 'pod-reemit' }]);
40
+ };
41
+ let warned = false;
42
+ return (category, event, data) => {
43
+ try {
44
+ if (announced !== `${currentDb}->${operationalDb}` || opts.open) {
45
+ // 어느 우주로 보냈는지 한 줄(A 에) — 조회자가 «B 에 왜 없나»를 여기서 본다.
46
+ write('self-implement.pod', 'reemit-target', { targetUniverse: operationalDb, childUniverse: currentDb });
47
+ announced = `${currentDb}->${operationalDb}`;
48
+ }
49
+ write(category, event, data);
50
+ } catch (error) {
51
+ fallback(category, event, data);
52
+ if (warned) return;
53
+ // 경고는 싱크(발사 하나)당 한 번 — 줄마다 찍으면 경고가 원장을 덮는다.
54
+ warned = true;
55
+ try {
56
+ fallback('self-implement.pod', 'reemit-target-unavailable', {
57
+ targetUniverse: operationalDb, childUniverse: currentDb, reason: error instanceof Error ? error.message : String(error),
58
+ });
59
+ } catch { /* nothing */ }
60
+ }
61
+ };
62
+ }
63
+
64
+ let shared: { key: string; emit: Emit } | null = null;
65
+
66
+ /** 재방출 기본 경로: 집(A)이 정해져 있으면 A 에만, 아니면 지금처럼 지금 우주(B)의 debug.log.
67
+ * 프로세스(발사)당 싱크 하나 — collector 의 `log`·`emit`, follower 가 같은 싱크를 써서 경고·대상 줄이 한 번이다. */
68
+ export function defaultPodReemit(): Emit {
69
+ const key = `${launchHomeLogsDb() ?? ''}->${logsDbPath()}`;
70
+ if (shared?.key === key) return shared.emit;
71
+ const operational = operationalReemit();
72
+ const emit: Emit = operational ?? ((category, event, data) => { debug.log(category, event, data); });
73
+ shared = { key, emit };
74
+ return emit;
75
+ }
@@ -8,7 +8,7 @@
8
8
  * - 컨텍스트 kubectl 컨텍스트 이름(이 맥의 kubeconfig)
9
9
  * - @ssh호스트 원격 클러스터면 그 기계 — 이미지 판 대조·반입(`docker load` ⊕ `k3d image import`)에 쓴다
10
10
  * - 상한 이 노드에 동시에 둘 Job 수(기본 2)
11
- * Job 마다 앞 순위부터 «진행 중 < 상한»인 노드를 고른다. 다 차면 자리가 날 때까지 기다린다.
11
+ * Job 마다 «빈 자리 비율»이 가장 큰 노드를 고른다(동률은 앞 순위 · 측정 실패 노드는 측정된 노드가 다 찼을 때만). 다 차면 자리가 날 때까지 기다린다.
12
12
  * 노드 선택의 진행 중 수는 로컬 프로세스 기준이다. 발사 허가는 별도의 lease 측정으로
13
13
  * 다른 호스트의 Running/Pending 및 메모리 예약까지 고려한다.
14
14
  * 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
@@ -271,7 +271,9 @@ export class PodPoolScheduler {
271
271
  return this.admission.snapshot();
272
272
  }
273
273
  /**
274
- * Pick the member with the most free slots: capacity − measured cluster occupancy − this process's inflight.
274
+ * Pick the member with the largest free share: (capacity − measured cluster occupancy − this process's inflight) / capacity.
275
+ * POOL-SPREAD (10-06): comparing absolute free slots always picked the big member (node-b 25 vs node-c 4), so the small
276
+ * member idled until the big one was full. Comparing the free share fills members in proportion to their capacity.
275
277
  * A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
276
278
  * Ties keep the configured member order. Returns null when every readable member is full.
277
279
  */
@@ -284,7 +286,12 @@ export class PodPoolScheduler {
284
286
  }
285
287
  const free: Record<string, number> = {};
286
288
  const measured: Record<string, boolean> = {};
287
- let best: { member: PodPoolMember; slots: number } | null = null;
289
+ let best: { member: PodPoolMember; slots: number; measured: boolean } | null = null;
290
+ // A measured member always beats an unmeasured one (its share would read 100% and draw every Job to a node
291
+ // that did not answer); unmeasured members compete only when no measured member has a free slot.
292
+ const better = (cand: { member: PodPoolMember; slots: number; measured: boolean }) => !best
293
+ || (cand.measured !== best.measured ? cand.measured
294
+ : cand.slots / cand.member.capacity > best.slots / best.member.capacity);
288
295
  for (const m of this.members) {
289
296
  const inflight = this.inflight.get(m.context) ?? 0;
290
297
  const row = reading[m.context];
@@ -294,11 +301,13 @@ export class PodPoolScheduler {
294
301
  const slots = m.capacity - occupied - inflight;
295
302
  free[m.context] = slots;
296
303
  if (slots <= 0) continue;
297
- if (!best || slots > best.slots) best = { member: m, slots };
304
+ const cand = { member: m, slots, measured: ok };
305
+ if (better(cand)) best = cand;
298
306
  }
299
307
  if (!best) return null;
300
308
  this.inflight.set(best.member.context, (this.inflight.get(best.member.context) ?? 0) + 1);
301
- debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured });
309
+ const share = Object.fromEntries(this.members.map((m) => [m.context, Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100]));
310
+ debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured, share });
302
311
  return best.member;
303
312
  }
304
313
  release(member: PodPoolMember): void {
@@ -1,8 +1,10 @@
1
1
  /** Pod Job 원천 단계 — default clone · 커밋 · PR · 호스트 번들.
2
2
  * sha·PR·커밋은 형식 검사 전에 스크립트에 넣지 않는다(셸 주입 차단). */
3
3
 
4
+ import { execFileSync } from 'node:child_process';
5
+
4
6
  export type PodSource =
5
- | { kind: 'default' }
7
+ | { kind: 'default'; base?: string }
6
8
  | { kind: 'commit'; sha: string }
7
9
  | { kind: 'pr'; number: number }
8
10
  | { kind: 'bundle'; bundlePath: string; sha256: string; sizeBytes: number; headCommit: string };
@@ -29,6 +31,27 @@ function assertSize(value: number): number {
29
31
  return value;
30
32
  }
31
33
 
34
+ /** Check the requested branch on origin before creating a Pod Job, not in the launched job. */
35
+ function assertRemoteBase(repoUrl: string, base: string): void {
36
+ if (base.startsWith('-') || base === 'HEAD') throw new Error(`invalid Pod base ref: ${base}`);
37
+ try {
38
+ execFileSync('git', ['check-ref-format', '--branch', base], { stdio: 'pipe' });
39
+ } catch {
40
+ throw new Error(`invalid Pod base ref: ${base}`);
41
+ }
42
+ try {
43
+ const advertised = execFileSync('git', ['ls-remote', '--exit-code', '--heads', repoUrl, `refs/heads/${base}`], {
44
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
45
+ }).trim();
46
+ const [sha, ref] = advertised.split('\t');
47
+ if (!/^[0-9a-f]{40,64}$/.test(sha ?? '') || ref !== `refs/heads/${base}`) {
48
+ throw new Error('origin did not advertise the requested branch');
49
+ }
50
+ } catch {
51
+ throw new Error(`Pod base ref not found on origin: ${base}`);
52
+ }
53
+ }
54
+
32
55
  /** Job 스크립트의 원천 단계. 성공 갈래는 `ELANOUS_POD_SOURCE <kind> <HEAD>` 로 끝난다. */
33
56
  export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath = '/host-mirror'): string {
34
57
  const quotedUrl = `'${repoUrl.replace(/'/g, `'\\''`)}'`;
@@ -48,6 +71,19 @@ export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath =
48
71
  'cd repo || exit 5',
49
72
  ];
50
73
  if (source.kind === 'default') {
74
+ if (source.base && source.base !== 'main') {
75
+ assertRemoteBase(repoUrl, source.base);
76
+ const base = source.base;
77
+ const quote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`;
78
+ const quotedTrackingRef = quote(`refs/remotes/origin/${base}`);
79
+ return [
80
+ ...start,
81
+ `git fetch --depth 50 origin ${quote(`+refs/heads/${base}:refs/remotes/origin/${base}`)} || exit 5`,
82
+ `git checkout -q -B ${quote(base)} ${quotedTrackingRef} || exit 5`,
83
+ 'head=$(git rev-parse HEAD)',
84
+ "printf 'ELANOUS_POD_SOURCE default %s\\n' \"$head\"",
85
+ ].join('\n');
86
+ }
51
87
  return [
52
88
  ...start,
53
89
  'if [ "$source_via" = mirror ]; then',
@@ -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';
@@ -63,6 +66,7 @@ import { declaredGoalType, type GoalType as DeclaredGoalType } from '../../self-
63
66
  import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
64
67
  import { getUserConfig } from '../../user-config.js';
65
68
  import { extractPodFailureReason } 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
 
@@ -594,14 +600,15 @@ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'chi
594
600
  return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
595
601
  }
596
602
 
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> {
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> {
598
604
  const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
599
605
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
600
- 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;
601
608
  const quotedAsk = (askBaseIndex >= 0 && o.args[askBaseIndex + 1]
602
609
  ? ` '--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' : '');
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' : '');
605
612
  const script = [
606
613
  'set -u',
607
614
  o.grokCredential === 'subscription'
@@ -609,7 +616,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
609
616
  : o.grokCredential === 'api_key'
610
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'
611
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()}`,
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)"',
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)"',
613
620
  // 🔑 스킬 키(.env) — 이미지엔 없다. 이 런의 Secret 에서 각 스킬 폴더로 0600 복사(값은 로그에 안 나온다).
614
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(',')}"`] : []),
615
622
  'git config --global user.name "elanous pod child" && git config --global user.email "noreply@anthropic.com" && gh auth setup-git',
@@ -622,9 +629,11 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
622
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=$!`,
623
630
  ...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
624
631
  // 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
625
- 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
626
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{}'`
627
- : `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=$?`,
628
637
  'cat /tmp/si.out',
629
638
  '[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
630
639
  // A long run's log export can pass the 5MB artifact limit; skipping it whole left no window into why a
@@ -1224,7 +1233,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1224
1233
  debug.log('self-implement.pod', 'memory-request', { spaceId: input.spaceId, job: name,
1225
1234
  tier: oomRetried ? retryTier : memoryTier, reason: oomRetried ? 'OOMKilled' : memoryReason,
1226
1235
  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 를 부르지 않는다
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 를 부르지 않는다
1228
1237
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
1229
1238
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
1230
1239
  releaseAdmission?.applied?.(name, context, namespace);
@@ -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
  }