elanous 0.2.19 → 0.2.20

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 (388) 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
  11. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
  35. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
  36. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
  41. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
  103. package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  104. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
  110. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  111. package/apps/pwa/out/approvals/index.html +1 -1
  112. package/apps/pwa/out/approvals/index.txt +23 -23
  113. package/apps/pwa/out/autopilot/index.html +1 -1
  114. package/apps/pwa/out/autopilot/index.txt +23 -23
  115. package/apps/pwa/out/board/index.html +1 -1
  116. package/apps/pwa/out/board/index.txt +23 -23
  117. package/apps/pwa/out/botlab/index.html +1 -1
  118. package/apps/pwa/out/botlab/index.txt +22 -22
  119. package/apps/pwa/out/bots/index.html +1 -1
  120. package/apps/pwa/out/bots/index.txt +22 -22
  121. package/apps/pwa/out/ceo/index.html +1 -1
  122. package/apps/pwa/out/ceo/index.txt +23 -23
  123. package/apps/pwa/out/chat/index.html +1 -1
  124. package/apps/pwa/out/chat/index.txt +22 -22
  125. package/apps/pwa/out/consult/index.html +1 -1
  126. package/apps/pwa/out/consult/index.txt +23 -23
  127. package/apps/pwa/out/control/index.html +1 -1
  128. package/apps/pwa/out/control/index.txt +23 -23
  129. package/apps/pwa/out/decisions/index.html +1 -1
  130. package/apps/pwa/out/decisions/index.txt +22 -22
  131. package/apps/pwa/out/design-check/index.html +1 -1
  132. package/apps/pwa/out/design-check/index.txt +23 -23
  133. package/apps/pwa/out/editor/index.html +1 -1
  134. package/apps/pwa/out/editor/index.txt +23 -23
  135. package/apps/pwa/out/exec/index.html +1 -1
  136. package/apps/pwa/out/exec/index.txt +23 -23
  137. package/apps/pwa/out/field/index.html +1 -1
  138. package/apps/pwa/out/field/index.txt +23 -23
  139. package/apps/pwa/out/healthz +1 -1
  140. package/apps/pwa/out/index.html +1 -1
  141. package/apps/pwa/out/index.txt +22 -22
  142. package/apps/pwa/out/inside/index.html +1 -1
  143. package/apps/pwa/out/inside/index.txt +22 -22
  144. package/apps/pwa/out/intake/index.html +1 -1
  145. package/apps/pwa/out/intake/index.txt +23 -23
  146. package/apps/pwa/out/live/index.html +1 -1
  147. package/apps/pwa/out/live/index.txt +23 -23
  148. package/apps/pwa/out/loops/index.html +1 -1
  149. package/apps/pwa/out/loops/index.txt +22 -22
  150. package/apps/pwa/out/market/index.html +1 -1
  151. package/apps/pwa/out/market/index.txt +22 -22
  152. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  153. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  154. package/apps/pwa/out/morning/index.html +1 -1
  155. package/apps/pwa/out/morning/index.txt +23 -23
  156. package/apps/pwa/out/observatory/index.html +1 -1
  157. package/apps/pwa/out/observatory/index.txt +23 -23
  158. package/apps/pwa/out/ops/checklist/index.html +1 -1
  159. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  160. package/apps/pwa/out/ops/release/index.html +1 -1
  161. package/apps/pwa/out/ops/release/index.txt +22 -22
  162. package/apps/pwa/out/ops/seats/index.html +1 -1
  163. package/apps/pwa/out/ops/seats/index.txt +22 -22
  164. package/apps/pwa/out/outputs/index.html +1 -1
  165. package/apps/pwa/out/outputs/index.txt +22 -22
  166. package/apps/pwa/out/reflection/index.html +1 -1
  167. package/apps/pwa/out/reflection/index.txt +23 -23
  168. package/apps/pwa/out/scheduler/index.html +1 -1
  169. package/apps/pwa/out/scheduler/index.txt +23 -23
  170. package/apps/pwa/out/sessions/index.html +1 -1
  171. package/apps/pwa/out/sessions/index.txt +22 -22
  172. package/apps/pwa/out/settings/devices/index.html +1 -1
  173. package/apps/pwa/out/settings/devices/index.txt +23 -23
  174. package/apps/pwa/out/settings/index.html +1 -1
  175. package/apps/pwa/out/settings/index.txt +23 -23
  176. package/apps/pwa/out/setup/done/index.html +1 -1
  177. package/apps/pwa/out/setup/done/index.txt +23 -23
  178. package/apps/pwa/out/setup/index.html +1 -1
  179. package/apps/pwa/out/setup/index.txt +23 -23
  180. package/apps/pwa/out/share/index.html +1 -1
  181. package/apps/pwa/out/share/index.txt +23 -23
  182. package/apps/pwa/out/showroom/index.html +1 -1
  183. package/apps/pwa/out/showroom/index.txt +22 -22
  184. package/apps/pwa/out/tasks/index.html +1 -1
  185. package/apps/pwa/out/tasks/index.txt +23 -23
  186. package/apps/pwa/out/term/index.html +1 -1
  187. package/apps/pwa/out/term/index.txt +23 -23
  188. package/apps/pwa/out/today/index.html +1 -1
  189. package/apps/pwa/out/today/index.txt +23 -23
  190. package/apps/pwa/out/trace/index.html +1 -1
  191. package/apps/pwa/out/trace/index.txt +23 -23
  192. package/apps/pwa/out/vault/index.html +1 -1
  193. package/apps/pwa/out/vault/index.txt +23 -23
  194. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  195. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  196. package/apps/pwa/out/workflows/index.html +1 -1
  197. package/apps/pwa/out/workflows/index.txt +23 -23
  198. package/apps/pwa/out/workspace/index.html +1 -1
  199. package/apps/pwa/out/workspace/index.txt +23 -23
  200. package/apps/pwa/out/worktrees/index.html +1 -1
  201. package/apps/pwa/out/worktrees/index.txt +23 -23
  202. package/graphs/demo/editor-recipes.yaml +22 -0
  203. package/graphs/retro/daily.yaml +25 -0
  204. package/graphs/retro/recipes.yaml +24 -0
  205. package/package.json +1 -1
  206. package/scripts/ask-marker-check.ts +3 -1
  207. package/scripts/land-global-checks.ts +89 -0
  208. package/scripts/lib/pch-regress-map.ts +2 -1
  209. package/scripts/release-loop/gate-node.ts +296 -25
  210. package/scripts/release-loop/gate-timing.ts +116 -7
  211. package/scripts/release-loop/preflight.ts +6 -4
  212. package/scripts/release-loop/publish-window.ts +141 -0
  213. package/scripts/release-loop/release-docs.ts +1 -4
  214. package/scripts/release-loop/release-universe.ts +38 -0
  215. package/scripts/release-loop/resume-release.ts +4 -1
  216. package/scripts/release-loop/unattended-release.ts +10 -2
  217. package/scripts/release-story/draft.ts +5 -1
  218. package/scripts/retro/shadow-compare.ts +71 -0
  219. package/scripts/rhythm/daily.ts +6 -1
  220. package/src/agent-mission/driver.ts +34 -6
  221. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  222. package/src/autopilot/build/merge-intent.ts +135 -7
  223. package/src/away/release-watch.ts +20 -3
  224. package/src/cli/config-drift.ts +513 -0
  225. package/src/cli/log-event-names.ts +4 -0
  226. package/src/cli/pr-cli.ts +20 -0
  227. package/src/cli/release-cli.ts +20 -5
  228. package/src/cli/retro-cli.ts +78 -0
  229. package/src/cli/tasks-cli.ts +94 -8
  230. package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
  231. package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
  232. package/src/domains/outbound-log-probe.ts +6 -1
  233. package/src/graph-runner/graph-run-api.ts +205 -0
  234. package/src/graph-runner/runner.ts +11 -1
  235. package/src/harness/harness-cli-command.ts +419 -52
  236. package/src/harness/harness-queue.ts +315 -25
  237. package/src/harness/harness-salvage-cli.ts +62 -0
  238. package/src/hq/fence-outcomes.ts +63 -0
  239. package/src/hq/fence-wrapper.ts +33 -0
  240. package/src/hq/hq.ts +11 -1
  241. package/src/index.ts +69 -6
  242. package/src/maturity/feature-maturity.ts +2 -2
  243. package/src/nexus/api/graph-kinds.ts +10 -4
  244. package/src/nexus/api/graphs-api.ts +178 -6
  245. package/src/nexus/api/http-server.ts +38 -3
  246. package/src/nexus/api/loop-edges.ts +4 -2
  247. package/src/nexus/api/ops-api.ts +22 -7
  248. package/src/nexus/api/ops-board-route.ts +48 -0
  249. package/src/onboarding/entry-hints.ts +5 -0
  250. package/src/onboarding.ts +24 -6
  251. package/src/release-loop/feature-store.ts +14 -11
  252. package/src/release-loop/gate-shards.ts +158 -0
  253. package/src/release-loop/release-schedule.ts +6 -2
  254. package/src/retro/daily.ts +372 -0
  255. package/src/seat-loop/op-decide.ts +173 -0
  256. package/src/seat-loop/seat-loop.ts +60 -3
  257. package/src/seat-loop/seat-report.ts +58 -3
  258. package/src/self-dev/ask-launch-flow.ts +5 -4
  259. package/src/self-dev/ask-launch-io.ts +49 -1
  260. package/src/self-dev/ask-path-candidates.ts +40 -2
  261. package/src/self-dev/draft-sweep.ts +371 -16
  262. package/src/self-dev/draft-triage-rules.ts +19 -4
  263. package/src/self-dev/pr-preview.ts +84 -16
  264. package/src/self-implement/blocked-draft-policy.ts +101 -0
  265. package/src/self-implement/execution-clarification.ts +11 -0
  266. package/src/self-implement/gate-env-retry.ts +83 -0
  267. package/src/self-implement/orchestrator.ts +218 -5
  268. package/src/self-implement/run-stop.ts +123 -0
  269. package/src/self-implement/run-ttl.ts +258 -0
  270. package/src/self-implement/salvage-retention.ts +123 -0
  271. package/src/self-implement/seams.ts +4 -0
  272. package/src/self-implement/self-implement-cli.ts +2 -0
  273. package/src/self-implement/sibling-resync.ts +223 -0
  274. package/src/self-implement/stop-autoheal.ts +204 -0
  275. package/src/task-agent/board.ts +1 -1
  276. package/src/task-agent/card-evidence.ts +99 -10
  277. package/src/task-agent/judge.ts +7 -0
  278. package/src/task-agent/live-moves.ts +120 -20
  279. package/src/task-agent/mission.ts +1 -1
  280. package/src/task-agent/shadow.ts +10 -1
  281. package/src/task-agent/task-hand.ts +7 -2
  282. package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
  283. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  284. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  285. package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
  286. package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
  287. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
  288. package/src/telegram-commands.ts +41 -0
  289. package/src/testing/real-cli-spawn.ts +106 -0
  290. package/src/user-config.ts +56 -3
  291. package/src/version/packed-revision.json +1 -1
  292. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
  301. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  304. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
  387. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
  388. /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -44,8 +44,12 @@ export interface TaskCard {
44
44
  goal?: string;
45
45
  milestone?: string;
46
46
  createdAt: string;
47
- /** launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사. */
48
- status: 'handed' | 'launched' | 'launch-failed';
47
+ /**
48
+ * launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사.
49
+ * failed = 발사된 런이 PR·수확 가지 없이 죽었거나 끝내 안 떴다(TA-JUDGE-DEAD-RUN · `card-evidence.ts`) — 사유
50
+ * (`failed/needs-owner|needs-relaunch — …`)는 history 마지막 칸 · 자동 재발사하지 않는다(사람/판단부 몫).
51
+ */
52
+ status: 'handed' | 'launched' | 'launch-failed' | 'failed';
49
53
  history: TaskCardEvent[];
50
54
  /** 미션 카드만 — 조각 카드 id(순서대로) · 쪼갠 출처(`mission.ts` splitMission). */
51
55
  pieces?: string[];
@@ -178,6 +182,7 @@ export function nextMoveFor(card: TaskCard, landed?: ReadonlySet<string>): TaskC
178
182
  if (card.status !== 'launched' && unmet.length > 0) return { kind: 'wait', reason: `after ${unmet.join(', ')}${landed ? '' : ' · 착지 미확인'}` };
179
183
  if (card.status === 'handed') return { kind: 'launch', command: launchArgs(card), reason: '넘겨받은 과제 — 첫 발사' };
180
184
  if (card.status === 'launch-failed') return { kind: 'launch', command: launchArgs(card), reason: '직전 발사 실패 — 다시 발사' };
185
+ if (card.status === 'failed') return { kind: 'wait', reason: card.history.at(-1)?.detail ?? '런 실패 — PR·수확 가지 없음' };
181
186
  return { kind: 'wait', reason: '발사됨 — 런 멈춤을 슈퍼바이저 그림자 판단(task-agent.shadow-move)이 받는다' };
182
187
  }
183
188
 
@@ -14,6 +14,7 @@ import { debug } from '../../debug/log.js';
14
14
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
15
15
  import { collectPodArtifacts } from './pod-artifact-return.js';
16
16
  import { podBunCacheVolume } from './pod-bun-cache.js';
17
+ import { podHostDirsInit, podInstallSlotsVolume } from './pod-install-slots.js';
17
18
  import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
18
19
  import { podSkillsDigest, readSkillEnvFiles, resolvePodSkills } from './pod-skills.js';
19
20
  import { podSourceScript, type PodSource } from './pod-source-receive.js';
@@ -176,6 +177,8 @@ export interface PodCommandJobInput {
176
177
  clone?: boolean;
177
178
  hostMirror?: string;
178
179
  bunCache?: string;
180
+ /** GATE-INSTALL-CACHE — hostPath of the node-wide install tokens (release gate shards only · `pod-install-slots.ts`). */
181
+ installSlots?: string;
179
182
  /** 컨테이너 메모리 한도(기본 16Gi) — 게이트가 파일 하나 격리 Job 에 올린다. */
180
183
  memoryLimit?: string;
181
184
  /** GATE-SPEED A3①: CPU request/limit override (release gate shards only). Omitted = request 1 / limit 4. */
@@ -197,6 +200,9 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
197
200
  }
198
201
  const grok = o.llm === 'grok';
199
202
  const bunCache = o.bunCache ? podBunCacheVolume(o.bunCache) : undefined;
203
+ const installSlots = o.installSlots ? podInstallSlotsVolume(o.installSlots) : undefined;
204
+ // hostPath directories the uid-1000 child writes: a root init container hands them over (DirectoryOrCreate is root 0755).
205
+ const hostDirMounts = [...(bunCache ? [bunCache.volumeMount] : []), ...(installSlots ? [installSlots.volumeMount] : [])];
200
206
  const cpu = resolvePodCpu(o.cpu);
201
207
  const quotedArgs = o.command.map(bashSingleQuote).join(' ');
202
208
  const credLines = [
@@ -233,7 +239,10 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
233
239
  spec: {
234
240
  restartPolicy: 'Never',
235
241
  securityContext: { runAsUser: 1000, fsGroup: 1000 },
236
- initContainers: [{ name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] }],
242
+ initContainers: [
243
+ { name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] },
244
+ ...(hostDirMounts.length ? [podHostDirsInit(o.image, o.imagePullPolicy ?? 'Never', hostDirMounts)] : []),
245
+ ],
237
246
  containers: [{
238
247
  name: 'child', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never',
239
248
  resources: { requests: { ...POD_CHILD_REQUESTS, cpu: cpu.request, memory: memoryRequestWithin(o.memoryLimit) }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: cpu.limit } },
@@ -243,16 +252,17 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
243
252
  { name: 'ELANOUS_SUBSTRATE', value: 'pod' },
244
253
  { name: 'ELANOUS_POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
245
254
  ],
246
- ...(secretKeys || o.hostMirror || bunCache ? { volumeMounts: [
255
+ ...(secretKeys || o.hostMirror || hostDirMounts.length ? { volumeMounts: [
247
256
  ...(secretKeys ? [{ name: 'creds', mountPath: '/creds', readOnly: true }] : []),
248
257
  ...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : []),
249
- ...(bunCache ? [bunCache.volumeMount] : []),
258
+ ...hostDirMounts,
250
259
  ] } : {}),
251
260
  }],
252
- ...(secretKeys || o.hostMirror || bunCache ? { volumes: [
261
+ ...(secretKeys || o.hostMirror || hostDirMounts.length ? { volumes: [
253
262
  ...(secretKeys ? [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] : []),
254
263
  ...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : []),
255
264
  ...(bunCache ? [bunCache.volume] : []),
265
+ ...(installSlots ? [installSlots.volume] : []),
256
266
  ] } : {}),
257
267
  },
258
268
  },
@@ -321,6 +331,8 @@ export interface RunPodCommandOptions {
321
331
  clone?: boolean;
322
332
  hostMirror?: string;
323
333
  bunCache?: string;
334
+ /** GATE-INSTALL-CACHE — hostPath of the node-wide install tokens (absolute). */
335
+ installSlots?: string;
324
336
  /** 컨테이너 메모리 한도(기본 16Gi · lite 면 2Gi). */
325
337
  memoryLimit?: string;
326
338
  /** GATE-SPEED A3①: CPU request/limit override — only the release gate passes it. Omitted = request 1 / limit 4. */
@@ -410,6 +422,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
410
422
  if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
411
423
  const bunCache = options.bunCache?.trim() || undefined;
412
424
  if (bunCache && !isAbsolute(bunCache)) throw new Error('pod.bunCache must be an absolute directory path');
425
+ const installSlots = options.installSlots?.trim() || undefined;
426
+ if (installSlots && !isAbsolute(installSlots)) throw new Error('pod installSlots must be an absolute directory path');
413
427
  // Fail before any Secret/Job is applied — a bad quantity must not reach the cluster.
414
428
  if (options.cpu) resolvePodCpu(options.cpu);
415
429
 
@@ -486,7 +500,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
486
500
  name, namespace, launch, image: imageRef ?? image,
487
501
  ...(imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
488
502
  repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
489
- ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(memoryLimit ? { memoryLimit } : {}), ...(options.cpu ? { cpu: options.cpu } : {}), deadlineSeconds,
503
+ ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(installSlots ? { installSlots } : {}), ...(memoryLimit ? { memoryLimit } : {}), ...(options.cpu ? { cpu: options.cpu } : {}), deadlineSeconds,
490
504
  ...(options.runId ? { runId: options.runId } : {}),
491
505
  });
492
506
  kubectl(['-n', namespace, 'delete', 'job', '-l', own, '--ignore-not-found']);
@@ -1,3 +1,7 @@
1
+ /** STOP-RECORD stop_class values used by the card terminal classifier. */
2
+ export const STOP_CLASS_POD_FAILURE = 'pod-failure' as const;
3
+ export const STOP_CLASS_NO_LAUNCH = 'no-launch' as const;
4
+
1
5
  /** Executor kinds of the dev child's `--json` result line (`src/index.ts` writeStdoutJson · DevExecutor.kind). */
2
6
  const CHILD_RESULT_KINDS = new Set(['self', 'external']);
3
7
 
@@ -0,0 +1,89 @@
1
+ // GATE-INSTALL-CACHE (0.2.20) — a node-wide throttle for the release-gate shards' `bun install`.
2
+ //
3
+ // Cause, measured on the 0.2.19 gate (`elanous logs --category release-loop.gate --event pod-shard --all --include-test`):
4
+ // · no cache is «not» the cause — the harness image carries a warm Bun cache, and a Pod that installed alone
5
+ // (10-07 10:48–10:55 · 12:13 · 12:16) took 2.4–5.0 s with `pod-bun-cache source=none`;
6
+ // · the same install took 150–340 s when 24 shard Pods (or ~20 isolation Pods) started together on the one 32-CPU
7
+ // node-b node (09:3x round 1: 221–337 s · 11:4x round 3: 150–240 s · 10:34 isolation wave: 124–188 s).
8
+ // ⇒ concurrent installs thrash the node (24 × copying node_modules out of the image cache + CPU), so a shared
9
+ // hostPath Bun cache would not help (the bytes are already local) — fewer installs «at once» does.
10
+ //
11
+ // Concurrency safety: the hostPath holds only throttle tokens (`slot-<n>` directories), never packages. Each Pod still
12
+ // installs into its own container (its own node_modules, its own image-local Bun cache), so correctness never depends on
13
+ // the token: a lost token (Pod killed mid-install) only lowers throughput until it goes stale (`staleSeconds`), a doubled
14
+ // token (two waiters reclaiming one stale slot in the same instant) only lets one extra install run, and a waiter that
15
+ // cannot get a token within `waitSeconds` — or finds the directory unwritable — installs anyway (fail-open).
16
+ // `mkdir` is atomic on one filesystem, and every Pod on a node sees the same hostPath directory.
17
+
18
+ /** Default hostPath of the install tokens on each gate node. */
19
+ export const POD_INSTALL_SLOTS_HOST_PATH = '/var/lib/elanous/gate-install-slots';
20
+ /** Default concurrent installs per node (`release.loop.gatePodInstallSlots` · 0 = off). Alone ≈ 3–5 s ⇒ 24 shards ≈ 6 waves. */
21
+ export const POD_INSTALL_SLOTS_DEFAULT = 4;
22
+ export const POD_INSTALL_SLOTS_MOUNT = '/gate-install-slots';
23
+
24
+ /** hostPath volume ⊕ mount for the tokens. `DirectoryOrCreate` makes a root-owned 0755 directory, so the Pod also needs
25
+ * {@link podHostDirsInit} (the child runs as uid 1000). */
26
+ export function podInstallSlotsVolume(hostPath: string = POD_INSTALL_SLOTS_HOST_PATH) {
27
+ return {
28
+ volume: { name: 'install-slots', hostPath: { path: hostPath, type: 'DirectoryOrCreate' } },
29
+ volumeMount: { name: 'install-slots', mountPath: POD_INSTALL_SLOTS_MOUNT, readOnly: false },
30
+ };
31
+ }
32
+
33
+ /**
34
+ * A root init container that hands the mounted hostPath directories to the child's uid. Without it a `DirectoryOrCreate`
35
+ * hostPath stays root:root 0755 and the child's `[ -w … ]` guard silently skips it (the Bun cache option was a no-op).
36
+ */
37
+ export function podHostDirsInit(image: string, imagePullPolicy: string, mounts: ReadonlyArray<{ name: string; mountPath: string; readOnly: boolean }>) {
38
+ const paths = mounts.map((mount) => `'${mount.mountPath.replace(/'/g, `'\\''`)}'`).join(' ');
39
+ return {
40
+ name: 'host-dirs', image, imagePullPolicy,
41
+ securityContext: { runAsUser: 0 },
42
+ command: ['sh', '-c'],
43
+ // Never fails the Pod: an unchanged directory only means the child falls back (no cache · no throttle).
44
+ args: [`for d in ${paths}; do mkdir -p "$d" && chown 1000:1000 "$d" && chmod 0775 "$d" || echo "[host-dirs] could not prepare $d"; done; exit 0`],
45
+ volumeMounts: mounts.map((mount) => ({ ...mount })),
46
+ };
47
+ }
48
+
49
+ const int = (value: number, label: string) => {
50
+ if (!Number.isSafeInteger(value) || value < 1) throw new Error(`invalid ${label}: ${value}`);
51
+ return value;
52
+ };
53
+
54
+ /**
55
+ * Bash that defines `install_slot_acquire` / `install_slot_release`. Acquire waits for one of `slots` tokens under `dir`
56
+ * (polling every `pollSeconds`, default 2 s), reclaims a token older than `staleSeconds` (minute granularity), and gives up after `waitSeconds` (install runs anyway).
57
+ * It prints `[gate] install-slot <n> waited <s>s` or `[gate] install-slot none …` for the shard log.
58
+ */
59
+ export function installSlotScript(o: { dir?: string; slots: number; waitSeconds?: number; staleSeconds?: number; pollSeconds?: number }): string {
60
+ const dir = `'${(o.dir ?? POD_INSTALL_SLOTS_MOUNT).replace(/'/g, `'\\''`)}'`;
61
+ const slots = int(o.slots, 'install slots');
62
+ const wait = int(o.waitSeconds ?? 600, 'install slot wait');
63
+ const poll = o.pollSeconds ?? 2;
64
+ if (!Number.isFinite(poll) || poll <= 0 || poll > 60) throw new Error(`invalid install slot poll: ${poll}`);
65
+ const staleMinutes = Math.max(1, Math.ceil(int(o.staleSeconds ?? 900, 'install slot stale age') / 60));
66
+ return [
67
+ 'install_slot=',
68
+ 'install_slot_acquire() {',
69
+ ` local d=${dir} t0 now n`,
70
+ ' t0=$(date +%s)',
71
+ ' if [ ! -d "$d" ] || [ ! -w "$d" ]; then echo "[gate] install-slot none (directory not writable)"; return 0; fi',
72
+ ' while :; do',
73
+ ` for n in $(seq 0 ${slots - 1}); do`,
74
+ ' if mkdir "$d/slot-$n" 2>/dev/null; then',
75
+ ' install_slot="$d/slot-$n"',
76
+ ' echo "[gate] install-slot $n waited $(( $(date +%s) - t0 ))s"; return 0',
77
+ ' fi',
78
+ // A token whose holder died (directory mtime older than the stale age · `find -mmin` is the same on GNU and BSD) is
79
+ // renamed away first (rename is atomic: one reclaimer wins) and the slot is tried again on the next pass.
80
+ ` if [ -n "$(find "$d/slot-$n" -maxdepth 0 -mmin +${staleMinutes} 2>/dev/null)" ]; then mv "$d/slot-$n" "$d/stale-$n-$$" 2>/dev/null && rm -rf "$d/stale-$n-$$"; fi`,
81
+ ' done',
82
+ ' now=$(date +%s)',
83
+ ` if [ $(( now - t0 )) -ge ${wait} ]; then echo "[gate] install-slot none (waited $(( now - t0 ))s)"; return 0; fi`,
84
+ ` sleep ${poll}`,
85
+ ' done',
86
+ '}',
87
+ 'install_slot_release() { if [ -n "$install_slot" ]; then rm -rf "$install_slot"; install_slot=; fi; }',
88
+ ].join('\n');
89
+ }
@@ -52,6 +52,18 @@ export interface PodLeaseMember {
52
52
  */
53
53
  harnessUsageBytes?: number[] | null;
54
54
  usageReason?: string | null;
55
+ /** GATE-RESERVE-AUTO — non-terminal release-gate shard Jobs (`gate-<uuid>`, kind=command). */
56
+ gateJobs?: number | null;
57
+ /** GATE-RESERVE-AUTO — of those, Jobs whose Pod is not bound to a node yet (not created, or Pending unscheduled). */
58
+ gateUnboundJobs?: number | null;
59
+ /** GATE-RESERVE-AUTO — CPU requested by the non-terminal gate Jobs' Pod templates (observability; Infinity: unreadable). */
60
+ gateCpuMillicores?: number | null;
61
+ /** GATE-RESERVE-AUTO — allocatable CPU of the schedulable (Ready, untainted, cordon-free) nodes. */
62
+ schedulableCpuMillicores?: number | null;
63
+ /** GATE-RESERVE-AUTO — per schedulable node: allocatable CPU − requests of every non-terminal Pod bound to it (gate Pods too). */
64
+ cpuFreeByNodeMillicores?: number[] | null;
65
+ /** GATE-RESERVE-AUTO — CPU requested by harness Pods still waiting for a node (they take CPU somewhere once bound). */
66
+ pendingHarnessCpuMillicores?: number | null;
55
67
  /** Pool-wide harness reservation: Σ min(limit, max(request, usage×1.5)), or max(request, limit) when the Pod's usage is unmeasured. */
56
68
  memoryReservedBytes?: number | null;
57
69
  /** Parallel to availableMemoryByNodeBytes: each schedulable node's allocatable memory. */
@@ -138,16 +150,19 @@ function object(value: unknown): Record<string, unknown> | null {
138
150
  return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
139
151
  }
140
152
 
141
- /** Effective scheduling reservation: max(sum of app requests, largest init request, Pod-level request) + overhead. */
142
- function requestedMemory(spec: Record<string, unknown>): number | null {
153
+ /**
154
+ * Effective scheduling reservation: max(sum of app requests, largest init request, Pod-level request) + overhead.
155
+ * A container without a request reserves its limit (Kubernetes defaults the request to the limit), or 0 with neither.
156
+ */
157
+ function requestedResource(spec: Record<string, unknown>, unit: 'memory' | 'cpu'): number | null {
143
158
  const request = (resources: unknown): number | null => {
144
159
  const r = object(resources);
145
160
  if (resources !== undefined && !r) return null;
146
161
  const requests = object(r?.requests);
147
162
  const limits = object(r?.limits);
148
163
  if ((r?.requests !== undefined && !requests) || (r?.limits !== undefined && !limits)) return null;
149
- const raw = requests?.memory ?? limits?.memory;
150
- return raw === undefined ? 0 : typeof raw === 'string' ? quantity(raw, 'memory') : null;
164
+ const raw = requests?.[unit] ?? limits?.[unit];
165
+ return raw === undefined ? 0 : typeof raw === 'string' ? quantity(raw, unit) : null;
151
166
  };
152
167
  const containers = spec.containers;
153
168
  if (!Array.isArray(containers) || !containers.length) return null;
@@ -167,12 +182,16 @@ function requestedMemory(spec: Record<string, unknown>): number | null {
167
182
  init = Math.max(init, v);
168
183
  }
169
184
  const pod = spec.resources === undefined ? 0 : request(spec.resources);
170
- const overheadRaw = object(spec.overhead)?.memory;
185
+ const overheadRaw = object(spec.overhead)?.[unit];
171
186
  const overhead = spec.overhead !== undefined && !object(spec.overhead) ? null
172
- : overheadRaw === undefined ? 0 : typeof overheadRaw === 'string' ? quantity(overheadRaw, 'memory') : null;
187
+ : overheadRaw === undefined ? 0 : typeof overheadRaw === 'string' ? quantity(overheadRaw, unit) : null;
173
188
  return pod === null || overhead === null ? null : Math.max(app, init, pod) + overhead;
174
189
  }
175
190
 
191
+ const requestedMemory = (spec: Record<string, unknown>): number | null => requestedResource(spec, 'memory');
192
+ /** GATE-RESERVE-AUTO — the CPU the scheduler reserves for a Pod (same rule as memory). */
193
+ const requestedCpu = (spec: Record<string, unknown>): number | null => requestedResource(spec, 'cpu');
194
+
176
195
  function runningLimit(spec: Record<string, unknown>): number | null {
177
196
  if (!Array.isArray(spec.containers) || !spec.containers.length) return null;
178
197
  let total = 0;
@@ -325,6 +344,8 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
325
344
  };
326
345
  const nodeItems = parsedItems(nodes);
327
346
  const nodeFree = new Map<string, number>();
347
+ let schedulableCpu = 0;
348
+ const nodeCpuFree = new Map<string, number>();
328
349
  const nodeNames = new Set<string>();
329
350
  if (!nodeItems?.length) reasons.push(`cluster nodes: ${nodes.status === 0 ? 'invalid/empty response' : nodes.stderr.trim().split('\n').pop() || `rc=${nodes.status}`}`);
330
351
  else {
@@ -350,11 +371,11 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
350
371
  }
351
372
  nodeNames.add(name);
352
373
  const repels = taintList.some((t) => t!.effect === 'NoSchedule' || t!.effect === 'NoExecute');
353
- if (spec?.unschedulable !== true && !repels && object(ready[0])?.status === 'True') { nodeFree.set(name, mem); nodeAllocatable.set(name, mem); }
374
+ if (spec?.unschedulable !== true && !repels && object(ready[0])?.status === 'True') { nodeFree.set(name, mem); nodeAllocatable.set(name, mem); nodeCpuFree.set(name, cores); schedulableCpu += cores; }
354
375
  memory += mem; cpu += cores;
355
376
  }
356
- if (valid) { result.allocatableMemoryBytes = memory; result.allocatableCpuMillicores = cpu; }
357
- else { nodeFree.clear(); nodeAllocatable.clear(); nodeNames.clear(); reasons.push('cluster nodes: invalid name/allocatable memory/cpu/readiness'); }
377
+ if (valid) { result.allocatableMemoryBytes = memory; result.allocatableCpuMillicores = cpu; result.schedulableCpuMillicores = schedulableCpu; }
378
+ else { nodeFree.clear(); nodeAllocatable.clear(); nodeCpuFree.clear(); nodeNames.clear(); reasons.push('cluster nodes: invalid name/allocatable memory/cpu/readiness'); }
358
379
  }
359
380
  // POD-ADMIT-BY-USAGE: every schedulable node needs a real-usage reading, or the member stays on limits.
360
381
  const unreadNode = podUsage && nodeUsage ? [...nodeFree.keys()].find((n) => !nodeUsage!.has(n)) : undefined;
@@ -366,6 +387,8 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
366
387
  if (podItems && jobItems) {
367
388
  const jobNames = new Set<string>();
368
389
  const leasedJobs = new Set<string>();
390
+ const gateJobs = new Map<string, number>();
391
+ const boundGateJobs = new Set<string>();
369
392
  let jobsValid = true;
370
393
  for (const job of jobItems) {
371
394
  const meta = object(job.metadata);
@@ -373,12 +396,27 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
373
396
  if (object(meta.labels)?.['elanous.substrate'] !== 'pod') continue;
374
397
  if (typeof meta.name !== 'string' || !meta.name) { jobsValid = false; continue; }
375
398
  jobNames.add(meta.name);
399
+ // GATE-RESERVE-AUTO: a release-gate shard Job (`gate-<uuid>`, kind=command) reserves its Pod template's CPU
400
+ // from creation until it is terminal — also while its Pod is still Pending or not created yet.
401
+ if (meta.name.startsWith('gate-') && object(meta.labels)?.['elanous.kind'] === 'command') {
402
+ const status = object(job.status);
403
+ const conditions = Array.isArray(status?.conditions) ? status.conditions as unknown[] : [];
404
+ const terminal = !!status?.completionTime || Number(status?.succeeded) > 0 ||
405
+ conditions.some((c) => (object(c)?.type === 'Failed' || object(c)?.type === 'Complete') && object(c)?.status === 'True');
406
+ if (!terminal) {
407
+ const spec = object(object(object(job.spec)?.template)?.spec);
408
+ // An unreadable request cannot be reserved as 0: it blocks harness admission until the gate ends.
409
+ gateJobs.set(meta.name, (spec ? requestedCpu(spec) : null) ?? Number.POSITIVE_INFINITY);
410
+ }
411
+ }
376
412
  if (object(meta.annotations)?.[POD_HOST_LEASE_ANNOTATION] === 'true') leasedJobs.add(meta.name);
377
413
  }
378
414
  if (!jobsValid) reasons.push('cluster jobs: missing name/labels');
379
415
  let running = 0, pending = 0, unleasedRunning = 0, limits = 0, reserved = 0, limitsValid = true, reservationsValid = nodeItems !== null && nodeNames.size === nodeItems.length;
380
416
  const pendingJobs = new Map<string, PendingJobIdentity>();
381
417
  let unknownPhase = false;
418
+ let pendingHarnessCpu = 0;
419
+ let cpuValid = true;
382
420
  const seenPods = new Set<string>();
383
421
  for (const pod of podItems) {
384
422
  const metadata = object(pod.metadata);
@@ -393,7 +431,9 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
393
431
  if (seenPods.has(identity)) { reservationsValid = false; continue; }
394
432
  seenPods.add(identity);
395
433
  } else reservationsValid = false;
396
- const harness = metadata?.namespace === 'elanous-test' && labels?.['elanous.probe'] !== 'dns' &&
434
+ const ownerJob = String(labels?.['elanous.job'] ?? labels?.['job-name'] ?? '');
435
+ const gatePod = metadata?.namespace === 'elanous-test' && gateJobs.has(ownerJob);
436
+ const harness = metadata?.namespace === 'elanous-test' && labels?.['elanous.probe'] !== 'dns' && !gatePod &&
397
437
  (labels?.['elanous.substrate'] === 'pod' || jobNames.has(String(labels?.['elanous.job'] ?? '')));
398
438
  const spec = object(pod.spec);
399
439
  let limit: number | null = null;
@@ -425,6 +465,17 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
425
465
  }
426
466
  // Pending Pods with no assigned node have no node reservation yet. They already occupy a pool slot.
427
467
  const nodeName = spec?.nodeName;
468
+ // GATE-RESERVE-AUTO: CPU is placed per node (a free core split across two nodes is not one slot). Every Pod
469
+ // bound to a schedulable node — gate Pods too — takes its request there; harness Pods without a node yet
470
+ // take CPU somewhere once bound. A gate Pod bound to any node marks its Job as placed.
471
+ const boundNode = typeof nodeName === 'string' && nodeName !== '' ? nodeName : null;
472
+ if (gatePod && boundNode !== null && nodeNames.has(boundNode)) boundGateJobs.add(ownerJob);
473
+ if ((boundNode !== null && nodeCpuFree.has(boundNode)) || (boundNode === null && harness && phase === 'Pending')) {
474
+ const cpu = spec ? requestedCpu(spec) : null;
475
+ if (cpu === null) cpuValid = false;
476
+ else if (boundNode !== null) nodeCpuFree.set(boundNode, nodeCpuFree.get(boundNode)! - cpu);
477
+ else pendingHarnessCpu += cpu;
478
+ }
428
479
  if ((nodeName === undefined || nodeName === '') && phase === 'Pending' && spec && typeof metadata?.namespace === 'string' &&
429
480
  requestedMemory(spec) !== null) continue;
430
481
  const requested = spec ? requestedMemory(spec) : null;
@@ -432,6 +483,15 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
432
483
  if (nodeFree.has(nodeName)) nodeFree.set(nodeName, nodeFree.get(nodeName)! - Math.max(requested, reserve ?? limit ?? 0));
433
484
  }
434
485
  if (unknownPhase) reasons.push('cluster pods: missing/Unknown phase');
486
+ if (jobsValid && !unknownPhase && result.schedulableCpuMillicores !== undefined) {
487
+ result.gateJobs = gateJobs.size;
488
+ result.gateUnboundJobs = [...gateJobs.keys()].filter((job) => !boundGateJobs.has(job)).length;
489
+ result.gateCpuMillicores = [...gateJobs.values()].reduce((sum, cpu) => sum + cpu, 0);
490
+ }
491
+ if (cpuValid && !unknownPhase && result.schedulableCpuMillicores !== undefined) {
492
+ result.cpuFreeByNodeMillicores = [...nodeCpuFree.values()];
493
+ result.pendingHarnessCpuMillicores = pendingHarnessCpu;
494
+ }
435
495
  if (jobsValid && !unknownPhase) { result.running = running; result.pending = pending; result.pendingJobs = [...pendingJobs.values()]; result.unleasedRunning = unleasedRunning; }
436
496
  if (limitsValid && jobsValid && !unknownPhase) {
437
497
  result.memoryLimitBytes = limits;
@@ -464,6 +524,29 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
464
524
  }) };
465
525
  }
466
526
 
527
+ /**
528
+ * GATE-RESERVE-AUTO — while a release-gate shard Job is not terminal, how many more harness Pods (each requesting
529
+ * `POD_CHILD_REQUESTS.cpu`) fit beside it. The gate gets its CPU first: while any gate shard is still waiting for a
530
+ * node, no harness Pod is admitted (the scheduler may place new harness Pods where the shard would have gone). Once
531
+ * every shard is bound its CPU sits on its node, and the answer is Σ per-node ⌊free ÷ request⌋ minus the harness Pods
532
+ * still waiting for a node. `undefined`: no gate Job is active (normal admission) · `null`: a gate is active but CPU
533
+ * could not be measured — a Pod's or a gate template's request is unreadable (admit nothing more). The count comes from this one reading, never from earlier ones.
534
+ */
535
+ export function harnessCpuSlotsBesideGate(measure: PoolLeaseMeasure): number | null | undefined {
536
+ if (!measure.members.some((m) => (m.gateJobs ?? 0) > 0)) return undefined;
537
+ const perPod = quantity(POD_CHILD_REQUESTS.cpu, 'cpu')!;
538
+ let slots = 0;
539
+ for (const m of measure.members) {
540
+ if (m.gateJobs == null || m.gateUnboundJobs == null || m.cpuFreeByNodeMillicores == null || m.pendingHarnessCpuMillicores == null) return null;
541
+ // A gate Job whose Pod template CPU cannot be read is unmeasured even when its Pod is bound (fail closed).
542
+ if (m.gateCpuMillicores == null || !Number.isFinite(m.gateCpuMillicores)) return null;
543
+ if (m.gateUnboundJobs > 0) return 0;
544
+ const placeable = m.cpuFreeByNodeMillicores.reduce((sum, free) => sum + Math.max(0, Math.floor(free / perPod)), 0);
545
+ slots += Math.max(0, placeable - Math.ceil(m.pendingHarnessCpuMillicores / perPod));
546
+ }
547
+ return slots;
548
+ }
549
+
467
550
  /** Per-node memory and per-member slots cannot be exchanged across nodes/clusters. */
468
551
  export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capacity: number; perGoalMemory?: string; liteGoalMemory?: string; accounts: number; perAccount: number }): PoolLeaseRecommendation {
469
552
  const empty: PoolLeaseRecommendation = { recommended: null, limitedBy: null, reason: null, capacitySlots: null, memorySlots: null, liteMemorySlots: null, placeableSlots: null, accountSlots: null, running: null, pending: null, unleasedRunning: null };