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.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/Y4zKzL62sakG5momz3SoQ/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1394-556de6d71bbdb0fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +1 -0
- package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8605-ea5bee9dfae512c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-6a061bd97f0b9398.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-54b32bd27726588d.js → page-b12b6d2f8d2d1aea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-7c97dcdfe2e37a60.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-5721537ca8a3598c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-b654240a572e4c48.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b9750abc897508c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-c8b1ccda6ba7ae98.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-9688f5b4f1ff950f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-35414e0a007b4082.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e001823c35c9dd67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a24072b35470157.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d37cb0d52f9b6bd5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-686da0df9bf23e5c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-239c17b911b84e72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-1218753c82d585f6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-4b5935e67531efb5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-3285bc87caf1eaa5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-ad553b0e06505ecd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -0
- package/apps/pwa/out/inside/index.txt +31 -0
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -0
- package/apps/pwa/out/outputs/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -24
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -24
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/drafts/draft-cleanup.yaml +19 -0
- package/graphs/drafts/recipes.yaml +23 -0
- package/graphs/intake/intake-daily.yaml +2 -1
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/ops/secret-exposure.yaml +24 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/video/explainer-line.yaml +2 -2
- package/package.json +2 -1
- package/plugins/elanous-hwp/README.md +25 -0
- package/plugins/elanous-hwp/cli.ts +22 -0
- package/plugins/elanous-hwp/converter.ts +237 -0
- package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
- package/plugins/elanous-hwp/plugin.json +16 -0
- package/plugins/elanous-hwp/plugin.ts +40 -0
- package/scripts/ci-public-export-leak-gate.ts +75 -14
- package/scripts/ci-public-leak-gate.ts +44 -9
- package/scripts/context-hooks/claude-stop.sh +4 -0
- package/scripts/context-hooks/emit.ts +24 -0
- package/scripts/context-hooks/post-tool-use.sh +4 -0
- package/scripts/coord-channel-watch.sh +12 -0
- package/scripts/coord-requests-bridge.ts +143 -0
- package/scripts/field-feed/lib.ts +4 -0
- package/scripts/fleet/node-setup.sh +20 -3
- package/scripts/homebrew-formula-bump.ts +69 -0
- package/scripts/marketing/measure-site-numbers.ts +169 -0
- package/scripts/measure-gate-shards.ts +309 -0
- package/scripts/mission-request-judge.ts +31 -1
- package/scripts/morning-report.ts +2 -2
- package/scripts/opportunity-scan.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +21 -4
- package/scripts/release-loop/cut-branch.ts +84 -0
- package/scripts/release-loop/gate-node.ts +14 -13
- package/scripts/release-loop/mac-smoke-node.ts +229 -0
- package/scripts/release-loop/node-verdict.ts +3 -3
- package/scripts/release-loop/tui-node-checks.ts +42 -0
- package/scripts/release-loop/tui-sim-node.ts +32 -8
- package/scripts/release-loop/unattended-release.ts +5 -3
- package/scripts/release-loop/version-node.ts +61 -10
- package/scripts/signal-digest-cycle.ts +2 -7
- package/scripts/signal-digest-photo.ts +11 -0
- package/scripts/tech1-public-measure.ts +94 -0
- package/scripts/test-diet/node.ts +1 -1
- package/scripts/tui-regress.ts +2 -2
- package/scripts/ux/demo-rehearsal.ts +243 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +73 -0
- package/skills/diagram-master/SKILL.md +366 -0
- package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
- package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
- package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
- package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
- package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
- package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
- package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
- package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
- package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
- package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
- package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
- package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
- package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
- package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
- package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
- package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
- package/skills/diagram-master/references/examples/README.md +69 -0
- package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
- package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
- package/skills/diagram-master/references/pyproject.toml +16 -0
- package/skills/diagram-master/references/quality-checklist.md +33 -0
- package/skills/diagram-master/references/routing-guide.md +164 -0
- package/skills/diagram-master/references/uv.lock +1356 -0
- package/skills/google-workspace/SKILL.md +33 -0
- package/skills/google-workspace/scripts/gws-safe.sh +177 -0
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/lecture-note-digitizer/SKILL.md +926 -0
- package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
- package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
- package/skills/lecture-note-digitizer/references/html-template.md +167 -0
- package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
- package/skills/omni-crawl/SKILL.md +21 -11
- package/skills/omni-crawl/scripts/main.ts +6 -6
- package/skills/omni-crawl/src/router.ts +3 -3
- package/skills/omni-digest/SKILL.md +17 -9
- package/skills/omni-digest/digest.md +2 -2
- package/skills/omni-digest/src/fetch.ts +30 -3
- package/skills/pitch-deck/SKILL.md +2 -2
- package/skills/pitch-deck/template.html +2 -2
- package/skills/youtube-master/SKILL.md +163 -0
- package/skills/youtube-master/package-lock.json +558 -0
- package/skills/youtube-master/package.json +13 -0
- package/skills/youtube-master/references/study-note-template.md +124 -0
- package/skills/youtube-master/scripts/main.ts +435 -0
- package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
- package/skills/youtube-master/src/audio.ts +63 -0
- package/skills/youtube-master/src/domain-staging.ts +50 -0
- package/skills/youtube-master/src/env.ts +36 -0
- package/skills/youtube-master/src/obsidian.ts +155 -0
- package/skills/youtube-master/src/router.ts +110 -0
- package/skills/youtube-master/src/stt.ts +199 -0
- package/skills/youtube-master/src/study-note.ts +175 -0
- package/skills/youtube-master/src/summarize.ts +358 -0
- package/skills/youtube-master/src/transcript.ts +76 -0
- package/skills/youtube-master/src/types.ts +91 -0
- package/skills/youtube-master/src/util.ts +93 -0
- package/skills/youtube-master/src/youtube.ts +70 -0
- package/skills/youtube-master/tsconfig.json +13 -0
- package/src/acp/server.ts +5 -2
- package/src/agent-mission/driver.ts +80 -10
- package/src/agent-mission/pty-decision.ts +71 -0
- package/src/agent-mission/review-loop.ts +30 -0
- package/src/autopilot/build/llm-conflict-merge.ts +16 -3
- package/src/autopilot/tool-registry.ts +2 -0
- package/src/boot/daemon-prompt-turn.ts +5 -0
- package/src/boot/daemon-runtime.ts +9 -3
- package/src/boot/daemon-tools/web-search.ts +3 -0
- package/src/card-followup/core.ts +109 -0
- package/src/chat/index.ts +1 -0
- package/src/cli/drive-input-jail.ts +101 -0
- package/src/cli/hooks-cli.ts +17 -3
- package/src/cli/make-cli.ts +43 -0
- package/src/cli/persona-cli.ts +90 -4
- package/src/cli/plugin-cli.ts +31 -22
- package/src/cli/pod-cli.ts +19 -9
- package/src/cli/pr-cli.ts +51 -15
- package/src/cli/pty-drive-cli.ts +41 -7
- package/src/cli/pty-drive-verdict.ts +40 -0
- package/src/cli/release-cli.ts +17 -4
- package/src/cli/setup-cli.ts +43 -1
- package/src/cli/skills-cli.ts +17 -1
- package/src/context-bus/claude-hooks-install.ts +10 -0
- package/src/context-bus/context-now-surfaces.ts +48 -0
- package/src/context-bus/context-now.ts +104 -0
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/external-events.ts +52 -0
- package/src/context-bus/long-term-memory.ts +108 -0
- package/src/dashboard/index.ts +9 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -1
- package/src/decisions/decision-cards.ts +10 -3
- package/src/decisions/decision-ledger.ts +13 -5
- package/src/discord/sprint21-runtime.ts +5 -1
- package/src/discord-self-message.ts +32 -0
- package/src/discord-slash-wire.ts +5 -1
- package/src/discord.ts +17 -1
- package/src/domains/core-tools.ts +3 -2
- package/src/domains/outbound-alert.ts +6 -1
- package/src/domains/telegram-kind-route.ts +51 -13
- package/src/exec-requests/default-outputs.ts +25 -0
- package/src/exec-requests/planner.ts +10 -2
- package/src/exec-requests/runner.ts +20 -6
- package/src/exec-requests/store.ts +3 -0
- package/src/execution-loop/launch-gate.ts +100 -0
- package/src/field/field-reel.ts +5 -1
- package/src/field-note/index.ts +201 -0
- package/src/field-note/transcribe.ts +248 -0
- package/src/graph-kinds/registry.ts +3 -3
- package/src/graph-runner/graph-cli.ts +61 -1
- package/src/graph-runner/graph-grow-llm.ts +37 -0
- package/src/graph-runner/graph-grow.ts +47 -0
- package/src/graph-runner/runner.ts +317 -25
- package/src/grok/agent-search.ts +8 -2
- package/src/guardian/secret-exposure-scan.ts +101 -0
- package/src/harness/harness-cli-command.ts +247 -7
- package/src/harness/harness-pod-dispatch.ts +43 -23
- package/src/harness/harness-seams.ts +38 -4
- package/src/harness/harness-stop.ts +23 -4
- package/src/harness/launch-registry.ts +91 -0
- package/src/harness/process-garbage.ts +5 -3
- package/src/hooks/dispatch.ts +82 -0
- package/src/hooks/expose.ts +254 -0
- package/src/hooks/github-shadow.ts +108 -0
- package/src/hooks/providers.ts +71 -5
- package/src/hooks/queue.ts +1 -1
- package/src/hooks/receiver.ts +21 -20
- package/src/index.ts +52 -14
- package/src/intake-plane/categorize.ts +18 -46
- package/src/intake-plane/discord-seat-work.ts +10 -0
- package/src/intake-plane/front-route-classifier.ts +33 -39
- package/src/intake-plane/persona-answer.ts +30 -0
- package/src/intake-plane/runtime-callables.ts +8 -1
- package/src/intake-plane/telegram-seat-work.ts +36 -2
- package/src/intake-plane/wish-folder.ts +67 -0
- package/src/intent-gate/gate.ts +30 -10
- package/src/live/detail-switch.ts +5 -0
- package/src/llm/difficulty.ts +57 -0
- package/src/llm/judge-layer.ts +101 -0
- package/src/llm/mission-router.ts +33 -19
- package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
- package/src/loops/contract/examples/steward.loop.yaml +50 -0
- package/src/loops/contract/loop-agent.schema.json +138 -0
- package/src/loops/contract/validate.ts +64 -0
- package/src/loops/registry.ts +20 -6
- package/src/market/signed-index.ts +4 -0
- package/src/maturity/bot-command-maturity.ts +45 -0
- package/src/maturity/feature-maturity.ts +26 -4
- package/src/model-tier/index.ts +2 -2
- package/src/model-tier/tier-resolver.ts +20 -20
- package/src/model-tier/types.ts +6 -6
- package/src/mss/logging/log-store.ts +15 -0
- package/src/nexus/api/config-model-tier.ts +5 -4
- package/src/nexus/api/fabric-plans.ts +53 -0
- package/src/nexus/api/harness-api.ts +5 -1
- package/src/nexus/api/health.ts +2 -0
- package/src/nexus/api/http-server.ts +36 -0
- package/src/nexus/api/inside-events.ts +226 -0
- package/src/nexus/api/meta-api.ts +21 -8
- package/src/nexus/api/missions.ts +6 -1
- package/src/nexus/api/ops-seats.ts +33 -0
- package/src/nexus/api/outputs-route.ts +44 -0
- package/src/nexus/api/seat-requests.ts +44 -2
- package/src/nexus/api/sessions-store.ts +17 -38
- package/src/nexus/api/sub-seat-charters.ts +34 -0
- package/src/nexus/api/tabs-mutations.ts +3 -1
- package/src/nexus/api/trace.ts +18 -6
- package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
- package/src/nexus/api/workflows.ts +80 -6
- package/src/nexus/index.ts +2 -0
- package/src/nexus/kinds/webterm.ts +2 -1
- package/src/nexus/webterm/pty.ts +6 -2
- package/src/onboarding/cli-auth-status.ts +80 -0
- package/src/onboarding/voice-ai.ts +3 -3
- package/src/org/seat-budget.ts +48 -0
- package/src/org/stall-escalation.ts +63 -0
- package/src/outputs/ledger.ts +120 -0
- package/src/persona/persona-todo.ts +79 -0
- package/src/persona/presets.ts +116 -2
- package/src/persona/team-set.ts +155 -0
- package/src/plugins/install/plugin-install.ts +14 -0
- package/src/plugins/maker/market-bundle.ts +109 -0
- package/src/plugins/maker/plugin-maker.ts +254 -11
- package/src/plugins/maker/wizard-generate.ts +103 -0
- package/src/pod-lease/admission.ts +150 -0
- package/src/pod-lease/dependency-state.ts +85 -0
- package/src/pod-lease/host-lease.ts +127 -0
- package/src/project/project-suggestion.ts +16 -0
- package/src/project/session-context.ts +24 -0
- package/src/release-loop/checklist.ts +2 -2
- package/src/release-loop/feature-store.ts +5 -3
- package/src/release-loop/next-md-merge.ts +58 -0
- package/src/release-loop/release-note.ts +59 -4
- package/src/seat-dispatch/seat-ask.ts +130 -0
- package/src/seat-dispatch/seat-questions.ts +100 -0
- package/src/seat-loop/seat-loop.ts +548 -29
- package/src/seat-loop/seat-report.ts +37 -2
- package/src/self-dev/dev-cli.ts +4 -1
- package/src/self-dev/dev-pipeline.ts +37 -1
- package/src/self-dev/draft-sweep.ts +38 -33
- package/src/self-dev/draft-triage-rules.ts +8 -5
- package/src/self-dev/fabric-plan-core.ts +297 -0
- package/src/self-dev/orchestrate.ts +36 -4
- package/src/self-dev/release-path-guard.ts +62 -0
- package/src/self-dev/shard-aggregate.ts +230 -0
- package/src/self-dev/shard-placement.ts +60 -0
- package/src/self-dev/shard-plan.ts +48 -0
- package/src/self-dev/shard-pod-run.ts +77 -0
- package/src/self-dev/shard-run.ts +105 -0
- package/src/self-implement/gate-baseline.ts +1 -1
- package/src/self-implement/gate-cli.ts +40 -1
- package/src/self-implement/gate-scope.ts +37 -7
- package/src/self-implement/graph-variant.ts +171 -0
- package/src/self-implement/harness-policy-gates.ts +1 -1
- package/src/self-implement/headless-elanous-driver.ts +22 -3
- package/src/self-implement/host-regate.ts +22 -1
- package/src/self-implement/importer-test-index.ts +2 -1
- package/src/self-implement/l8-merge-queue.ts +130 -0
- package/src/self-implement/orchestrator.ts +242 -9
- package/src/self-implement/route-consumer-test-index.ts +122 -0
- package/src/self-implement/seams.ts +72 -11
- package/src/self-implement/test-seams.ts +1 -0
- package/src/self-implement/uncommitted-work.ts +59 -0
- package/src/session/index.ts +36 -13
- package/src/skills/gift-install.ts +215 -0
- package/src/skills/tools/web-search.ts +4 -1
- package/src/steward/launch.ts +42 -26
- package/src/steward/steward-cards.ts +17 -9
- package/src/steward/triage.ts +46 -14
- package/src/task-cards/card-cli.ts +25 -1
- package/src/task-orchestrator/mission.ts +5 -0
- package/src/task-orchestrator/store.ts +12 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +29 -7
- package/src/task-orchestrator/surfaces/pod-lease.ts +156 -19
- package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +214 -40
- package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
- package/src/task-orchestrator/types.ts +2 -0
- package/src/telegram-card-followup.ts +16 -96
- package/src/telegram-commands.ts +10 -1
- package/src/telegram-project-command.ts +71 -0
- package/src/telegram-report.ts +21 -17
- package/src/telegram.ts +29 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
- package/src/user-config.ts +102 -20
- package/src/version/packed-revision.json +1 -1
- package/src/web-search/grok.ts +7 -1
- package/src/web-search/index.ts +28 -1
- package/src/web-search/provider.ts +4 -0
- package/src/web-search/x-intent.ts +5 -0
- package/src/workflow-runtime/executor.ts +277 -47
- package/src/workflow-runtime/index.ts +1 -0
- package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
- package/src/workflow-runtime/plugin-kind-node.ts +1 -1
- package/src/workflow-runtime/schema.ts +79 -4
- package/src/workflow-runtime/types.ts +35 -4
- package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-cb6debea56cce4fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-e5bea36f183b25e6.js +0 -1
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +0 -1
- /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
|
-
/**
|
|
114
|
-
export
|
|
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
|
|
243
|
-
|
|
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 =
|
|
259
|
-
|
|
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
|
-
*
|
|
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
|
|
77
|
-
const
|
|
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) ? `${
|
|
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()
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 });
|