elanous 0.2.10 → 0.2.11

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 (584) 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/Y4zKzL62sakG5momz3SoQ/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1394-556de6d71bbdb0fb.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
  7. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
  20. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
  25. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8605-ea5bee9dfae512c7.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-6a061bd97f0b9398.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-54b32bd27726588d.js → page-b12b6d2f8d2d1aea.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/chat/page-7c97dcdfe2e37a60.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/control/page-5721537ca8a3598c.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/design-check/page-b654240a572e4c48.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/editor/page-b9750abc897508c3.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/intake/page-c8b1ccda6ba7ae98.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/market/page-9688f5b4f1ff950f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/morning/page-35414e0a007b4082.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e001823c35c9dd67.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a24072b35470157.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d37cb0d52f9b6bd5.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/sessions/page-686da0df9bf23e5c.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/setup/page-239c17b911b84e72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/tasks/page-1218753c82d585f6.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/term/page-4b5935e67531efb5.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/today/page-3285bc87caf1eaa5.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/trace/page-ad553b0e06505ecd.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
  105. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -0
  134. package/apps/pwa/out/inside/index.txt +31 -0
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/market/index.html +1 -1
  140. package/apps/pwa/out/market/index.txt +22 -22
  141. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  142. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  143. package/apps/pwa/out/morning/index.html +1 -1
  144. package/apps/pwa/out/morning/index.txt +23 -23
  145. package/apps/pwa/out/observatory/index.html +1 -1
  146. package/apps/pwa/out/observatory/index.txt +23 -23
  147. package/apps/pwa/out/ops/checklist/index.html +1 -1
  148. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  149. package/apps/pwa/out/ops/release/index.html +1 -1
  150. package/apps/pwa/out/ops/release/index.txt +22 -22
  151. package/apps/pwa/out/ops/seats/index.html +1 -1
  152. package/apps/pwa/out/ops/seats/index.txt +22 -22
  153. package/apps/pwa/out/outputs/index.html +1 -0
  154. package/apps/pwa/out/outputs/index.txt +30 -0
  155. package/apps/pwa/out/reflection/index.html +1 -1
  156. package/apps/pwa/out/reflection/index.txt +23 -23
  157. package/apps/pwa/out/scheduler/index.html +1 -1
  158. package/apps/pwa/out/scheduler/index.txt +23 -23
  159. package/apps/pwa/out/sessions/index.html +1 -1
  160. package/apps/pwa/out/sessions/index.txt +22 -22
  161. package/apps/pwa/out/settings/devices/index.html +1 -1
  162. package/apps/pwa/out/settings/devices/index.txt +23 -23
  163. package/apps/pwa/out/settings/index.html +1 -1
  164. package/apps/pwa/out/settings/index.txt +23 -23
  165. package/apps/pwa/out/setup/done/index.html +1 -1
  166. package/apps/pwa/out/setup/done/index.txt +23 -23
  167. package/apps/pwa/out/setup/index.html +1 -1
  168. package/apps/pwa/out/setup/index.txt +23 -23
  169. package/apps/pwa/out/share/index.html +1 -1
  170. package/apps/pwa/out/share/index.txt +23 -23
  171. package/apps/pwa/out/showroom/index.html +1 -1
  172. package/apps/pwa/out/showroom/index.txt +22 -22
  173. package/apps/pwa/out/tasks/index.html +1 -1
  174. package/apps/pwa/out/tasks/index.txt +23 -23
  175. package/apps/pwa/out/term/index.html +1 -1
  176. package/apps/pwa/out/term/index.txt +23 -24
  177. package/apps/pwa/out/today/index.html +1 -1
  178. package/apps/pwa/out/today/index.txt +23 -23
  179. package/apps/pwa/out/trace/index.html +1 -1
  180. package/apps/pwa/out/trace/index.txt +23 -23
  181. package/apps/pwa/out/vault/index.html +1 -1
  182. package/apps/pwa/out/vault/index.txt +23 -23
  183. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  184. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  185. package/apps/pwa/out/workflows/index.html +1 -1
  186. package/apps/pwa/out/workflows/index.txt +23 -23
  187. package/apps/pwa/out/workspace/index.html +1 -1
  188. package/apps/pwa/out/workspace/index.txt +23 -24
  189. package/apps/pwa/out/worktrees/index.html +1 -1
  190. package/apps/pwa/out/worktrees/index.txt +23 -23
  191. package/graphs/drafts/draft-cleanup.yaml +19 -0
  192. package/graphs/drafts/recipes.yaml +23 -0
  193. package/graphs/intake/intake-daily.yaml +2 -1
  194. package/graphs/ops/recipes.yaml +3 -0
  195. package/graphs/ops/secret-exposure.yaml +24 -0
  196. package/graphs/release/recipes.yaml +3 -0
  197. package/graphs/release/release-loop.yaml +4 -1
  198. package/graphs/video/explainer-line.yaml +2 -2
  199. package/package.json +2 -1
  200. package/plugins/elanous-hwp/README.md +25 -0
  201. package/plugins/elanous-hwp/cli.ts +22 -0
  202. package/plugins/elanous-hwp/converter.ts +237 -0
  203. package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
  204. package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
  205. package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
  206. package/plugins/elanous-hwp/plugin.json +16 -0
  207. package/plugins/elanous-hwp/plugin.ts +40 -0
  208. package/scripts/ci-public-export-leak-gate.ts +75 -14
  209. package/scripts/ci-public-leak-gate.ts +44 -9
  210. package/scripts/context-hooks/claude-stop.sh +4 -0
  211. package/scripts/context-hooks/emit.ts +24 -0
  212. package/scripts/context-hooks/post-tool-use.sh +4 -0
  213. package/scripts/coord-channel-watch.sh +12 -0
  214. package/scripts/coord-requests-bridge.ts +143 -0
  215. package/scripts/field-feed/lib.ts +4 -0
  216. package/scripts/fleet/node-setup.sh +20 -3
  217. package/scripts/homebrew-formula-bump.ts +69 -0
  218. package/scripts/marketing/measure-site-numbers.ts +169 -0
  219. package/scripts/measure-gate-shards.ts +309 -0
  220. package/scripts/mission-request-judge.ts +31 -1
  221. package/scripts/morning-report.ts +2 -2
  222. package/scripts/opportunity-scan.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +21 -4
  224. package/scripts/release-loop/cut-branch.ts +84 -0
  225. package/scripts/release-loop/gate-node.ts +14 -13
  226. package/scripts/release-loop/mac-smoke-node.ts +229 -0
  227. package/scripts/release-loop/node-verdict.ts +3 -3
  228. package/scripts/release-loop/tui-node-checks.ts +42 -0
  229. package/scripts/release-loop/tui-sim-node.ts +32 -8
  230. package/scripts/release-loop/unattended-release.ts +5 -3
  231. package/scripts/release-loop/version-node.ts +61 -10
  232. package/scripts/signal-digest-cycle.ts +2 -7
  233. package/scripts/signal-digest-photo.ts +11 -0
  234. package/scripts/tech1-public-measure.ts +94 -0
  235. package/scripts/test-diet/node.ts +1 -1
  236. package/scripts/tui-regress.ts +2 -2
  237. package/scripts/ux/demo-rehearsal.ts +243 -0
  238. package/scripts/ux/lib/rehearsal-verdict.ts +73 -0
  239. package/skills/diagram-master/SKILL.md +366 -0
  240. package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
  241. package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
  242. package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
  243. package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
  244. package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
  245. package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
  246. package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
  247. package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
  248. package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
  249. package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
  250. package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
  251. package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
  252. package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
  253. package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
  254. package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
  255. package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
  256. package/skills/diagram-master/references/examples/README.md +69 -0
  257. package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
  258. package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
  259. package/skills/diagram-master/references/pyproject.toml +16 -0
  260. package/skills/diagram-master/references/quality-checklist.md +33 -0
  261. package/skills/diagram-master/references/routing-guide.md +164 -0
  262. package/skills/diagram-master/references/uv.lock +1356 -0
  263. package/skills/google-workspace/SKILL.md +33 -0
  264. package/skills/google-workspace/scripts/gws-safe.sh +177 -0
  265. package/skills/grill-me/SKILL.md +1 -1
  266. package/skills/lecture-note-digitizer/SKILL.md +926 -0
  267. package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
  268. package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
  269. package/skills/lecture-note-digitizer/references/html-template.md +167 -0
  270. package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
  271. package/skills/omni-crawl/SKILL.md +21 -11
  272. package/skills/omni-crawl/scripts/main.ts +6 -6
  273. package/skills/omni-crawl/src/router.ts +3 -3
  274. package/skills/omni-digest/SKILL.md +17 -9
  275. package/skills/omni-digest/digest.md +2 -2
  276. package/skills/omni-digest/src/fetch.ts +30 -3
  277. package/skills/pitch-deck/SKILL.md +2 -2
  278. package/skills/pitch-deck/template.html +2 -2
  279. package/skills/youtube-master/SKILL.md +163 -0
  280. package/skills/youtube-master/package-lock.json +558 -0
  281. package/skills/youtube-master/package.json +13 -0
  282. package/skills/youtube-master/references/study-note-template.md +124 -0
  283. package/skills/youtube-master/scripts/main.ts +435 -0
  284. package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
  285. package/skills/youtube-master/src/audio.ts +63 -0
  286. package/skills/youtube-master/src/domain-staging.ts +50 -0
  287. package/skills/youtube-master/src/env.ts +36 -0
  288. package/skills/youtube-master/src/obsidian.ts +155 -0
  289. package/skills/youtube-master/src/router.ts +110 -0
  290. package/skills/youtube-master/src/stt.ts +199 -0
  291. package/skills/youtube-master/src/study-note.ts +175 -0
  292. package/skills/youtube-master/src/summarize.ts +358 -0
  293. package/skills/youtube-master/src/transcript.ts +76 -0
  294. package/skills/youtube-master/src/types.ts +91 -0
  295. package/skills/youtube-master/src/util.ts +93 -0
  296. package/skills/youtube-master/src/youtube.ts +70 -0
  297. package/skills/youtube-master/tsconfig.json +13 -0
  298. package/src/acp/server.ts +5 -2
  299. package/src/agent-mission/driver.ts +80 -10
  300. package/src/agent-mission/pty-decision.ts +71 -0
  301. package/src/agent-mission/review-loop.ts +30 -0
  302. package/src/autopilot/build/llm-conflict-merge.ts +16 -3
  303. package/src/autopilot/tool-registry.ts +2 -0
  304. package/src/boot/daemon-prompt-turn.ts +5 -0
  305. package/src/boot/daemon-runtime.ts +9 -3
  306. package/src/boot/daemon-tools/web-search.ts +3 -0
  307. package/src/card-followup/core.ts +109 -0
  308. package/src/chat/index.ts +1 -0
  309. package/src/cli/drive-input-jail.ts +101 -0
  310. package/src/cli/hooks-cli.ts +17 -3
  311. package/src/cli/make-cli.ts +43 -0
  312. package/src/cli/persona-cli.ts +90 -4
  313. package/src/cli/plugin-cli.ts +31 -22
  314. package/src/cli/pod-cli.ts +19 -9
  315. package/src/cli/pr-cli.ts +51 -15
  316. package/src/cli/pty-drive-cli.ts +41 -7
  317. package/src/cli/pty-drive-verdict.ts +40 -0
  318. package/src/cli/release-cli.ts +17 -4
  319. package/src/cli/setup-cli.ts +43 -1
  320. package/src/cli/skills-cli.ts +17 -1
  321. package/src/context-bus/claude-hooks-install.ts +10 -0
  322. package/src/context-bus/context-now-surfaces.ts +48 -0
  323. package/src/context-bus/context-now.ts +104 -0
  324. package/src/context-bus/coord-events.ts +2 -2
  325. package/src/context-bus/external-events.ts +52 -0
  326. package/src/context-bus/long-term-memory.ts +108 -0
  327. package/src/dashboard/index.ts +9 -4
  328. package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -1
  329. package/src/decisions/decision-cards.ts +10 -3
  330. package/src/decisions/decision-ledger.ts +13 -5
  331. package/src/discord/sprint21-runtime.ts +5 -1
  332. package/src/discord-self-message.ts +32 -0
  333. package/src/discord-slash-wire.ts +5 -1
  334. package/src/discord.ts +17 -1
  335. package/src/domains/core-tools.ts +3 -2
  336. package/src/domains/outbound-alert.ts +6 -1
  337. package/src/domains/telegram-kind-route.ts +51 -13
  338. package/src/exec-requests/default-outputs.ts +25 -0
  339. package/src/exec-requests/planner.ts +10 -2
  340. package/src/exec-requests/runner.ts +20 -6
  341. package/src/exec-requests/store.ts +3 -0
  342. package/src/execution-loop/launch-gate.ts +100 -0
  343. package/src/field/field-reel.ts +5 -1
  344. package/src/field-note/index.ts +201 -0
  345. package/src/field-note/transcribe.ts +248 -0
  346. package/src/graph-kinds/registry.ts +3 -3
  347. package/src/graph-runner/graph-cli.ts +61 -1
  348. package/src/graph-runner/graph-grow-llm.ts +37 -0
  349. package/src/graph-runner/graph-grow.ts +47 -0
  350. package/src/graph-runner/runner.ts +317 -25
  351. package/src/grok/agent-search.ts +8 -2
  352. package/src/guardian/secret-exposure-scan.ts +101 -0
  353. package/src/harness/harness-cli-command.ts +247 -7
  354. package/src/harness/harness-pod-dispatch.ts +43 -23
  355. package/src/harness/harness-seams.ts +38 -4
  356. package/src/harness/harness-stop.ts +23 -4
  357. package/src/harness/launch-registry.ts +91 -0
  358. package/src/harness/process-garbage.ts +5 -3
  359. package/src/hooks/dispatch.ts +82 -0
  360. package/src/hooks/expose.ts +254 -0
  361. package/src/hooks/github-shadow.ts +108 -0
  362. package/src/hooks/providers.ts +71 -5
  363. package/src/hooks/queue.ts +1 -1
  364. package/src/hooks/receiver.ts +21 -20
  365. package/src/index.ts +52 -14
  366. package/src/intake-plane/categorize.ts +18 -46
  367. package/src/intake-plane/discord-seat-work.ts +10 -0
  368. package/src/intake-plane/front-route-classifier.ts +33 -39
  369. package/src/intake-plane/persona-answer.ts +30 -0
  370. package/src/intake-plane/runtime-callables.ts +8 -1
  371. package/src/intake-plane/telegram-seat-work.ts +36 -2
  372. package/src/intake-plane/wish-folder.ts +67 -0
  373. package/src/intent-gate/gate.ts +30 -10
  374. package/src/live/detail-switch.ts +5 -0
  375. package/src/llm/difficulty.ts +57 -0
  376. package/src/llm/judge-layer.ts +101 -0
  377. package/src/llm/mission-router.ts +33 -19
  378. package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
  379. package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
  380. package/src/loops/contract/examples/steward.loop.yaml +50 -0
  381. package/src/loops/contract/loop-agent.schema.json +138 -0
  382. package/src/loops/contract/validate.ts +64 -0
  383. package/src/loops/registry.ts +20 -6
  384. package/src/market/signed-index.ts +4 -0
  385. package/src/maturity/bot-command-maturity.ts +45 -0
  386. package/src/maturity/feature-maturity.ts +26 -4
  387. package/src/model-tier/index.ts +2 -2
  388. package/src/model-tier/tier-resolver.ts +20 -20
  389. package/src/model-tier/types.ts +6 -6
  390. package/src/mss/logging/log-store.ts +15 -0
  391. package/src/nexus/api/config-model-tier.ts +5 -4
  392. package/src/nexus/api/fabric-plans.ts +53 -0
  393. package/src/nexus/api/harness-api.ts +5 -1
  394. package/src/nexus/api/health.ts +2 -0
  395. package/src/nexus/api/http-server.ts +36 -0
  396. package/src/nexus/api/inside-events.ts +226 -0
  397. package/src/nexus/api/meta-api.ts +21 -8
  398. package/src/nexus/api/missions.ts +6 -1
  399. package/src/nexus/api/ops-seats.ts +33 -0
  400. package/src/nexus/api/outputs-route.ts +44 -0
  401. package/src/nexus/api/seat-requests.ts +44 -2
  402. package/src/nexus/api/sessions-store.ts +17 -38
  403. package/src/nexus/api/sub-seat-charters.ts +34 -0
  404. package/src/nexus/api/tabs-mutations.ts +3 -1
  405. package/src/nexus/api/trace.ts +18 -6
  406. package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
  407. package/src/nexus/api/workflows.ts +80 -6
  408. package/src/nexus/index.ts +2 -0
  409. package/src/nexus/kinds/webterm.ts +2 -1
  410. package/src/nexus/webterm/pty.ts +6 -2
  411. package/src/onboarding/cli-auth-status.ts +80 -0
  412. package/src/onboarding/voice-ai.ts +3 -3
  413. package/src/org/seat-budget.ts +48 -0
  414. package/src/org/stall-escalation.ts +63 -0
  415. package/src/outputs/ledger.ts +120 -0
  416. package/src/persona/persona-todo.ts +79 -0
  417. package/src/persona/presets.ts +116 -2
  418. package/src/persona/team-set.ts +155 -0
  419. package/src/plugins/install/plugin-install.ts +14 -0
  420. package/src/plugins/maker/market-bundle.ts +109 -0
  421. package/src/plugins/maker/plugin-maker.ts +254 -11
  422. package/src/plugins/maker/wizard-generate.ts +103 -0
  423. package/src/pod-lease/admission.ts +150 -0
  424. package/src/pod-lease/dependency-state.ts +85 -0
  425. package/src/pod-lease/host-lease.ts +127 -0
  426. package/src/project/project-suggestion.ts +16 -0
  427. package/src/project/session-context.ts +24 -0
  428. package/src/release-loop/checklist.ts +2 -2
  429. package/src/release-loop/feature-store.ts +5 -3
  430. package/src/release-loop/next-md-merge.ts +58 -0
  431. package/src/release-loop/release-note.ts +59 -4
  432. package/src/seat-dispatch/seat-ask.ts +130 -0
  433. package/src/seat-dispatch/seat-questions.ts +100 -0
  434. package/src/seat-loop/seat-loop.ts +548 -29
  435. package/src/seat-loop/seat-report.ts +37 -2
  436. package/src/self-dev/dev-cli.ts +4 -1
  437. package/src/self-dev/dev-pipeline.ts +37 -1
  438. package/src/self-dev/draft-sweep.ts +38 -33
  439. package/src/self-dev/draft-triage-rules.ts +8 -5
  440. package/src/self-dev/fabric-plan-core.ts +297 -0
  441. package/src/self-dev/orchestrate.ts +36 -4
  442. package/src/self-dev/release-path-guard.ts +62 -0
  443. package/src/self-dev/shard-aggregate.ts +230 -0
  444. package/src/self-dev/shard-placement.ts +60 -0
  445. package/src/self-dev/shard-plan.ts +48 -0
  446. package/src/self-dev/shard-pod-run.ts +77 -0
  447. package/src/self-dev/shard-run.ts +105 -0
  448. package/src/self-implement/gate-baseline.ts +1 -1
  449. package/src/self-implement/gate-cli.ts +40 -1
  450. package/src/self-implement/gate-scope.ts +37 -7
  451. package/src/self-implement/graph-variant.ts +171 -0
  452. package/src/self-implement/harness-policy-gates.ts +1 -1
  453. package/src/self-implement/headless-elanous-driver.ts +22 -3
  454. package/src/self-implement/host-regate.ts +22 -1
  455. package/src/self-implement/importer-test-index.ts +2 -1
  456. package/src/self-implement/l8-merge-queue.ts +130 -0
  457. package/src/self-implement/orchestrator.ts +242 -9
  458. package/src/self-implement/route-consumer-test-index.ts +122 -0
  459. package/src/self-implement/seams.ts +72 -11
  460. package/src/self-implement/test-seams.ts +1 -0
  461. package/src/self-implement/uncommitted-work.ts +59 -0
  462. package/src/session/index.ts +36 -13
  463. package/src/skills/gift-install.ts +215 -0
  464. package/src/skills/tools/web-search.ts +4 -1
  465. package/src/steward/launch.ts +42 -26
  466. package/src/steward/steward-cards.ts +17 -9
  467. package/src/steward/triage.ts +46 -14
  468. package/src/task-cards/card-cli.ts +25 -1
  469. package/src/task-orchestrator/mission.ts +5 -0
  470. package/src/task-orchestrator/store.ts +12 -3
  471. package/src/task-orchestrator/surfaces/pod-command-job.ts +29 -7
  472. package/src/task-orchestrator/surfaces/pod-lease.ts +156 -19
  473. package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
  474. package/src/task-orchestrator/surfaces/self-implement-pod.ts +214 -40
  475. package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
  476. package/src/task-orchestrator/types.ts +2 -0
  477. package/src/telegram-card-followup.ts +16 -96
  478. package/src/telegram-commands.ts +10 -1
  479. package/src/telegram-project-command.ts +71 -0
  480. package/src/telegram-report.ts +21 -17
  481. package/src/telegram.ts +29 -3
  482. package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
  483. package/src/user-config.ts +102 -20
  484. package/src/version/packed-revision.json +1 -1
  485. package/src/web-search/grok.ts +7 -1
  486. package/src/web-search/index.ts +28 -1
  487. package/src/web-search/provider.ts +4 -0
  488. package/src/web-search/x-intent.ts +5 -0
  489. package/src/workflow-runtime/executor.ts +277 -47
  490. package/src/workflow-runtime/index.ts +1 -0
  491. package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
  492. package/src/workflow-runtime/plugin-kind-node.ts +1 -1
  493. package/src/workflow-runtime/schema.ts +79 -4
  494. package/src/workflow-runtime/types.ts +35 -4
  495. package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/2375-cb6debea56cce4fb.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  505. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  506. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
  525. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  533. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  534. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +0 -1
  535. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
  536. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
  537. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
  538. package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
  539. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
  540. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
  541. package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
  542. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
  543. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
  544. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +0 -1
  545. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  546. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +0 -1
  547. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
  548. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
  549. package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
  550. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
  551. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
  552. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
  553. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
  554. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
  555. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
  556. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
  557. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
  558. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
  559. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
  560. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
  561. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
  562. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
  563. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +0 -1
  564. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  565. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +0 -1
  566. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
  567. package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
  568. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
  569. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
  570. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
  571. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
  572. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
  573. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
  574. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
  575. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
  576. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +0 -1
  577. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  578. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  579. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +0 -1
  580. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  581. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  582. package/apps/pwa/out/_next/static/chunks/webpack-e5bea36f183b25e6.js +0 -1
  583. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +0 -1
  584. /package/apps/pwa/out/_next/static/{2DsiDfDCks44g1cPUMhiW → Y4zKzL62sakG5momz3SoQ}/_ssgManifest.js +0 -0
@@ -1,13 +1,22 @@
1
1
  import type { PodPoolMember, PoolKubectl } from './pod-pool.js';
2
+ import { debug } from '../../debug/log.js';
2
3
  import { spawnSync } from 'node:child_process';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { dlopen } from 'bun:ffi';
8
+ import { hostLeaseBaseDir } from '../../pod-lease/host-lease.js';
9
+
10
+ /** Persisted on a Job admitted through the host lease; the file lease is released after apply. */
11
+ export const POD_HOST_LEASE_ANNOTATION = 'elanous.dev/host-lease-admitted';
3
12
 
4
13
  // `get pods --all-namespaces -o json` on a busy cluster is several MB — the default spawnSync
5
14
  // buffer fails with ENOBUFS (10-03 node-b measured). Same proxy stripping as the Pod launch kubectl.
6
15
  const LEASE_KUBECTL_MAX_BUFFER = 256 * 1024 * 1024;
7
- export function leaseKubectl(args: readonly string[]): { status: number | null; stdout: string; stderr: string } {
16
+ export function leaseKubectl(args: readonly string[], input?: string): { status: number | null; stdout: string; stderr: string } {
8
17
  const env = { ...process.env };
9
18
  for (const k of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy']) delete env[k];
10
- const r = spawnSync('kubectl', [...args], { encoding: 'utf8', env, timeout: 120_000, maxBuffer: LEASE_KUBECTL_MAX_BUFFER });
19
+ const r = spawnSync('kubectl', [...args], { encoding: 'utf8', input, env, timeout: 120_000, maxBuffer: LEASE_KUBECTL_MAX_BUFFER });
11
20
  return { status: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error ? String(r.error) : '') };
12
21
  }
13
22
 
@@ -16,6 +25,8 @@ export interface PodLeaseMember {
16
25
  capacity: number;
17
26
  running: number | null;
18
27
  pending: number | null;
28
+ /** Running harness Pods whose Jobs predate host lease admission. */
29
+ unleasedRunning: number | null;
19
30
  memoryLimitBytes: number | null;
20
31
  /** All-node allocatable memory, before existing reservations. */
21
32
  allocatableMemoryBytes: number | null;
@@ -35,10 +46,14 @@ export interface PoolLeaseRecommendation {
35
46
  reason: string | null;
36
47
  capacitySlots: number | null;
37
48
  memorySlots: number | null;
49
+ /** POD7 — the same memory headroom counted in lite goals (2Gi) · observation only: `recommended` stays on the standard size. */
50
+ liteMemorySlots?: number | null;
38
51
  placeableSlots: number | null;
39
52
  accountSlots: number | null;
40
53
  running: number | null;
41
54
  pending: number | null;
55
+ /** Absent only for injected legacy status fixtures; real measurements always report a number or null. */
56
+ unleasedRunning?: number | null;
42
57
  }
43
58
 
44
59
  /** Kubernetes quantity to bytes (memory) or millicores (CPU). */
@@ -110,12 +125,114 @@ function runningLimit(spec: Record<string, unknown>): number | null {
110
125
  return total;
111
126
  }
112
127
 
113
- /** Read only: each cluster probe is kubectl get. Missing reservations cannot become zero. */
114
- export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl } = {}): PoolLeaseMeasure {
128
+ /** A short-lived Pod proves cluster DNS from inside the same namespace as harness Jobs. Image pull failures are not DNS failures. */
129
+ export type PoolDnsProbe = 'ready' | 'dns' | 'image' | 'unknown';
130
+ const DNS_CACHE_MS = 10 * 60_000;
131
+ const DNS_LOCK_WAIT_MS = 6 * 60_000;
132
+ // flock is released by the kernel on process exit, including a crash before any cache write.
133
+ // The lock file is never unlinked: unlinking it would let waiters lock different inodes.
134
+ const dnsFlock = process.platform === 'linux' || process.platform === 'darwin'
135
+ ? dlopen(process.platform === 'linux' ? 'libc.so.6' : 'libSystem.B.dylib', {
136
+ flock: { args: ['int', 'int'], returns: 'int' },
137
+ }).symbols.flock : null;
138
+
139
+ /** The cache and lock are shared by CLI processes, keyed by context rather than pool composition. */
140
+ export function probePoolDns(context: string, kubectl: PoolKubectl = leaseKubectl, options: { dir?: string; now?: () => number } = {}): PoolDnsProbe {
141
+ const dir = options.dir ?? hostLeaseBaseDir();
142
+ const now = options.now ?? Date.now;
143
+ const key = createHash('sha256').update(context).digest('hex');
144
+ const cache = join(dir, `dns-${key}.cache`);
145
+ const outcome = join(dir, `dns-${key}.outcome`);
146
+ const lock = join(dir, `dns-${key}.flock`);
147
+ const readRecord = (path: string): Record<string, unknown> | null => {
148
+ try { return object(JSON.parse(readFileSync(path, 'utf8'))); }
149
+ catch { return null; }
150
+ };
151
+ const cached = (): boolean => {
152
+ const record = readRecord(cache);
153
+ return record?.result === 'ready' && typeof record.at === 'number' &&
154
+ now() >= record.at && now() - record.at < DNS_CACHE_MS;
155
+ };
156
+ if (cached()) return 'ready';
157
+ const previous = readRecord(outcome);
158
+ try { mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { return 'unknown'; }
159
+ if (!dnsFlock) return 'unknown';
160
+ let fd: number;
161
+ try { fd = openSync(lock, 'a', 0o600); } catch { return 'unknown'; }
162
+ const deadline = Date.now() + DNS_LOCK_WAIT_MS;
163
+ let held = false;
164
+ try {
165
+ const sharedFailure = (): PoolDnsProbe | null => {
166
+ const record = readRecord(outcome);
167
+ // Only a caller that started before this generation was written can reuse it.
168
+ if (record?.generation !== undefined && record.generation !== previous?.generation &&
169
+ ['dns', 'image', 'unknown'].includes(String(record.result))) return record.result as PoolDnsProbe;
170
+ return null;
171
+ };
172
+ while (!held) {
173
+ if (cached()) return 'ready';
174
+ const shared = sharedFailure();
175
+ if (shared) return shared;
176
+ if (dnsFlock(fd, 2 | 4) === 0) { held = true; break; } // LOCK_EX | LOCK_NB
177
+ if (Date.now() >= deadline) return 'unknown';
178
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
179
+ }
180
+ if (cached()) return 'ready';
181
+ const shared = sharedFailure();
182
+ if (shared) return shared;
183
+ const result = runPoolDnsProbe(context, kubectl);
184
+ const destination = result === 'ready' ? cache : outcome;
185
+ const temp = `${destination}.${randomUUID()}.tmp`;
186
+ try {
187
+ writeFileSync(temp, JSON.stringify({ result, at: now(), generation: randomUUID() }), { mode: 0o600 });
188
+ renameSync(temp, destination);
189
+ } catch { try { rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ } }
190
+ return result;
191
+ } finally {
192
+ if (held) dnsFlock(fd, 8); // LOCK_UN
193
+ closeSync(fd);
194
+ }
195
+ }
196
+
197
+ function runPoolDnsProbe(context: string, kubectl: PoolKubectl): PoolDnsProbe {
198
+ const name = `elanous-dns-${randomUUID().slice(0, 12)}`;
199
+ const base = ['--context', context, '--request-timeout=10s'];
200
+ try {
201
+ const core = kubectl([...base, '-n', 'kube-system', 'get', 'deployment', 'coredns', '-o', 'json']);
202
+ if (core.status !== 0) return 'unknown';
203
+ const deployment = object(JSON.parse(core.stdout));
204
+ const spec = object(deployment?.spec);
205
+ const status = object(deployment?.status);
206
+ if (typeof spec?.replicas !== 'number' || spec.replicas < 1 || status?.readyReplicas !== spec.replicas) return 'dns';
207
+ const manifest = JSON.stringify({ apiVersion: 'v1', kind: 'Pod', metadata: { name, namespace: 'elanous-test', labels: { 'elanous.probe': 'dns' } }, spec: {
208
+ restartPolicy: 'Never', activeDeadlineSeconds: 40, automountServiceAccountToken: false,
209
+ containers: [{ name: 'dns', image: 'busybox:1.36', command: ['nslookup', 'kubernetes.default.svc.cluster.local'], resources: { requests: { cpu: '10m', memory: '16Mi' }, limits: { cpu: '100m', memory: '64Mi' } } }],
210
+ } });
211
+ if (kubectl([...base, 'create', '-f', '-'], manifest).status !== 0) return 'unknown';
212
+ const waited = kubectl([...base, '-n', 'elanous-test', 'wait', '--for=jsonpath={.status.phase}=Succeeded', `pod/${name}`, '--timeout=30s']);
213
+ const pod = kubectl([...base, '-n', 'elanous-test', 'get', `pod/${name}`, '-o', 'json']);
214
+ if (pod.status !== 0) return 'unknown';
215
+ const state = object(JSON.parse(pod.stdout));
216
+ const phase = object(state?.status)?.phase;
217
+ const statuses = object(state?.status)?.containerStatuses;
218
+ if (Array.isArray(statuses) && statuses.some((c) =>
219
+ ['ErrImagePull', 'ImagePullBackOff', 'InvalidImageName'].includes(String(object(object(object(c)?.state)?.waiting)?.reason)))) return 'image';
220
+ if (phase !== 'Succeeded' && phase !== 'Failed') return 'unknown';
221
+ if (waited.status !== 0 && phase !== 'Failed') return 'unknown';
222
+ const log = kubectl([...base, '-n', 'elanous-test', 'logs', `pod/${name}`]);
223
+ if (log.status !== 0) return 'unknown';
224
+ if (phase === 'Failed') return /can't resolve|server can't find|connection timed out|no servers could be reached|NXDOMAIN|SERVFAIL/iu.test(log.stdout) ? 'dns' : 'unknown';
225
+ return /\bName:\s*kubernetes\.default\.svc\.cluster\.local\b/u.test(log.stdout) && /\bAddress(?:es)?:\s*\S+/u.test(log.stdout) ? 'ready' : 'dns';
226
+ } catch { return 'unknown'; }
227
+ finally { try { kubectl([...base, '-n', 'elanous-test', 'delete', `pod/${name}`, '--ignore-not-found=true', '--wait=false']); } catch { /* best-effort cleanup */ } }
228
+ }
229
+
230
+ /** Each cluster reads resource reservations; DNS requires a bounded disposable Pod. */
231
+ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe } = {}): PoolLeaseMeasure {
115
232
  const kubectl = deps.kubectl ?? leaseKubectl;
116
233
  return { members: members.map((member) => {
117
234
  const base = ['--context', member.context, '--request-timeout=10s'];
118
- const result: PodLeaseMember = { context: member.context, capacity: member.capacity, running: null, pending: null, memoryLimitBytes: null, allocatableMemoryBytes: null, allocatableCpuMillicores: null, availableMemoryByNodeBytes: null, reason: null };
235
+ const result: PodLeaseMember = { context: member.context, capacity: member.capacity, running: null, pending: null, unleasedRunning: null, memoryLimitBytes: null, allocatableMemoryBytes: null, allocatableCpuMillicores: null, availableMemoryByNodeBytes: null, reason: null };
119
236
  try {
120
237
  const nodes = kubectl([...base, 'get', 'nodes', '-o', 'json']);
121
238
  const jobs = kubectl([...base, '-n', 'elanous-test', 'get', 'jobs', '-l', 'elanous.substrate=pod', '-o', 'json']);
@@ -165,6 +282,7 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
165
282
  if (!podItems) reasons.push(`cluster pods: ${pods.status === 0 ? 'invalid response' : pods.stderr.trim().split('\n').pop() || `rc=${pods.status}`}`);
166
283
  if (podItems && jobItems) {
167
284
  const jobNames = new Set<string>();
285
+ const leasedJobs = new Set<string>();
168
286
  let jobsValid = true;
169
287
  for (const job of jobItems) {
170
288
  const meta = object(job.metadata);
@@ -172,9 +290,10 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
172
290
  if (object(meta.labels)?.['elanous.substrate'] !== 'pod') continue;
173
291
  if (typeof meta.name !== 'string' || !meta.name) { jobsValid = false; continue; }
174
292
  jobNames.add(meta.name);
293
+ if (object(meta.annotations)?.[POD_HOST_LEASE_ANNOTATION] === 'true') leasedJobs.add(meta.name);
175
294
  }
176
295
  if (!jobsValid) reasons.push('cluster jobs: missing name/labels');
177
- let running = 0, pending = 0, limits = 0, limitsValid = true, reservationsValid = nodeItems !== null && nodeNames.size === nodeItems.length;
296
+ let running = 0, pending = 0, unleasedRunning = 0, limits = 0, limitsValid = true, reservationsValid = nodeItems !== null && nodeNames.size === nodeItems.length;
178
297
  let unknownPhase = false;
179
298
  const seenPods = new Set<string>();
180
299
  for (const pod of podItems) {
@@ -190,7 +309,7 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
190
309
  if (seenPods.has(identity)) { reservationsValid = false; continue; }
191
310
  seenPods.add(identity);
192
311
  } else reservationsValid = false;
193
- const harness = metadata?.namespace === 'elanous-test' &&
312
+ const harness = metadata?.namespace === 'elanous-test' && labels?.['elanous.probe'] !== 'dns' &&
194
313
  (labels?.['elanous.substrate'] === 'pod' || jobNames.has(String(labels?.['elanous.job'] ?? '')));
195
314
  const spec = object(pod.spec);
196
315
  let limit: number | null = null;
@@ -198,6 +317,8 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
198
317
  if (phase === 'Pending') pending++;
199
318
  else {
200
319
  running++;
320
+ const jobName = labels?.['elanous.job'] ?? labels?.['job-name'];
321
+ if (typeof jobName !== 'string' || !leasedJobs.has(jobName)) unleasedRunning++;
201
322
  limit = spec ? runningLimit(spec) : null;
202
323
  if (limit === null) limitsValid = false;
203
324
  else limits += limit;
@@ -212,15 +333,21 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
212
333
  if (nodeFree.has(nodeName)) nodeFree.set(nodeName, nodeFree.get(nodeName)! - Math.max(requested, limit ?? 0));
213
334
  }
214
335
  if (unknownPhase) reasons.push('cluster pods: missing/Unknown phase');
215
- if (jobsValid && !unknownPhase) { result.running = running; result.pending = pending; }
336
+ if (jobsValid && !unknownPhase) { result.running = running; result.pending = pending; result.unleasedRunning = unleasedRunning; }
216
337
  if (limitsValid && jobsValid && !unknownPhase) result.memoryLimitBytes = limits;
217
338
  else if (!limitsValid) reasons.push('cluster pods: Running Pod memory limit missing/invalid');
218
339
  if (reservationsValid && limitsValid && jobsValid && !unknownPhase) result.availableMemoryByNodeBytes = [...nodeFree.values()];
219
340
  else if (!reservationsValid && !unknownPhase) reasons.push('cluster reservations: node assignment or memory request/limit missing/invalid');
220
341
  }
221
342
  result.reason = reasons.length ? reasons.join('; ').slice(0, 240) : null;
343
+ if (nodeItems?.length && jobItems && podItems) {
344
+ let dnsState: PoolDnsProbe = 'unknown';
345
+ try { dnsState = (deps.dns ?? ((context) => probePoolDns(context, kubectl)))(member.context); } catch { /* probe could not be measured */ }
346
+ if (dnsState === 'dns') { result.capacity = 0; result.availableMemoryByNodeBytes = []; result.reason = 'dns'; }
347
+ else if (dnsState !== 'ready') { result.availableMemoryByNodeBytes = null; result.reason = `측정 불가: dns probe ${dnsState}`; }
348
+ }
222
349
  } catch (error) {
223
- result.running = null; result.pending = null; result.memoryLimitBytes = null;
350
+ result.running = null; result.pending = null; result.unleasedRunning = null; result.memoryLimitBytes = null;
224
351
  result.allocatableMemoryBytes = null; result.allocatableCpuMillicores = null; result.availableMemoryByNodeBytes = null;
225
352
  result.reason = `cluster: ${error instanceof Error ? error.message : String(error)}`.split('\n')[0]!.slice(0, 240);
226
353
  }
@@ -229,32 +356,42 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
229
356
  }
230
357
 
231
358
  /** Per-node memory and per-member slots cannot be exchanged across nodes/clusters. */
232
- export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capacity: number; perGoalMemory?: string; accounts: number; perAccount: number }): PoolLeaseRecommendation {
233
- const empty: PoolLeaseRecommendation = { recommended: null, limitedBy: null, reason: null, capacitySlots: null, memorySlots: null, placeableSlots: null, accountSlots: null, running: null, pending: null };
359
+ export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capacity: number; perGoalMemory?: string; liteGoalMemory?: string; accounts: number; perAccount: number }): PoolLeaseRecommendation {
360
+ 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 };
234
361
  const goalBytes = quantity(options.perGoalMemory ?? '16Gi', 'memory');
235
362
  if (goalBytes === null || goalBytes <= 0) return { ...empty, reason: '측정 불가: perGoalMemory' };
236
- if (!measure.members.length || measure.members.some((m) => m.running === null || m.pending === null || m.allocatableMemoryBytes === null || m.memoryLimitBytes === null || m.allocatableCpuMillicores === null || m.availableMemoryByNodeBytes === null)) {
363
+ if (!measure.members.length || measure.members.some((m) => m.running === null || m.pending === null || m.unleasedRunning === null || m.allocatableMemoryBytes === null || m.memoryLimitBytes === null || m.allocatableCpuMillicores === null || m.availableMemoryByNodeBytes === null)) {
237
364
  return { ...empty, reason: `측정 불가: cluster${measure.members.map((m) => m.reason ? ` ${m.context}: ${m.reason}` : '').join('')}` };
238
365
  }
239
366
  if (!Number.isSafeInteger(options.capacity) || options.capacity < 0) return { ...empty, reason: '측정 불가: capacity' };
240
367
  const running = measure.members.reduce((sum, m) => sum + m.running!, 0);
241
368
  const pending = measure.members.reduce((sum, m) => sum + m.pending!, 0);
242
- const capacitySlots = Math.max(0, Math.min(options.capacity - running - pending,
243
- measure.members.reduce((sum, m) => sum + Math.max(0, m.capacity - m.running! - m.pending!), 0)));
369
+ const unleasedRunning = measure.members.reduce((sum, m) => sum + m.unleasedRunning!, 0);
370
+ // Pool-wide free = sum of per-member free slots: a member over its cap counts as full, never negative,
371
+ // so it cannot zero another member's free slot (and its unleased Pods only consume its own free slots).
372
+ const memberFree = measure.members.map((m) => Math.max(0, m.capacity - m.running! - m.pending!));
373
+ const eligibleUnleasedRunning = measure.members.reduce((sum, m, i) => sum + (m.reason === 'dns' ? 0 : Math.min(m.unleasedRunning!, memberFree[i]!)), 0);
374
+ const eligibleOccupied = measure.members.reduce((sum, m) => sum + (m.reason === 'dns' ? 0 : Math.min(m.capacity, m.running! + m.pending!)), 0);
375
+ const capacitySlots = Math.max(0, Math.min(options.capacity - eligibleOccupied,
376
+ memberFree.reduce((sum, free) => sum + free, 0)));
244
377
  const memberMemorySlots = measure.members.map((m) => m.availableMemoryByNodeBytes!.reduce(
245
378
  (sum, bytes) => sum + Math.max(0, Math.floor(bytes / goalBytes)), 0));
246
379
  // Preserve the requested allocatable-minus-harness-limit metric. The node-local
247
380
  // placement bound additionally accounts for every namespace's reservations.
248
- const memorySlots = measure.members.reduce((sum, m) => sum + Math.max(0,
249
- Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) / goalBytes)), 0);
381
+ const memorySlots = measure.members.reduce((sum, m) => sum + (m.reason === 'dns' ? 0 : Math.max(0,
382
+ Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) / goalBytes))), 0);
250
383
  const placeableSlots = measure.members.reduce((sum, m, i) => sum + Math.min(
251
384
  Math.max(0, m.capacity - m.running! - m.pending!), memberMemorySlots[i]!,
252
385
  ), 0);
253
386
  // Account count is not a concurrency limit (same IP · own accounts, 10-03 decision) — observed only, never a bound.
254
387
  const accountSlots = [options.accounts, options.perAccount].every((n) => Number.isSafeInteger(n) && n >= 0)
255
388
  ? Math.max(0, options.accounts * options.perAccount - running) : null;
389
+ const liteBytes = quantity(options.liteGoalMemory ?? '2Gi', 'memory');
390
+ const liteMemorySlots = liteBytes === null || liteBytes <= 0 ? null : measure.members.reduce((sum, m) => sum + (m.reason === 'dns' ? 0 : Math.max(0,
391
+ Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) / liteBytes))), 0);
256
392
  const memoryBound = Math.min(memorySlots, placeableSlots);
257
- const recommended = Math.min(capacitySlots, memoryBound);
258
- const limitedBy = recommended === memoryBound && memoryBound < capacitySlots ? 'memory' : 'capacity';
259
- return { recommended, limitedBy, reason: null, capacitySlots, memorySlots, placeableSlots, accountSlots, running, pending };
393
+ const recommended = Math.max(0, Math.min(capacitySlots, memoryBound) - eligibleUnleasedRunning);
394
+ const limitedBy = memoryBound < capacitySlots ? 'memory' : 'capacity';
395
+ if (unleasedRunning > 0) debug.log('pod-lease', 'unleased', { unleasedRunning, running, recommended });
396
+ return { recommended, limitedBy, reason: measure.members.some((m) => m.reason === 'dns') ? 'dns' : null, capacitySlots, memorySlots, liteMemorySlots, placeableSlots, accountSlots, running, pending, unleasedRunning };
260
397
  }
@@ -1,21 +1,26 @@
1
1
  /**
2
2
  * ☸️ Pod 풀 — 여러 k8s 클러스터(k3d)를 «우선순위 ⊕ 노드별 상한»으로 묶는다 (대표 2026-09-25).
3
3
  *
4
- * 스펙: `ELANOUS_POD_POOL` 또는 `--pod-pool` = `컨텍스트[@ssh호스트][:상한]` 을 쉼표로, «앞이 우선».
5
- * 예) `pool-node-b@node-b:8`
4
+ * 스펙: `ELANOUS_POD_POOL` 또는 `--pod-pool` = `컨텍스트[@ssh호스트][:상한][#레지스트리주소]` 을 쉼표로, «앞이 우선».
5
+ * 예) `pool-node-b@node-b:8#k3d-elanous-registry:5051,pool-node-c@node-c:2#k3d-elanous-registry:5052`
6
6
  * ⭐ 운영 권장(대표 2026-09-25): M3 Ultra(node-b) «전용» — Pod 한 개가 6Gi 를 넘겨(OOM) 한도를 12Gi 로 올렸고, node-c(OrbStack VM 16GB)는 그 한 개도 빠듯해 기본 풀에서 뺀다(필요할 때만 명시) ·
7
7
  * 이 맥(mbp)은 브라우저·편집 프로그램으로 메모리가 모자라기 쉬워 «기본 풀에서 뺀다»(필요할 때만 `k3d-elanous-h1:1` 을 명시).
8
8
  * - 컨텍스트 kubectl 컨텍스트 이름(이 맥의 kubeconfig)
9
9
  * - @ssh호스트 원격 클러스터면 그 기계 — 이미지 판 대조·반입(`docker load` ⊕ `k3d image import`)에 쓴다
10
10
  * - 상한 이 노드에 동시에 둘 Job 수(기본 2)
11
11
  * Job 마다 앞 순위부터 «진행 중 < 상한»인 노드를 고른다. 다 차면 자리가 날 때까지 기다린다.
12
- * ⛔ 진행 중 수는 «이 프로세스가 띄운 것»만 센다 — 다른 호스트가 같은 클러스터에 띄운 Job 은 안 센다(알려진 한계).
12
+ * 노드 선택의 진행 중 수는 로컬 프로세스 기준이다. 발사 허가는 별도의 lease 측정으로
13
+ * 다른 호스트의 Running/Pending 및 메모리 예약까지 고려한다.
13
14
  * 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
14
15
  */
15
16
  import { spawn, spawnSync } from 'node:child_process';
16
17
  import { existsSync } from 'node:fs';
17
18
  import { join, resolve } from 'node:path';
18
19
  import { debug } from '../../debug/log.js';
20
+ import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
21
+ import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
22
+ import { HostPoolLease } from '../../pod-lease/host-lease.js';
23
+ import { measurePoolLease, recommendConcurrency, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
19
24
 
20
25
  export interface PodPoolMember {
21
26
  readonly context: string;
@@ -26,11 +31,17 @@ export interface PodPoolMember {
26
31
  readonly k3dCluster: string;
27
32
  /** 노드 로컬 레지스트리의 이미지(커밋 태그) — 있으면 Pod 가 이것을 pull 한다(델타 · 대표 2026-09-26). 없으면 반입된 로컬 이미지. */
28
33
  readonly imageRef?: string;
34
+ /** 클러스터 안 레지스트리 주소(host:port). 호스트 push/조회는 같은 포트의 localhost. */
35
+ readonly registry?: string;
29
36
  }
30
37
 
31
38
  /** 노드 로컬 레지스트리(`scripts/fleet/node-setup.sh` 5b) — 클러스터 안에서 부르는 이름. */
32
39
  export const NODE_REGISTRY = `k3d-elanous-registry:${process.env.ELANOUS_FLEET_REGISTRY_PORT ?? '5050'}`;
33
40
 
41
+ function memberRegistry(member: PodPoolMember): string {
42
+ return member.registry ?? NODE_REGISTRY;
43
+ }
44
+
34
45
  /** 노드 쪽 빌드가 실패했을 때, 같은 판을 굽는 다른 발사의 레지스트리 태그를 기다리는 한도. */
35
46
  const CONCURRENT_BUILD_WAIT_MS = 5 * 60_000;
36
47
  const CONCURRENT_BUILD_POLL_MS = 15_000;
@@ -41,15 +52,15 @@ const sleep = (ms: number): Promise<void> => new Promise((done) => setTimeout(do
41
52
  * 노드 쪽 빌드가 부딪혀 실패한 뒤, 같은 판을 굽는 다른 발사가 레지스트리에 이 커밋 태그를 올리는지 본다.
42
53
  * 생기면 그 imageRef. 한도까지 없으면 null. 간격은 시험이 짧게 주입한다.
43
54
  */
44
- async function waitForConcurrentBuild(member: PodPoolMember, commit: string | null, opts: { intervalMs?: number; maxMs?: number; now?: () => number; sleep?: (ms: number) => Promise<void>; run?: RemoteRun } = {}): Promise<{ imageRef: string | null; waitedMs: number }> {
55
+ async function waitForConcurrentBuild(member: PodPoolMember, commit: string | null, opts: { intervalMs?: number; maxMs?: number; now?: () => number; sleep?: (ms: number) => Promise<void>; run?: RemoteRun; image?: string } = {}): Promise<{ imageRef: string | null; waitedMs: number }> {
45
56
  const intervalMs = opts.intervalMs ?? CONCURRENT_BUILD_POLL_MS;
46
57
  const maxMs = opts.maxMs ?? CONCURRENT_BUILD_WAIT_MS;
47
- if (maxMs <= 0) return { imageRef: registryImageRef(member, commit, opts.run), waitedMs: 0 };
58
+ if (maxMs <= 0) return { imageRef: registryImageRef(member, commit, opts.run, opts.image), waitedMs: 0 };
48
59
  const now = opts.now ?? Date.now;
49
60
  const pause = opts.sleep ?? sleep;
50
61
  const started = now();
51
62
  for (;;) {
52
- const imageRef = registryImageRef(member, commit, opts.run);
63
+ const imageRef = registryImageRef(member, commit, opts.run, opts.image);
53
64
  const waitedMs = now() - started;
54
65
  if (imageRef || waitedMs >= maxMs) return { imageRef, waitedMs };
55
66
  const remaining = maxMs - waitedMs;
@@ -70,15 +81,17 @@ function defaultLocalImageInspect(image: string): { status: number | null; stdou
70
81
  }
71
82
 
72
83
  /** 이 커밋의 이미지가 노드 레지스트리에 있나 — 있으면 클러스터 안 이미지 주소. */
73
- export function registryImageRef(member: PodPoolMember, commit: string | null, run: RemoteRun = defaultRemoteRun): string | null {
84
+ export function registryImageRef(member: PodPoolMember, commit: string | null, run: RemoteRun = defaultRemoteRun, image = 'elanous-harness:local'): string | null {
74
85
  if (!member.sshHost || !commit) return null;
86
+ const imageName = image === 'elanous-harness-lite:local' ? 'elanous-harness-lite' : 'elanous-harness';
75
87
  const tag = commit.slice(0, 12);
76
- const port = process.env.ELANOUS_FLEET_REGISTRY_PORT ?? '5050';
77
- const r = run(member.sshHost, `curl -fsS http://localhost:${port}/v2/elanous-harness/tags/list 2>/dev/null`);
88
+ const registry = memberRegistry(member);
89
+ const port = registry.slice(registry.lastIndexOf(':') + 1);
90
+ const r = run(member.sshHost, `curl -fsS --max-time 5 http://localhost:${port}/v2/${imageName}/tags/list 2>/dev/null`);
78
91
  if (r.status !== 0) return null;
79
92
  try {
80
93
  const tags = (JSON.parse(r.stdout) as { tags?: string[] }).tags ?? [];
81
- return tags.includes(tag) ? `${NODE_REGISTRY}/elanous-harness:${tag}` : null;
94
+ return tags.includes(tag) ? `${registry}/${imageName}:${tag}` : null;
82
95
  } catch { return null; }
83
96
  }
84
97
 
@@ -90,17 +103,18 @@ export function remoteImageSkillsDigest(member: PodPoolMember, image: string, ru
90
103
  return v && v !== '<no value>' ? v : null;
91
104
  }
92
105
 
93
- const MEMBER = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:@([A-Za-z0-9][A-Za-z0-9._-]*))?(?::(\d+))?$/u;
106
+ const MEMBER = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:@([A-Za-z0-9][A-Za-z0-9._-]*))?(?::(\d+))?(?:#(k3d-[A-Za-z0-9][A-Za-z0-9._-]*):([1-9]\d{0,4}))?$/u;
94
107
 
95
108
  export function parsePodPool(spec: string): PodPoolMember[] {
96
109
  const members = spec.split(',').map((s) => s.trim()).filter(Boolean).map((part) => {
97
110
  const m = MEMBER.exec(part);
98
- if (!m) throw new Error(`--pod-pool: 못 읽는 노드 「${part}」 — 형식 컨텍스트[@ssh호스트][:상한]`);
111
+ if (!m) throw new Error(`--pod-pool: 못 읽는 노드 「${part}」 — 형식 컨텍스트[@ssh호스트][:상한][#k3d-레지스트리:포트]`);
99
112
  const capacity = m[3] === undefined ? 2 : Number(m[3]);
100
113
  if (!Number.isInteger(capacity) || capacity < 1) throw new Error(`--pod-pool: 상한은 1 이상 — 「${part}」`);
114
+ if (m[5] && Number(m[5]) > 65535) throw new Error(`--pod-pool: 레지스트리 포트는 65535 이하 — 「${part}」`);
101
115
  const context = m[1]!;
102
116
  const k3dCluster = context.startsWith('k3d-') ? context.slice(4) : 'elanous-pool';
103
- return { context, capacity, k3dCluster, ...(m[2] ? { sshHost: m[2] } : {}) };
117
+ return { context, capacity, k3dCluster, ...(m[2] ? { sshHost: m[2] } : {}), ...(m[4] ? { registry: `${m[4]}:${m[5]}` } : {}) };
104
118
  });
105
119
  if (members.length === 0) throw new Error('--pod-pool: 노드가 없다');
106
120
  if (new Set(members.map((m) => m.context)).size !== members.length) throw new Error('--pod-pool: 컨텍스트가 겹친다');
@@ -110,6 +124,7 @@ export function parsePodPool(spec: string): PodPoolMember[] {
110
124
  /** 스펙 해석 순서: 명시 인자 → `ELANOUS_POD_POOL` → 없음(null = 현재 컨텍스트 하나). */
111
125
  export function resolvePodPoolSpec(explicit: string | undefined, env: NodeJS.ProcessEnv = process.env, configPool: () => string | undefined = defaultConfigPool): string | null {
112
126
  // ⭐ «--substrate pod 만 써도 분배»(대표 2026-09-26) — 인자 → 환경 → 설정 `pod.pool` 순. 셋 다 없으면 현재 컨텍스트 하나.
127
+ // harness 입구는 `harness.podPool` 을 먼저 보는 자기 해석(하니스 실행 칸 기본값 해석)으로 풀을 «인자»로 넘긴다 — 범용 경로 계약은 그대로.
113
128
  const v = explicit?.trim() || env.ELANOUS_POD_POOL?.trim() || configPool()?.trim();
114
129
  return v ? v : null;
115
130
  }
@@ -118,14 +133,97 @@ function defaultConfigPool(): string | undefined {
118
133
  try {
119
134
  // eslint-disable-next-line @typescript-eslint/no-require-imports
120
135
  const { getUserConfig } = require('../../user-config.js') as typeof import('../../user-config.js');
121
- return getUserConfig().pod?.pool;
136
+ return genericConfigPodPool(getUserConfig());
122
137
  } catch { return undefined; }
123
138
  }
124
139
 
140
+ /** The generic Pod path reads only `pod.pool` (harness launches resolve `harness.podPool` themselves and pass it explicitly). */
141
+ export function genericConfigPodPool(config: { pod?: { pool?: string }; harness?: { podPool?: string } }): string | undefined {
142
+ return config.pod?.pool;
143
+ }
144
+
125
145
  /** 노드 자리 배분 — 우선순위 순서로 첫 빈 자리. 순수(시각·대기는 호출자가). */
126
146
  export class PodPoolScheduler {
127
147
  private readonly inflight = new Map<string, number>();
128
- constructor(readonly members: readonly PodPoolMember[]) {}
148
+ private readonly admission: PodLeaseAdmission;
149
+ private readonly outstanding = new Set<PodLeaseRelease>();
150
+ private lastFree = 0;
151
+ private budget = 0;
152
+ private initialized = false;
153
+ private readonly measure: () => Promise<PoolLeaseRecommendation>;
154
+ private lastRaw: PoolLeaseRecommendation | null = null;
155
+ private readonly hostLease: HostPoolLease;
156
+ private readonly pollMs: number;
157
+ 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> } = {}) {
158
+ this.pollMs = options.pollMs ?? 15_000;
159
+ // One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
160
+ this.hostLease = options.hostLease ?? new HostPoolLease([...members.map((m) => `${m.context}@${m.sshHost ?? ''}`)].sort().join(','));
161
+ const measureStatus = options.status ?? (() => {
162
+ const measure = measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) });
163
+ return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
164
+ });
165
+ this.measure = async () => measureStatus();
166
+ this.admission = new PodLeaseAdmission({ status: async () => {
167
+ const measured = await measureStatus();
168
+ this.lastRaw = measured;
169
+ // Launches admitted by other processes on this host but not yet visible as Jobs hold free slots too.
170
+ const others = measured.recommended === null ? 0 : this.hostLease.othersPending();
171
+ const decision = measured.recommended === null ? measured : { ...measured, recommended: Math.max(0, measured.recommended - others) };
172
+ // An unknown reading cannot erase permits already granted: on recovery,
173
+ // the first healthy recommendation must still account for their slots.
174
+ if (decision.recommended === null) return decision;
175
+ // A new launch consumes a measured additional slot. Compare consecutive
176
+ // measurements so repeated stale readings cannot grant that slot again.
177
+ this.budget = Math.max(0, this.budget + decision.recommended - (this.initialized ? this.lastFree : 0));
178
+ this.initialized = true;
179
+ this.lastFree = decision.recommended;
180
+ return { ...decision, recommended: this.admission.snapshot().active + this.budget };
181
+ }, ...(options.pollMs ? { pollMs: options.pollMs } : {}), dependencyMerged: options.dependencyMerged ?? ((after, signal) => predecessorState(after, {}, signal)) });
182
+ }
183
+ /** FIFO gate within this process, then a host lease so independent CLI processes cannot over-admit the same pool. */
184
+ async acquireAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease & { applied: () => void }> {
185
+ const local = await this.acquireLocalAdmission(signal, after);
186
+ try {
187
+ for (;;) {
188
+ if (signal?.aborted) throw new Error('pod lease admission aborted');
189
+ const free = this.lastRaw?.recommended ?? null;
190
+ // Same check as the local gate, but atomic across processes: other unapplied leases must stay under the free slots.
191
+ const lease = free !== null && free > 0 ? this.hostLease.tryReserve((others) => others < free) : null;
192
+ if (lease) {
193
+ let hostReleased = false;
194
+ const releaseHost = () => { if (!hostReleased) { hostReleased = true; lease(); } };
195
+ // Once the Job is applied the cluster measurement counts it — the host lease would double count.
196
+ return Object.assign(() => { releaseHost(); local(); }, { applied: releaseHost });
197
+ }
198
+ await new Promise<void>((resolve, reject) => {
199
+ const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, this.pollMs);
200
+ const onAbort = () => { clearTimeout(timer); reject(new Error('pod lease admission aborted')); };
201
+ signal?.addEventListener('abort', onAbort, { once: true });
202
+ });
203
+ try { this.lastRaw = await this.measure(); } catch { this.lastRaw = null; }
204
+ }
205
+ } catch (error) {
206
+ local();
207
+ throw error;
208
+ }
209
+ }
210
+ private acquireLocalAdmission(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease> {
211
+ return this.admission.acquire(signal, after).then((release) => {
212
+ this.budget = Math.max(0, this.budget - 1);
213
+ this.outstanding.add(release);
214
+ return () => {
215
+ if (!this.outstanding.delete(release)) return;
216
+ // A release only restores a locally reserved slot while the latest
217
+ // healthy measurement still reports free capacity. If the pool fell
218
+ // from N=2 to N=0, a release cannot manufacture a new permit.
219
+ if (this.lastFree > 0) this.budget = Math.min(this.lastFree, this.budget + 1);
220
+ release();
221
+ };
222
+ });
223
+ }
224
+ admissionSnapshot(): ReturnType<PodLeaseAdmission['snapshot']> {
225
+ return this.admission.snapshot();
226
+ }
129
227
  /** 자리가 있으면 그 노드를 잡고 돌려준다. 없으면 null. */
130
228
  tryAcquire(): PodPoolMember | null {
131
229
  for (const m of this.members) {
@@ -192,7 +290,7 @@ export function syncPoolImage(member: PodPoolMember, image: string, localCommit:
192
290
  : { ok: false, action: 'failed', detail: `보낸 뒤 판이 ${after ?? '없음'} — 기대 ${localCommit ?? '?'}` };
193
291
  }
194
292
 
195
- export type PoolKubectl = (args: readonly string[]) => { status: number | null; stdout: string; stderr: string };
293
+ export type PoolKubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
196
294
 
197
295
  /**
198
296
  * 발사 전 점검 — 노드마다 «컨텍스트가 닿나 · elanous-test 네임스페이스가 있나».
@@ -220,11 +318,14 @@ export function podImageBuildScript(cwd: string = process.cwd()): string | null
220
318
  return null;
221
319
  }
222
320
 
223
- type RemoteBuild = (host: string, cluster: string) => Promise<{ ok: boolean; detail: string }>;
321
+ type RemoteBuild = (host: string, cluster: string, registry?: string) => Promise<{ ok: boolean; detail: string }>;
224
322
 
225
- function defaultRemoteBuild(script: string): RemoteBuild {
226
- return (host, cluster) => new Promise((done) => {
227
- const child = spawn('bash', [script], { env: { ...process.env, ELANOUS_BUILD_REMOTE: host, ELANOUS_L2_CLUSTER: cluster }, stdio: ['ignore', 'pipe', 'pipe'] });
323
+ function defaultRemoteBuild(script: string, image: string): RemoteBuild {
324
+ return (host, cluster, registry) => new Promise((done) => {
325
+ const env: NodeJS.ProcessEnv = { ...process.env, ELANOUS_BUILD_REMOTE: host, ELANOUS_L2_CLUSTER: cluster, ELANOUS_BUILD_IMAGE: image === 'elanous-harness-lite:local' ? 'lite' : 'full' };
326
+ delete env.ELANOUS_BUILD_REGISTRY;
327
+ if (registry) env.ELANOUS_BUILD_REGISTRY = registry;
328
+ const child = spawn('bash', [script], { env, stdio: ['ignore', 'pipe', 'pipe'] });
228
329
  let out = '';
229
330
  child.stdout.on('data', (d) => { out += d; }); child.stderr.on('data', (d) => { out += d; });
230
331
  const timer = setTimeout(() => child.kill('SIGKILL'), 1_800_000);
@@ -241,7 +342,7 @@ function defaultRemoteBuild(script: string): RemoteBuild {
241
342
  export async function syncPoolImages(members: readonly PodPoolMember[], image: string, localCommit: string | null, deps: { run?: RemoteRun; remoteBuild?: RemoteBuild; buildScript?: string | null; ship?: typeof syncPoolImage; localSkillsDigest?: string | null; waitIntervalMs?: number; waitMaxMs?: number; now?: () => number; sleep?: (ms: number) => Promise<void>; inspect?: LocalImageInspect; transfer?: ImageShip } = {}): Promise<Map<string, PoolImageSync>> {
242
343
  const run = deps.run ?? defaultRemoteRun;
243
344
  const script = deps.buildScript === undefined ? podImageBuildScript() : deps.buildScript;
244
- const remoteBuild = deps.remoteBuild ?? (script ? defaultRemoteBuild(script) : null);
345
+ const remoteBuild = deps.remoteBuild ?? (script ? defaultRemoteBuild(script, image) : null);
245
346
  const ship = deps.ship ?? ((m: PodPoolMember, img: string, commit: string | null, remote: RemoteRun) => syncPoolImage(m, img, commit, { run: remote, inspect: deps.inspect, transfer: deps.transfer }));
246
347
  const results = new Map<string, PoolImageSync>();
247
348
  await Promise.all(members.map(async (m) => {
@@ -251,16 +352,16 @@ export async function syncPoolImages(members: readonly PodPoolMember[], image: s
251
352
  // ⭐ «같은 판» = 커밋 ⊕ 스킬 해시 ⊕ (레지스트리 노드면) 레지스트리에 그 커밋 태그가 있다 — 스킬만 바뀌어도 다시 굽는다(대표 2026-09-26 «스킬 셋트 싱크»).
252
353
  const skillsNow = deps.localSkillsDigest;
253
354
  const skillsSame = skillsNow == null || remoteImageSkillsDigest(m, image, run) === skillsNow;
254
- const refBefore = registryImageRef(m, localCommit, run);
255
- const hasRegistry = run(m.sshHost, 'docker inspect k3d-elanous-registry >/dev/null 2>&1').status === 0;
355
+ const refBefore = registryImageRef(m, localCommit, run, image);
356
+ const hasRegistry = run(m.sshHost, `docker inspect ${memberRegistry(m).split(':')[0]} >/dev/null 2>&1`).status === 0;
256
357
  if (localCommit && before === localCommit && skillsSame && (!hasRegistry || refBefore)) {
257
358
  results.set(m.context, { ok: true, action: 'fresh', detail: before.slice(0, 12), ms: Date.now() - t0, ...(refBefore ? { imageRef: refBefore } : {}) });
258
359
  return;
259
360
  }
260
361
  if (remoteBuild) {
261
- const b = await remoteBuild(m.sshHost, m.k3dCluster);
362
+ const b = await remoteBuild(m.sshHost, m.k3dCluster, m.registry);
262
363
  const after = remoteImageCommit(m, image, run);
263
- const refAfter = registryImageRef(m, localCommit ?? after, run);
364
+ const refAfter = registryImageRef(m, localCommit ?? after, run, image);
264
365
  // ⭐ 레지스트리 노드면 «이 판 커밋 태그가 레지스트리에 있나»로 판정한다 — Pod 는 그 커밋 태그를 pull 한다.
265
366
  // 🩸 2026-09-26(🅣 실측): 두 발사가 같은 노드를 동시에 구우면 가변 태그 `:local` 을 서로 덮어, 빌드는 성공했는데
266
367
  // `:local` 의 판이 남의 커밋이라 «못 맞췄다»로 노드를 뺐다(런이 시작도 못 함). 커밋 태그는 덮이지 않는다.
@@ -271,7 +372,7 @@ export async function syncPoolImages(members: readonly PodPoolMember[], image: s
271
372
  if (b.ok && after && (!localCommit || after === localCommit)) { results.set(m.context, { ok: true, action: 'built', ...(refAfter ? { imageRef: refAfter } : {}), detail: `${before?.slice(0, 12) ?? '없음'} → ${after.slice(0, 12)} (노드 쪽 빌드)`, ms: Date.now() - t0 }); return; }
272
373
  // 빌드가 성공하면 위에서 즉시 끝난다. 실패했을 때만, 통째 전송 전에 같은 판을 굽는 다른 발사의 태그를 기다린다.
273
374
  if (!b.ok) {
274
- const waited = await waitForConcurrentBuild(m, localCommit, { intervalMs: deps.waitIntervalMs, maxMs: deps.waitMaxMs, now: deps.now, sleep: deps.sleep, run });
375
+ const waited = await waitForConcurrentBuild(m, localCommit, { intervalMs: deps.waitIntervalMs, maxMs: deps.waitMaxMs, now: deps.now, sleep: deps.sleep, run, image });
275
376
  debug.log('self-implement.pod', 'pool-image-wait-concurrent', { context: m.context, commit: localCommit, waitedMs: waited.waitedMs, outcome: waited.imageRef ? 'built' : 'timeout' });
276
377
  if (waited.imageRef) {
277
378
  results.set(m.context, { ok: true, action: 'built', imageRef: waited.imageRef, detail: '동시 빌드 결과 사용', ms: Date.now() - t0 });