elanous 0.2.14 → 0.2.15

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 (293) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -1,9 +1,9 @@
1
- import { lstatSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
3
3
  import { gunzipSync } from 'node:zlib';
4
4
  import { debug } from '../../debug/log.js';
5
5
  import { getDefaultLogStore, type LogStore } from '../../mss/logging/log-store.js';
6
- import type { LogRecord } from '../../mss/logging/record.js';
6
+ import { fromJsonl, type LogRecord } from '../../mss/logging/record.js';
7
7
 
8
8
  const FILE_LIMIT = 5 * 1024 * 1024;
9
9
  const TOTAL_LIMIT = 20 * 1024 * 1024;
@@ -83,50 +83,58 @@ export function parsePodArtifactChunks(logs: string): ParseResult {
83
83
 
84
84
  const POD_LOGS_PATH = 'pod-logs/logs.jsonl';
85
85
 
86
- function childRunId(data: unknown): string | undefined {
87
- if (!data || typeof data !== 'object' || Array.isArray(data)) return undefined;
88
- const value = (data as Record<string, unknown>).runId;
89
- return typeof value === 'string' && value.length > 0 ? value : undefined;
90
- }
91
-
92
- function decisionRecord(line: string): { rec: LogRecord; surface: string } | undefined {
93
- let parsed: unknown;
94
- try { parsed = JSON.parse(line); } catch { return undefined; }
95
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
96
- const row = parsed as Record<string, unknown>;
97
- if (row.category !== 'harness.decision') return undefined;
98
- if (typeof row.ts !== 'string' || typeof row.event !== 'string') return undefined;
99
- const runId = childRunId(row.data);
100
- if (!runId) return undefined;
101
- const data = { ...(row.data as Record<string, unknown>), podJob: '', importedFrom: 'pod' as const };
102
- return {
103
- rec: {
104
- ts: row.ts,
105
- category: 'harness.decision',
106
- event: row.event,
107
- data,
108
- ...(typeof row.level === 'string' ? { level: row.level as LogRecord['level'] } : {}),
109
- ...(typeof row.trace_id === 'string' ? { trace_id: row.trace_id } : {}),
110
- },
111
- surface: typeof row.surface === 'string' && row.surface ? row.surface : 'pod',
112
- };
86
+ /** Last measured child ledger context when a failed Pod emitted no terminal JSON. */
87
+ export function podNoResultDiagnostic(ledger: string): string | null {
88
+ let stage: string | undefined;
89
+ let round: number | undefined;
90
+ let mustFix: string | undefined;
91
+ for (const line of ledger.split(/\r?\n/)) {
92
+ try {
93
+ const entry = JSON.parse(line) as { event?: unknown; data?: Record<string, unknown> };
94
+ if (!entry || typeof entry !== 'object' || !entry.data || typeof entry.data !== 'object' || typeof entry.event !== 'string' || entry.event === 'pod-ledger-incomplete') continue;
95
+ if (typeof entry.data.round === 'number' && Number.isSafeInteger(entry.data.round)) round = entry.data.round;
96
+ if (entry.event !== 'run-origin') stage = entry.event;
97
+ // `reviewed` carries pointers only (count + finding ids); the text stays in the review artifact.
98
+ if (entry.event === 'reviewed' && typeof entry.data.mustFix === 'number') {
99
+ const ids = Array.isArray(entry.data.findingIds) ? entry.data.findingIds.filter((id): id is string => typeof id === 'string') : [];
100
+ mustFix = `${entry.data.mustFix}${ids.length ? ` ${JSON.stringify(ids)}` : ''}`;
101
+ }
102
+ } catch { /* incomplete ledger line */ }
103
+ }
104
+ return stage ? `child terminal result missing; last ledger stage=${stage}; round=${round ?? 'unknown'}; mustFix=${mustFix ?? 'unknown'}` : null;
113
105
  }
114
106
 
115
- /** Import only harness.decision rows from a returned pod log into the launching host store. */
116
- export function importPodDecisionLogs(bytes: Buffer, job: string, store: LogStore | null = getDefaultLogStore()): { imported: number; skipped: number } {
107
+ /** Import valid JSONL log records into the launching host store, with durable per-job line receipts. */
108
+ export function importPodLogs(bytes: Buffer, job: string, store: LogStore | null = getDefaultLogStore()): { imported: number; skipped: number } {
117
109
  if (!store) return { imported: 0, skipped: 0 };
118
- const text = bytes.toString('utf8');
119
- const rows: Array<{ rec: LogRecord; surface: string }> = [];
110
+ const rows: Array<{ rec: LogRecord; surface: string; line: number }> = [];
120
111
  let skipped = 0;
121
- for (const line of text.split(/\r?\n/)) {
112
+ for (const [lineNumber, line] of bytes.toString('utf8').split(/\r?\n/).entries()) {
122
113
  if (!line) continue;
123
- const row = decisionRecord(line);
124
- if (!row) { skipped += 1; continue; }
125
- row.rec.data = { ...(row.rec.data as Record<string, unknown>), podJob: job };
126
- rows.push(row);
114
+ const rec = fromJsonl(line);
115
+ if (!rec || !Number.isFinite(Date.parse(rec.ts))) { skipped++; continue; }
116
+ const data = rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
117
+ ? rec.data as Record<string, unknown>
118
+ : rec.data === undefined ? {} : { originalData: rec.data };
119
+ const surface = (rec as LogRecord & { surface?: unknown }).surface;
120
+ rows.push({
121
+ rec: { ...rec, data: { ...data, origin: 'pod', podJob: job } },
122
+ surface: typeof surface === 'string' && surface ? surface : 'pod',
123
+ line: lineNumber,
124
+ });
125
+ }
126
+ return { imported: store.insertPodBatch(job, rows), skipped };
127
+ }
128
+
129
+ function importReturnedLogs(bytes: Buffer, job: string, store: LogStore | null,
130
+ log: (category: string, event: string, data: Record<string, unknown>) => void): void {
131
+ try {
132
+ if (!store) throw new Error('host log store unavailable');
133
+ const imported = importPodLogs(bytes, job, store);
134
+ log('self-implement.pod', 'pod-logs-imported', { job, imported: imported.imported, skipped: imported.skipped });
135
+ } catch (error) {
136
+ debug.log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
127
137
  }
128
- if (rows.length > 0) store.insertBatch(rows);
129
- return { imported: rows.length, skipped };
130
138
  }
131
139
 
132
140
  /** Reject symlink parents as well as lexical traversal; never replace a host file. */
@@ -176,17 +184,24 @@ export function collectPodArtifacts(logs: string, { dir, job, log = (c, e, d) =>
176
184
  podLogsReturned = true;
177
185
  const lines = bytes.length ? bytes.toString('utf8').split('\n').length - (bytes.at(-1) === 10 ? 1 : 0) : 0;
178
186
  log('self-implement.pod', 'pod-logs-returned', { job, path: destination, lines });
179
- try {
180
- const imported = importPodDecisionLogs(bytes, job, logStore);
181
- log('self-implement.pod', 'pod-decisions-imported', { job, imported: imported.imported, skipped: imported.skipped });
182
- } catch (error) {
183
- log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
184
- }
187
+ importReturnedLogs(bytes, job, logStore, log);
185
188
  }
186
189
  } catch (e) {
187
190
  const reason = (e as NodeJS.ErrnoException).code === 'EEXIST' ? 'exists' : e instanceof Error ? e.message : String(e);
188
- if ((e as NodeJS.ErrnoException).code === 'EEXIST') log('self-implement.pod', 'artifact-collect-skipped', { job, path, reason });
189
- else log('self-implement.pod', 'artifact-collect-incomplete', { job, path, reason });
191
+ if ((e as NodeJS.ErrnoException).code === 'EEXIST') {
192
+ log('self-implement.pod', 'artifact-collect-skipped', { job, path, reason });
193
+ if (path === podLogsPath) {
194
+ try {
195
+ const existing = resolve(dir, job, path);
196
+ if (!lstatSync(existing).isSymbolicLink() && readFileSync(existing).equals(bytes)) {
197
+ podLogsReturned = true;
198
+ importReturnedLogs(bytes, job, logStore, log);
199
+ }
200
+ } catch (error) {
201
+ debug.log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
202
+ }
203
+ }
204
+ } else log('self-implement.pod', 'artifact-collect-incomplete', { job, path, reason });
190
205
  if (path === podLogsPath) podLogsMissingReason = reason;
191
206
  }
192
207
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { randomBytes } from 'node:crypto';
9
9
  import { spawnSync } from 'node:child_process';
10
+ import { mkdirSync, writeFileSync } from 'node:fs';
10
11
  import { isAbsolute, resolve } from 'node:path';
11
12
  import { getUserConfig } from '../../user-config.js';
12
13
  import { debug } from '../../debug/log.js';
@@ -35,6 +36,14 @@ export const POD_LITE_IMAGE = 'elanous-harness-lite:local';
35
36
 
36
37
  export const POD_LITE_MEMORY_LIMIT = '2Gi';
37
38
 
39
+ /** 요청은 한도를 넘을 수 없다(쿠버네티스가 Job 을 거부한다) — lite 2Gi 처럼 한도가 기본 요청보다 작으면 한도로 맞춘다. */
40
+ export function memoryRequestWithin(limit: string | undefined): string {
41
+ const gi = (value: string | undefined) => { const m = /^(\d+)Gi$/.exec(value ?? ''); return m ? Number(m[1]) : null; };
42
+ const request = gi(POD_CHILD_REQUESTS.memory);
43
+ const max = gi(limit);
44
+ return request !== null && max !== null && request > max ? limit! : POD_CHILD_REQUESTS.memory;
45
+ }
46
+
38
47
  /** POD7 must-fix: the skill list alone can't tell a network-only job from one that needs ffmpeg/browser — so lite is
39
48
  * only ever chosen when the caller says so (`lite`), and then only for network skills without a clone. Anything we
40
49
  * can't judge keeps the full image. A lite request that doesn't fit is refused with the reason, not silently widened. */
@@ -50,6 +59,26 @@ export function podCommandImageFor(skills: readonly string[], clone: boolean, li
50
59
 
51
60
  const COMMAND_LOG_CHARS = 80;
52
61
 
62
+ /** 자식 로그 전문 상한(바이트) — 넘으면 끝부분을 남긴다. config `pod.childLogMaxBytes` 가 양의 정수일 때만 그 값. */
63
+ export const POD_CHILD_LOG_MAX_BYTES_DEFAULT = 1_048_576;
64
+
65
+ export function podChildLogMaxBytes(read: () => number | undefined = () => getUserConfig().pod?.childLogMaxBytes): number {
66
+ const configured = read();
67
+ return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0
68
+ ? configured
69
+ : POD_CHILD_LOG_MAX_BYTES_DEFAULT;
70
+ }
71
+
72
+ /** 크기 상한을 넘으면 끝부분을 남긴다(잘린 앞은 표시). */
73
+ export function tailBytes(text: string, maxBytes: number): string {
74
+ const bytes = Buffer.from(text);
75
+ if (bytes.length <= maxBytes) return text;
76
+ const cut = bytes.subarray(bytes.length - maxBytes);
77
+ let start = 0;
78
+ while (start < cut.length && (cut[start]! & 0xc0) === 0x80) start += 1;
79
+ return `…[앞 ${bytes.length - maxBytes}바이트 생략]\n${cut.subarray(start).toString('utf8')}`;
80
+ }
81
+
53
82
  const GATE = `ok=0
54
83
  for i in $(seq 1 60); do
55
84
  if curl -s -m 1 -o /dev/null http://host.orb.internal:31415/health || curl -s -m 1 -o /dev/null http://core.elanous-prod:8080/; then ok=0; else ok=$((ok+1)); fi
@@ -156,7 +185,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
156
185
  initContainers: [{ name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] }],
157
186
  containers: [{
158
187
  name: 'child', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never',
159
- resources: { requests: { ...POD_CHILD_REQUESTS }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
188
+ resources: { requests: { ...POD_CHILD_REQUESTS, memory: memoryRequestWithin(o.memoryLimit) }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
160
189
  command: ['bash', '-c'], args: [script],
161
190
  env: [
162
191
  ...(o.runId ? [{ name: 'ELANOUS_RUN_ID', value: o.runId }] : []),
@@ -214,12 +243,23 @@ export function podCommandSecretKeys(secret: Record<string, unknown> | null): st
214
243
  return Object.keys((secret.stringData as Record<string, string> | undefined) ?? {});
215
244
  }
216
245
 
246
+ export interface PodArtifactCollection {
247
+ /** 회수한 산출 파일 수(child.log 제외). 로그 조회 실패면 null. */
248
+ files: number | null;
249
+ /** 회수한 산출 파일의 상대 경로. */
250
+ names: string[];
251
+ /** 로그 조회 자체가 실패했을 때의 사유. 성공이면 null. */
252
+ error: string | null;
253
+ }
254
+
217
255
  export interface PodCommandResult {
218
256
  exitCode: number;
219
257
  artifactsDir: string;
220
258
  job: string;
221
259
  /** 이 런이 실제로 쓴 이미지(레지스트리 판 태그 또는 로컬 이름). */
222
260
  image?: string;
261
+ /** collectPodArtifacts 가 회수한 파일. 로그 조회 실패면 files=null · error=사유. runPodCommand 는 항상 채운다. */
262
+ artifacts?: PodArtifactCollection;
223
263
  }
224
264
 
225
265
  export interface RunPodCommandOptions {
@@ -255,6 +295,8 @@ export interface RunPodCommandOptions {
255
295
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
256
296
  env?: NodeJS.ProcessEnv;
257
297
  configHostMirror?: () => string | undefined;
298
+ /** 자식 로그 전문 상한(바이트). 생략 시 config `pod.childLogMaxBytes`, 없으면 1MiB. 넘으면 끝부분 우선. */
299
+ childLogMaxBytes?: number;
258
300
  }
259
301
 
260
302
  function jobFinished(types: string): 'complete' | 'failed' | null {
@@ -263,6 +305,19 @@ function jobFinished(types: string): 'complete' | 'failed' | null {
263
305
  return null;
264
306
  }
265
307
 
308
+ function artifactNames(logs: string): string[] {
309
+ const names = new Set<string>();
310
+ for (const line of logs.split(/\r?\n/)) {
311
+ const match = /^ELANOUS_POD_ARTIFACT (\S+) \d+\/\d+ /.exec(line);
312
+ if (!match) continue;
313
+ try {
314
+ const path = Buffer.from(match[1]!, 'base64url').toString('utf8');
315
+ if (path && !path.split('/').some((part) => part === '' || part === '.' || part === '..')) names.add(path);
316
+ } catch { /* 회수 쪽(collectPodArtifacts)이 불완전 사유를 남긴다 */ }
317
+ }
318
+ return [...names];
319
+ }
320
+
266
321
  function exitCodeFromPod(kubectl: Kubectl, namespace: string, job: string, state: 'complete' | 'failed'): number {
267
322
  const got = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${job}`, '-o', 'jsonpath={.items[0].status.containerStatuses[?(@.name=="child")].state.terminated.exitCode}']);
268
323
  const code = Number(got.stdout.trim());
@@ -316,7 +371,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
316
371
  }
317
372
  if (pool) {
318
373
  for (;;) {
319
- member = pool.tryAcquire();
374
+ member = await pool.tryAcquire();
320
375
  if (member) break;
321
376
  await sleep(options.pollMs ?? 15_000);
322
377
  }
@@ -387,12 +442,29 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
387
442
  if (finished) { state = finished; break; }
388
443
  await sleep(options.pollMs ?? 15_000);
389
444
  }
390
- const logs = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child']).stdout;
391
- collectPodArtifacts(logs, {
392
- dir: options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`,
393
- job: name,
394
- log: (c, e, d) => log(c, e, d),
395
- });
445
+ const logsResult = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child']);
446
+ const logsFailed = logsResult.status !== 0;
447
+ const logs = logsFailed ? '' : logsResult.stdout;
448
+ const artifactDir = options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`;
449
+ let artifacts: PodArtifactCollection;
450
+ if (logsFailed) {
451
+ const reason = logsResult.stderr.trim() || `kubectl logs exit ${logsResult.status}`;
452
+ artifacts = { files: null, names: [], error: reason };
453
+ log('pod.command', 'artifact-collect-failed', { job: name, reason });
454
+ } else {
455
+ collectPodArtifacts(logs, {
456
+ dir: artifactDir,
457
+ job: name,
458
+ log: (c, e, d) => log(c, e, d),
459
+ });
460
+ const names = artifactNames(logs);
461
+ artifacts = { files: names.length, names, error: null };
462
+ }
463
+ // 표지가 없어도 알리는 경로는 실제로 존재해야 한다. 자식 로그 전문은 child.log (상한 넘으면 끝부분).
464
+ mkdirSync(artifactsDir, { recursive: true });
465
+ const maxBytes = options.childLogMaxBytes ?? podChildLogMaxBytes();
466
+ const childLog = logsFailed ? `산출 회수 못 함: ${artifacts.error}` : tailBytes(logs, maxBytes);
467
+ writeFileSync(resolve(artifactsDir, 'child.log'), childLog);
396
468
  const exitCode = exitCodeFromPod(kubectl, namespace, name, state);
397
469
  cleanupSecret();
398
470
  log('pod.command', 'job-finished', {
@@ -401,8 +473,11 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
401
473
  skills,
402
474
  llm: llm ?? null,
403
475
  command: command.join(' ').slice(0, COMMAND_LOG_CHARS),
476
+ artifactsDir,
477
+ artifacts,
478
+ ...(logsFailed ? { artifactError: `산출 회수 못 함: ${artifacts.error}` } : {}),
404
479
  });
405
- return { exitCode, artifactsDir, job: name, image: imageRef ?? image };
480
+ return { exitCode, artifactsDir, job: name, image: imageRef ?? image, artifacts };
406
481
  } catch (err) {
407
482
  cleanupSecret();
408
483
  throw err;
@@ -0,0 +1,48 @@
1
+ /** Extract a human-readable reason from a failed child Pod without treating transfer markers as child errors. */
2
+ export function extractPodFailureReason(input: {
3
+ logs: string;
4
+ logTailReason?: string;
5
+ containerReason?: string | null;
6
+ jobReason?: string;
7
+ deadlineSeconds?: number;
8
+ }): string {
9
+ if (input.jobReason === 'DeadlineExceeded') {
10
+ return `DeadlineExceeded: Job 수명 상한 ${input.deadlineSeconds ?? 'unknown'}초 초과`;
11
+ }
12
+ if (input.containerReason === 'OOMKilled') return 'OOMKilled: 컨테이너 메모리 한도 초과';
13
+
14
+ const isBookkeeping = (line: string): boolean =>
15
+ /^ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
16
+ const isError = (line: string): boolean =>
17
+ /\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
18
+ const meaningful: Array<{ text: string; terminal: boolean }> = [];
19
+ for (const line of input.logs.split(/\r\n|\n|\r/u)) {
20
+ const trimmed = line.trim();
21
+ if (!trimmed || isBookkeeping(trimmed)) continue;
22
+ if (trimmed.startsWith('{')) {
23
+ let parsed: unknown;
24
+ try { parsed = JSON.parse(trimmed); } catch { /* An ordinary line may begin with a brace. */ }
25
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
26
+ const row = parsed as Record<string, unknown>;
27
+ if (typeof row.ok === 'boolean' && typeof row.stage === 'string') {
28
+ meaningful.length = 0;
29
+ if (row.ok === false && typeof row.error === 'string') {
30
+ const errorLines = row.error.split(/\r\n|\n|\r/u).map((part) => part.trim())
31
+ .filter((part) => part && !isBookkeeping(part));
32
+ const errorLine = [...errorLines].reverse().find(isError) ?? errorLines.at(-1);
33
+ if (errorLine) meaningful.push({ text: errorLine, terminal: true });
34
+ }
35
+ continue;
36
+ }
37
+ // A JSON transfer/observation is not a plain-text failure reason.
38
+ continue;
39
+ }
40
+ }
41
+ meaningful.push({ text: trimmed, terminal: false });
42
+ }
43
+ const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
44
+ const reason = errorLine?.text.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
45
+ if (reason) return reason;
46
+ const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
47
+ return `사유 못 읽음: ${why.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240)}`;
48
+ }
@@ -20,7 +20,7 @@ import { debug } from '../../debug/log.js';
20
20
  import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
21
21
  import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
22
22
  import { HostPoolLease, leaseHasPendingPod } from '../../pod-lease/host-lease.js';
23
- import { measurePoolLease, recommendConcurrency, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
23
+ import { measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
24
24
 
25
25
  export interface PodPoolMember {
26
26
  readonly context: string;
@@ -142,7 +142,34 @@ export function genericConfigPodPool(config: { pod?: { pool?: string }; harness?
142
142
  return config.pod?.pool;
143
143
  }
144
144
 
145
- /** 노드 자리 배분 — 우선순위 순서로 첫 빈 자리. 순수(시각·대기는 호출자가). */
145
+ /**
146
+ * Cluster occupancy of one pool member: Running + Pending harness goal Pods.
147
+ * `null` means the read failed — callers must not treat that as zero.
148
+ */
149
+ export type MemberOccupancy = { occupied: number } | { occupied: null; reason: string };
150
+
151
+ /** Read each member's cluster occupancy (Running + Pending harness goal Pods). A failed member is `occupied: null`, never 0. */
152
+ export function measureMemberOccupancy(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; measure?: (members: readonly PodPoolMember[]) => { members: PodLeaseMember[] } } = {}): Record<string, MemberOccupancy> {
153
+ let measured: { members: PodLeaseMember[] };
154
+ try {
155
+ measured = deps.measure
156
+ ? deps.measure(members)
157
+ : measurePoolLease(members, { ...(deps.kubectl ? { kubectl: deps.kubectl } : {}), ...(deps.dns ? { dns: deps.dns } : {}) });
158
+ } catch (error) {
159
+ const reason = error instanceof Error ? error.message : String(error);
160
+ return Object.fromEntries(members.map((m) => [m.context, { occupied: null, reason }]));
161
+ }
162
+ const byContext = new Map(measured.members.map((m) => [m.context, m]));
163
+ return Object.fromEntries(members.map((member) => {
164
+ const row = byContext.get(member.context);
165
+ if (!row || row.running === null || row.pending === null) {
166
+ return [member.context, { occupied: null, reason: row?.reason ?? '측정 불가' }];
167
+ }
168
+ return [member.context, { occupied: row.running + row.pending }];
169
+ }));
170
+ }
171
+
172
+ /** 노드 자리 배분 — 클러스터 실측 점유가 있으면 남은 칸이 가장 큰 멤버. 측정이 없으면 우선순위 순 첫 빈 자리. */
146
173
  export function podPoolHostLease(members: readonly PodPoolMember[]): HostPoolLease {
147
174
  return new HostPoolLease([...members.map((m) => `${m.context}@${m.sshHost ?? ''}`)].sort().join(','));
148
175
  }
@@ -158,7 +185,8 @@ export class PodPoolScheduler {
158
185
  private lastRaw: PoolLeaseRecommendation | null = null;
159
186
  private readonly hostLease: HostPoolLease;
160
187
  private readonly pollMs: number;
161
- constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
188
+ private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
189
+ constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; occupancy?: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
162
190
  this.pollMs = options.pollMs ?? 15_000;
163
191
  // One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
164
192
  this.hostLease = options.hostLease ?? podPoolHostLease(members);
@@ -167,6 +195,7 @@ export class PodPoolScheduler {
167
195
  return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
168
196
  });
169
197
  this.measure = async () => measureStatus();
198
+ this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
170
199
  this.admission = new PodLeaseAdmission({ status: async () => {
171
200
  const measured = await measureStatus();
172
201
  this.lastRaw = measured;
@@ -241,13 +270,36 @@ export class PodPoolScheduler {
241
270
  admissionSnapshot(): ReturnType<PodLeaseAdmission['snapshot']> {
242
271
  return this.admission.snapshot();
243
272
  }
244
- /** 자리가 있으면 그 노드를 잡고 돌려준다. 없으면 null. */
245
- tryAcquire(): PodPoolMember | null {
273
+ /**
274
+ * Pick the member with the most free slots: capacity − measured cluster occupancy − this process's inflight.
275
+ * A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
276
+ * Ties keep the configured member order. Returns null when every readable member is full.
277
+ */
278
+ async tryAcquire(): Promise<PodPoolMember | null> {
279
+ let reading: Record<string, MemberOccupancy>;
280
+ try { reading = await this.occupancy(); }
281
+ catch (error) {
282
+ const reason = error instanceof Error ? error.message : String(error);
283
+ reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: null, reason }]));
284
+ }
285
+ const free: Record<string, number> = {};
286
+ const measured: Record<string, boolean> = {};
287
+ let best: { member: PodPoolMember; slots: number } | null = null;
246
288
  for (const m of this.members) {
247
- const n = this.inflight.get(m.context) ?? 0;
248
- if (n < m.capacity) { this.inflight.set(m.context, n + 1); return m; }
289
+ const inflight = this.inflight.get(m.context) ?? 0;
290
+ const row = reading[m.context];
291
+ const ok = !!row && row.occupied !== null;
292
+ measured[m.context] = ok;
293
+ const occupied = ok ? row.occupied! : 0;
294
+ const slots = m.capacity - occupied - inflight;
295
+ free[m.context] = slots;
296
+ if (slots <= 0) continue;
297
+ if (!best || slots > best.slots) best = { member: m, slots };
249
298
  }
250
- return null;
299
+ if (!best) return null;
300
+ 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 });
302
+ return best.member;
251
303
  }
252
304
  release(member: PodPoolMember): void {
253
305
  this.inflight.set(member.context, Math.max(0, (this.inflight.get(member.context) ?? 0) - 1));