elanous 0.2.16 → 0.2.18
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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +77 -11
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +149 -41
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/driver.ts +53 -10
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -4
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +24 -14
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +93 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASK-AGENT 과제 넘기기 문 — `elanous task hand "<한 줄>"` (RFC-task-agent-any-task-to-completion §A9 표 1행 · R3).
|
|
3
|
+
*
|
|
4
|
+
* - 과제 카드를 실행부와 «같은» 상태 파일(`effectiveInstanceRoot()/task-agent-actions.json`)의 `tasks` 칸에 적는다.
|
|
5
|
+
* 종전 칸(landingDay·landingsToday·failureCounts)은 그대로 두고 덧붙이기만 한다(하위 호환 — 실행부 쓰기는 `...disk` 로 보존).
|
|
6
|
+
* - 첫 수는 «launch» = `harness say [--seat <자리>] --substrate pod --merge-by-host "<원문>"`.
|
|
7
|
+
* - 기본은 SHADOW: 카드와 명령만 남기고 아무것도 띄우지 않는다. `--live` 일 때만 주입된 launcher 로 띄운다.
|
|
8
|
+
* - 관측: `debug.log('task-agent', 'handed', {taskId, seat, checklistId, mode})`.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { debug } from '../debug/log.js';
|
|
14
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
15
|
+
import { withFileLockSync } from '../storage/file-lock.js';
|
|
16
|
+
|
|
17
|
+
export const TASK_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
18
|
+
export type TaskSeat = typeof TASK_SEATS[number];
|
|
19
|
+
/** 과제 카드 id 접두 — `task show <id>` 가 이 접두로 TOX 태스크와 갈린다. */
|
|
20
|
+
export const TASK_CARD_PREFIX = 'ta-';
|
|
21
|
+
|
|
22
|
+
export interface TaskCardEvent { at: string; event: string; detail?: string }
|
|
23
|
+
export interface TaskCardMove { kind: 'launch' | 'wait'; command?: string[]; reason: string }
|
|
24
|
+
export interface TaskCard {
|
|
25
|
+
id: string;
|
|
26
|
+
text: string;
|
|
27
|
+
seat?: TaskSeat;
|
|
28
|
+
checklistId?: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
/** launch-failed = 발사기가 띄우기에 실패했다(사유는 history 마지막 칸) — 다음 수는 다시 발사. */
|
|
31
|
+
status: 'handed' | 'launched' | 'launch-failed';
|
|
32
|
+
history: TaskCardEvent[];
|
|
33
|
+
/** 미션 카드만 — 조각 카드 id(순서대로) · 쪼갠 출처(`mission.ts` splitMission). */
|
|
34
|
+
pieces?: string[];
|
|
35
|
+
splitSource?: string;
|
|
36
|
+
/** 조각 카드만 — 소속 미션 카드 id · 먼저 착지해야 하는 조각 id(선후). */
|
|
37
|
+
mission?: string;
|
|
38
|
+
after?: string[];
|
|
39
|
+
/** 조각 카드만 — 착지 근거(② `tasks advance --pr|--goal`): 이 조각 런이 연 PR 번호 · 골 id(16자 · 런 원장 `pr-opened` 로 PR 을 찾는다). */
|
|
40
|
+
pr?: number;
|
|
41
|
+
goalId?: string;
|
|
42
|
+
/** 조각 카드만 — ② 가 선행 착지 뒤 넘겼다(한 번만 · 같은 잠금 안에서 먼저 적는다). */
|
|
43
|
+
handed?: { at: string; mode: 'shadow' | 'live'; claim?: string; error?: string };
|
|
44
|
+
/** 미션 카드만 — 모든 조각 착지 → 칸 green «제안»(체크리스트는 손대지 않는다 · OP/주인이 뒤집는다). */
|
|
45
|
+
greenProposal?: { at: string; checklistId: string | null; evidence: Record<string, string> };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TaskAgentStateFile { tasks?: Record<string, TaskCard>; [key: string]: unknown }
|
|
49
|
+
|
|
50
|
+
export function taskAgentStatePath(): string {
|
|
51
|
+
return join(effectiveInstanceRoot(), 'task-agent-actions.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 상태 파일을 읽을 수 없거나(ENOENT 제외) 해석할 수 없다 — 덮어쓰기 전에 멈춘다. */
|
|
55
|
+
export class TaskAgentStateError extends Error {
|
|
56
|
+
constructor(path: string, cause: unknown) {
|
|
57
|
+
super(`task-agent 상태 파일을 읽을 수 없다 — 덮어쓰지 않고 멈춘다: ${path} (${cause instanceof Error ? cause.message : String(cause)})`);
|
|
58
|
+
this.name = 'TaskAgentStateError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 실행부·과제 넘기기 문이 함께 쓰는 상태 파일(`task-agent-actions.json`)의 엄격한 읽기.
|
|
64
|
+
* 파일이 «없음»(ENOENT)만 빈 상태다. 그 밖의 읽기·해석 오류(손상된 JSON · 객체 아님 · 권한)는
|
|
65
|
+
* 던진다 — 빈 상태로 읽고 다시 쓰면 failureCounts·착지 기록·과제 카드가 지워진다.
|
|
66
|
+
*/
|
|
67
|
+
export function readTaskAgentState<T extends object = TaskAgentStateFile>(path: string): T {
|
|
68
|
+
let raw: string;
|
|
69
|
+
try { raw = readFileSync(path, 'utf8'); } catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return {} as T;
|
|
71
|
+
return stateError(path, error);
|
|
72
|
+
}
|
|
73
|
+
let parsed: unknown;
|
|
74
|
+
try { parsed = JSON.parse(raw); } catch (error) { return stateError(path, error); }
|
|
75
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return stateError(path, new Error('최상위가 객체가 아니다'));
|
|
76
|
+
return parsed as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stateError(path: string, cause: unknown): never {
|
|
80
|
+
const error = new TaskAgentStateError(path, cause);
|
|
81
|
+
try { debug.log('task-agent', 'state-unreadable', { path, reason: error.message }); } catch { /* fail-soft */ }
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readState(path: string): TaskAgentStateFile {
|
|
86
|
+
return readTaskAgentState<TaskAgentStateFile>(path);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 카드 여럿을 한 잠금 안에서 적는다(미션 ⊕ 조각) — 종전 칸은 `...disk` 로 보존. */
|
|
90
|
+
export function writeTaskCards(path: string, cards: readonly TaskCard[]): void {
|
|
91
|
+
withFileLockSync(`${path}.lock`, () => {
|
|
92
|
+
const disk = readState(path);
|
|
93
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
94
|
+
const tasks = { ...(disk.tasks ?? {}) };
|
|
95
|
+
for (const card of cards) tasks[card.id] = card;
|
|
96
|
+
writeFileSync(path, JSON.stringify({ ...disk, tasks }));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 카드 하나를 잠금 안에서 읽고 고쳐 쓴다 — `update` 가 undefined 를 돌려주면 쓰지 않는다. 쓴(또는 그대로인) 카드를 돌려준다. */
|
|
101
|
+
export function updateTaskCard(path: string, id: string, update: (card: TaskCard | undefined, tasks: Readonly<Record<string, TaskCard>>) => TaskCard | undefined): TaskCard | undefined {
|
|
102
|
+
return withFileLockSync(`${path}.lock`, () => {
|
|
103
|
+
const disk = readState(path);
|
|
104
|
+
const current = disk.tasks?.[id];
|
|
105
|
+
const next = update(current, disk.tasks ?? {});
|
|
106
|
+
if (!next) return current;
|
|
107
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
108
|
+
writeFileSync(path, JSON.stringify({ ...disk, tasks: { ...(disk.tasks ?? {}), [id]: next } }));
|
|
109
|
+
return next;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readTaskCard(id: string, path = taskAgentStatePath()): TaskCard | undefined {
|
|
114
|
+
return readState(path).tasks?.[id];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 첫 수의 elanous 인자(진입점·--config-dir 앞붙임은 launcher 몫). */
|
|
118
|
+
export function launchArgs(card: Pick<TaskCard, 'text' | 'seat'>): string[] {
|
|
119
|
+
return ['harness', 'say', ...(card.seat ? ['--seat', card.seat] : []), '--substrate', 'pod', '--merge-by-host', card.text];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `landed` = 착지한 조각 id(② 감지가 채운다). 안 주면 «모른다» — 선행은 미착지로 보고(fail-closed)
|
|
124
|
+
* 이유에 «착지 미확인»을 단다(착지했다/안 했다를 단정하지 않는다).
|
|
125
|
+
* 미션 카드는 스스로 발사하지 않는다 · 아직 안 뜬 조각의 선행이 덜 착지했으면 `wait after <id>`.
|
|
126
|
+
*/
|
|
127
|
+
export function nextMoveFor(card: TaskCard, landed?: ReadonlySet<string>): TaskCardMove {
|
|
128
|
+
if (card.pieces) return { kind: 'wait', reason: `미션 — 조각 ${card.pieces.length}장이 선후대로 발사된다` };
|
|
129
|
+
const unmet = (card.after ?? []).filter((id) => !landed?.has(id));
|
|
130
|
+
if (card.status !== 'launched' && unmet.length > 0) return { kind: 'wait', reason: `after ${unmet.join(', ')}${landed ? '' : ' · 착지 미확인'}` };
|
|
131
|
+
if (card.status === 'handed') return { kind: 'launch', command: launchArgs(card), reason: '넘겨받은 과제 — 첫 발사' };
|
|
132
|
+
if (card.status === 'launch-failed') return { kind: 'launch', command: launchArgs(card), reason: '직전 발사 실패 — 다시 발사' };
|
|
133
|
+
return { kind: 'wait', reason: '발사됨 — 런 멈춤을 슈퍼바이저 그림자 판단(task-agent.shadow-move)이 받는다' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function shellQuote(arg: string): string {
|
|
137
|
+
return /^[A-Za-z0-9_./:=@%+-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type TaskLauncher = (args: string[]) => void | Promise<void>;
|
|
141
|
+
|
|
142
|
+
export interface HandTaskOptions {
|
|
143
|
+
text: string;
|
|
144
|
+
seat?: TaskSeat;
|
|
145
|
+
checklistId?: string;
|
|
146
|
+
live?: boolean;
|
|
147
|
+
statePath?: string;
|
|
148
|
+
/** `--live` 일 때만 불린다. */
|
|
149
|
+
launcher?: TaskLauncher;
|
|
150
|
+
now?: () => Date;
|
|
151
|
+
id?: string;
|
|
152
|
+
/** 미션 조각으로 넘길 때 — 카드에 소속 미션·선후를 싣는다. */
|
|
153
|
+
mission?: string;
|
|
154
|
+
after?: string[];
|
|
155
|
+
/** 착지한 조각 id — `after` 가 모두 여기 있어야 발사한다(② 의 자리). */
|
|
156
|
+
landed?: ReadonlySet<string>;
|
|
157
|
+
/** 카드에 그대로 싣는 칸(② 의 넘김 표지·착지 근거) — handTask 가 카드를 새로 적을 때 지워지지 않게. */
|
|
158
|
+
cardFields?: Pick<TaskCard, 'pr' | 'goalId' | 'handed'>;
|
|
159
|
+
/**
|
|
160
|
+
* 있으면 카드 쓰기마다 잠금 안에서 디스크 카드를 보고 참일 때만 쓴다(② 의 claim 소유 확인).
|
|
161
|
+
* 거짓이면 쓰지 않고 `TaskCardSupersededError` — 다른 넘김이 그 카드를 가져갔다(발사 전이면 발사하지 않는다).
|
|
162
|
+
*/
|
|
163
|
+
writeGuard?: (current: TaskCard | undefined) => boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `writeGuard` 가 거짓 — 다른 넘김이 카드를 가져갔다. 카드는 건드리지 않았다. */
|
|
167
|
+
export class TaskCardSupersededError extends Error {
|
|
168
|
+
constructor(id: string) {
|
|
169
|
+
super(`과제 카드를 다른 넘김이 가져갔다 — 쓰지 않았다: ${id}`);
|
|
170
|
+
this.name = 'TaskCardSupersededError';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `--live` 발사가 실패했다 — 카드는 launch-failed 로 남았다. */
|
|
175
|
+
export class TaskLaunchError extends Error {
|
|
176
|
+
constructor(card: TaskCard, reason: string) {
|
|
177
|
+
super(`발사 실패 (${card.id} → launch-failed): ${reason}`);
|
|
178
|
+
this.name = 'TaskLaunchError';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface HandTaskResult { card: TaskCard; move: TaskCardMove; mode: 'shadow' | 'live'; launched: boolean }
|
|
183
|
+
|
|
184
|
+
export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
|
|
185
|
+
const text = opts.text.trim();
|
|
186
|
+
if (!text) throw new Error('과제 한 줄이 비어 있다');
|
|
187
|
+
if (opts.seat !== undefined && !TASK_SEATS.includes(opts.seat)) throw new Error(`자리는 ${TASK_SEATS.join('|')} 중 하나다: ${opts.seat}`);
|
|
188
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
189
|
+
const id = opts.id ?? `${TASK_CARD_PREFIX}${now.toISOString().slice(0, 10).replaceAll('-', '')}-${randomBytes(3).toString('hex')}`;
|
|
190
|
+
const mode = opts.live ? 'live' : 'shadow';
|
|
191
|
+
const path = opts.statePath ?? taskAgentStatePath();
|
|
192
|
+
const card: TaskCard = {
|
|
193
|
+
id, text,
|
|
194
|
+
...(opts.seat ? { seat: opts.seat } : {}),
|
|
195
|
+
...(opts.checklistId ? { checklistId: opts.checklistId } : {}),
|
|
196
|
+
createdAt: now.toISOString(),
|
|
197
|
+
status: 'handed',
|
|
198
|
+
history: [],
|
|
199
|
+
...(opts.mission ? { mission: opts.mission, after: [...(opts.after ?? [])] } : {}),
|
|
200
|
+
...(opts.cardFields ?? {}),
|
|
201
|
+
};
|
|
202
|
+
const move = nextMoveFor(card, opts.landed);
|
|
203
|
+
if (move.kind !== 'launch') throw new Error(`발사할 수 없는 과제다 — ${move.reason}`);
|
|
204
|
+
const guard = opts.writeGuard;
|
|
205
|
+
const writeCard = (target: string, next: TaskCard): void => {
|
|
206
|
+
if (!guard) { writeTaskCards(target, [next]); return; }
|
|
207
|
+
const owned = { ok: false };
|
|
208
|
+
// 착지 근거(pr·goalId)는 `recordPieceRef` 소유 — claim 뒤에 적힌 최신 근거를 디스크에서 이어 받는다.
|
|
209
|
+
updateTaskCard(target, next.id, (current) => {
|
|
210
|
+
if (!guard(current)) return undefined;
|
|
211
|
+
owned.ok = true;
|
|
212
|
+
// next 의 옛 근거는 버리고 디스크의 근거로 «교체»한다 — 정정된 근거 옆에 옛 근거가 되살아나지 않게.
|
|
213
|
+
const { pr: _stalePr, goalId: _staleGoal, ...base } = next;
|
|
214
|
+
return { ...base, ...(current?.pr !== undefined ? { pr: current.pr } : {}), ...(current?.goalId !== undefined ? { goalId: current.goalId } : {}) };
|
|
215
|
+
});
|
|
216
|
+
if (!owned.ok) throw new TaskCardSupersededError(next.id);
|
|
217
|
+
};
|
|
218
|
+
writeCard(path, card);
|
|
219
|
+
try { debug.log('task-agent', 'handed', { taskId: id, seat: card.seat ?? null, checklistId: card.checklistId ?? null, mode }); } catch { /* fail-soft */ }
|
|
220
|
+
if (mode === 'shadow') return { card, move, mode, launched: false };
|
|
221
|
+
if (!opts.launcher) throw new Error('--live 인데 launcher 가 없다');
|
|
222
|
+
try {
|
|
223
|
+
await opts.launcher(move.command!);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
226
|
+
const failed: TaskCard = { ...card, status: 'launch-failed', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch-failed', detail: reason }] };
|
|
227
|
+
writeCard(path, failed);
|
|
228
|
+
try { debug.log('task-agent', 'launch-failed', { taskId: id, seat: card.seat ?? null, reason }); } catch { /* fail-soft */ }
|
|
229
|
+
throw new TaskLaunchError(failed, reason);
|
|
230
|
+
}
|
|
231
|
+
const launched: TaskCard = { ...card, status: 'launched', history: [{ at: (opts.now ?? (() => new Date()))().toISOString(), event: 'launch', detail: move.command!.join(' ') }] };
|
|
232
|
+
writeCard(path, launched);
|
|
233
|
+
try { debug.log('task-agent', 'launched', { taskId: id, seat: card.seat ?? null }); } catch { /* fail-soft */ }
|
|
234
|
+
return { card: launched, move, mode, launched: true };
|
|
235
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface TaskCard {
|
|
|
16
16
|
goalId: string;
|
|
17
17
|
title: string;
|
|
18
18
|
status: CardStatus;
|
|
19
|
+
closedReason?: string;
|
|
19
20
|
createdAt: string;
|
|
20
21
|
sections: CardSection[];
|
|
21
22
|
}
|
|
@@ -206,7 +207,7 @@ export class CardStore {
|
|
|
206
207
|
if (card.status === 'closed') return card;
|
|
207
208
|
this.append(id, { type: 'closed', createdAt: new Date().toISOString(),
|
|
208
209
|
...(reason === undefined ? {} : { reason: redactSecrets(reason.trim()) }) });
|
|
209
|
-
return { ...card, status: 'closed' as const };
|
|
210
|
+
return { ...card, status: 'closed' as const, ...(reason === undefined ? {} : { closedReason: redactSecrets(reason.trim()) }) };
|
|
210
211
|
}).immediate();
|
|
211
212
|
}
|
|
212
213
|
|
|
@@ -216,8 +217,10 @@ export class CardStore {
|
|
|
216
217
|
} | null;
|
|
217
218
|
if (!row) return null;
|
|
218
219
|
const events = readFileSync(cardEventsPath(id, this.root), 'utf8').trimEnd().split('\n').map((line) => JSON.parse(line) as CardEvent);
|
|
220
|
+
const closed = events.find((event): event is Extract<CardEvent, { type: 'closed' }> => event.type === 'closed');
|
|
219
221
|
return {
|
|
220
222
|
id: row.id, goalId: row.goal_id, title: row.title, status: row.status, createdAt: row.created_at,
|
|
223
|
+
...(closed?.reason === undefined ? {} : { closedReason: closed.reason }),
|
|
221
224
|
sections: events.filter((event): event is Extract<CardEvent, { type: 'section' }> => event.type === 'section')
|
|
222
225
|
.map(({ key, owner, content, createdAt }) => ({ key, owner, content, createdAt })),
|
|
223
226
|
};
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/** Executor kinds of the dev child's `--json` result line (`src/index.ts` writeStdoutJson · DevExecutor.kind). */
|
|
2
|
+
const CHILD_RESULT_KINDS = new Set(['self', 'external']);
|
|
3
|
+
|
|
4
|
+
/** The dev child's terminal line nests the stage: {ok, kind, result:{stage, ok, node, outcome, …}}. Other JSON rows are not results. */
|
|
5
|
+
export function nestedPodResult(row: Record<string, unknown>): { stage: string; ok: boolean; error?: string } | null {
|
|
6
|
+
if (typeof row.kind !== 'string' || !CHILD_RESULT_KINDS.has(row.kind) || typeof row.ok !== 'boolean') return null;
|
|
7
|
+
if (!row.result || typeof row.result !== 'object' || Array.isArray(row.result)) return null;
|
|
8
|
+
const result = row.result as Record<string, unknown>;
|
|
9
|
+
if (typeof result.stage !== 'string' || typeof result.ok !== 'boolean') return null;
|
|
10
|
+
if (result.ok) return { stage: result.stage, ok: true };
|
|
11
|
+
const classification = result.abandonedClassification;
|
|
12
|
+
const detail = typeof result.supervisorVerdict === 'string' ? result.supervisorVerdict
|
|
13
|
+
: classification && typeof classification === 'object' && !Array.isArray(classification)
|
|
14
|
+
? (classification as Record<string, unknown>).classification : undefined;
|
|
15
|
+
const reason = [result.node, result.outcome].filter((part): part is string => typeof part === 'string' && part.length > 0).join(' ');
|
|
16
|
+
const error = [reason || (typeof result.error === 'string' ? result.error : ''), typeof detail === 'string' && detail ? detail : ''].filter(Boolean).join(' · ');
|
|
17
|
+
return { stage: result.stage, ok: false, ...(error ? { error } : {}) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A child terminal row: the legacy flat form {stage, ok, error?} (no kind, or a child kind) or the nested dev result. Any other kind is not a result. */
|
|
21
|
+
export function podTerminalRow(row: Record<string, unknown>): { stage: string; ok: boolean; error?: string } | null {
|
|
22
|
+
if (typeof row.kind === 'string' && !CHILD_RESULT_KINDS.has(row.kind)) return null;
|
|
23
|
+
if (typeof row.stage === 'string' && typeof row.ok === 'boolean') {
|
|
24
|
+
return { stage: row.stage, ok: row.ok, ...(typeof row.error === 'string' ? { error: row.error } : {}) };
|
|
25
|
+
}
|
|
26
|
+
return nestedPodResult(row);
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
/** Extract a human-readable reason from a failed child Pod without treating transfer markers as child errors. */
|
|
2
30
|
export function extractPodFailureReason(input: {
|
|
3
31
|
logs: string;
|
|
@@ -5,6 +33,8 @@ export function extractPodFailureReason(input: {
|
|
|
5
33
|
containerReason?: string | null;
|
|
6
34
|
jobReason?: string;
|
|
7
35
|
deadlineSeconds?: number;
|
|
36
|
+
/** POD-NORESULT: the stage and PR from the child's parsed result row (disposition) — used only when no log line gives a readable reason. */
|
|
37
|
+
result?: { stage?: string | null; prUrl?: string | null; prNumber?: number | null } | null;
|
|
8
38
|
}): string {
|
|
9
39
|
if (input.jobReason === 'DeadlineExceeded') {
|
|
10
40
|
return `DeadlineExceeded: Job 수명 상한 ${input.deadlineSeconds ?? 'unknown'}초 초과`;
|
|
@@ -12,7 +42,7 @@ export function extractPodFailureReason(input: {
|
|
|
12
42
|
if (input.containerReason === 'OOMKilled') return 'OOMKilled: 컨테이너 메모리 한도 초과';
|
|
13
43
|
|
|
14
44
|
const isBookkeeping = (line: string): boolean =>
|
|
15
|
-
/^ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
45
|
+
/^(?:ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)|\[graph\] )/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
16
46
|
const isError = (line: string): boolean =>
|
|
17
47
|
/\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
|
|
18
48
|
const meaningful: Array<{ text: string; terminal: boolean }> = [];
|
|
@@ -24,10 +54,11 @@ export function extractPodFailureReason(input: {
|
|
|
24
54
|
try { parsed = JSON.parse(trimmed); } catch { /* An ordinary line may begin with a brace. */ }
|
|
25
55
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
26
56
|
const row = parsed as Record<string, unknown>;
|
|
27
|
-
|
|
57
|
+
const terminal = podTerminalRow(row);
|
|
58
|
+
if (terminal) {
|
|
28
59
|
meaningful.length = 0;
|
|
29
|
-
if (
|
|
30
|
-
const errorLines =
|
|
60
|
+
if (terminal.ok === false && typeof terminal.error === 'string') {
|
|
61
|
+
const errorLines = terminal.error.split(/\r\n|\n|\r/u).map((part) => part.trim())
|
|
31
62
|
.filter((part) => part && !isBookkeeping(part));
|
|
32
63
|
const errorLine = [...errorLines].reverse().find(isError) ?? errorLines.at(-1);
|
|
33
64
|
if (errorLine) meaningful.push({ text: errorLine, terminal: true });
|
|
@@ -43,6 +74,14 @@ export function extractPodFailureReason(input: {
|
|
|
43
74
|
const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
|
|
44
75
|
const reason = errorLine?.text.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
|
|
45
76
|
if (reason) return reason;
|
|
77
|
+
// POD-NORESULT: an unconverged child (exit 2 → BackoffLimitExceeded under backoffLimit 0) often leaves a result row
|
|
78
|
+
// with a stage and PR but no error text — report that instead of «unreadable».
|
|
79
|
+
const stage = input.result?.stage?.trim();
|
|
80
|
+
if (stage) {
|
|
81
|
+
const prNumber = input.result?.prNumber;
|
|
82
|
+
const pr = Number.isSafeInteger(prNumber) && prNumber! > 0 ? `PR #${prNumber}` : input.result?.prUrl ? `PR ${input.result.prUrl}` : '';
|
|
83
|
+
return pr ? `수확 가능(${stage}) · ${pr}` : `수렴 못 함(단계 ${stage})`;
|
|
84
|
+
}
|
|
46
85
|
const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
|
|
47
86
|
return `사유 못 읽음: ${why.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240)}`;
|
|
48
87
|
}
|
|
@@ -64,6 +64,9 @@ export interface PoolLeaseRecommendation {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Kubernetes quantity to bytes (memory) or millicores (CPU). */
|
|
67
|
+
/** Kubernetes memory quantity (`16Gi`, `2Gi`, `512Mi`) → bytes; null when unparseable. */
|
|
68
|
+
export function memoryQuantityBytes(raw: string): number | null { return quantity(raw, 'memory'); }
|
|
69
|
+
|
|
67
70
|
function quantity(raw: string, unit: 'memory' | 'cpu'): number | null {
|
|
68
71
|
const m = /^(\d+(?:\.\d+)?)(n|u|m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$/.exec(raw);
|
|
69
72
|
if (!m) return null;
|
|
@@ -235,7 +238,7 @@ function runPoolDnsProbe(context: string, kubectl: PoolKubectl): PoolDnsProbe {
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/** Each cluster reads resource reservations; DNS requires a bounded disposable Pod. */
|
|
238
|
-
export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe } = {}): PoolLeaseMeasure {
|
|
241
|
+
export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; /** Read-only snapshots must not create the disposable DNS Pod or update its cache. */ skipDnsProbe?: boolean } = {}): PoolLeaseMeasure {
|
|
239
242
|
const kubectl = deps.kubectl ?? leaseKubectl;
|
|
240
243
|
return { members: members.map((member) => {
|
|
241
244
|
const base = ['--context', member.context, '--request-timeout=10s'];
|
|
@@ -354,7 +357,7 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
354
357
|
else if (!reservationsValid && !unknownPhase) reasons.push('cluster reservations: node assignment or memory request/limit missing/invalid');
|
|
355
358
|
}
|
|
356
359
|
result.reason = reasons.length ? reasons.join('; ').slice(0, 240) : null;
|
|
357
|
-
if (nodeItems?.length && jobItems && podItems) {
|
|
360
|
+
if (!deps.skipDnsProbe && nodeItems?.length && jobItems && podItems) {
|
|
358
361
|
let dnsState: PoolDnsProbe = 'unknown';
|
|
359
362
|
try { dnsState = (deps.dns ?? ((context) => probePoolDns(context, kubectl)))(member.context); } catch { /* probe could not be measured */ }
|
|
360
363
|
if (dnsState === 'dns') { result.capacity = 0; result.availableMemoryByNodeBytes = []; result.reason = 'dns'; }
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
|
|
15
15
|
*/
|
|
16
16
|
import { spawn, spawnSync } from 'node:child_process';
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
18
19
|
import { join, resolve } from 'node:path';
|
|
19
20
|
import { debug } from '../../debug/log.js';
|
|
20
21
|
import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
|
|
@@ -186,7 +187,8 @@ export class PodPoolScheduler {
|
|
|
186
187
|
private readonly hostLease: HostPoolLease;
|
|
187
188
|
private readonly pollMs: number;
|
|
188
189
|
private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
|
|
189
|
-
|
|
190
|
+
private readonly memberMemory: () => Array<{ context: string; allocatableMemoryBytes: number | null }> | Promise<Array<{ context: string; allocatableMemoryBytes: number | null }>>;
|
|
191
|
+
constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; memberMemory?: () => Array<{ context: string; allocatableMemoryBytes: number | null }> | Promise<Array<{ context: string; allocatableMemoryBytes: number | null }>>; occupancy?: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
|
|
190
192
|
this.pollMs = options.pollMs ?? 15_000;
|
|
191
193
|
// One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
|
|
192
194
|
this.hostLease = options.hostLease ?? podPoolHostLease(members);
|
|
@@ -195,6 +197,9 @@ export class PodPoolScheduler {
|
|
|
195
197
|
return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
|
|
196
198
|
});
|
|
197
199
|
this.measure = async () => measureStatus();
|
|
200
|
+
// An injected status (tests, callers with their own measurement) without memberMemory never shells out to kubectl here.
|
|
201
|
+
this.memberMemory = options.memberMemory ?? (options.status ? (() => []) : () => measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }).members
|
|
202
|
+
.map((m: PodLeaseMember) => ({ context: m.context, allocatableMemoryBytes: m.allocatableMemoryBytes })));
|
|
198
203
|
this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
|
|
199
204
|
this.admission = new PodLeaseAdmission({ status: async () => {
|
|
200
205
|
const measured = await measureStatus();
|
|
@@ -267,6 +272,23 @@ export class PodPoolScheduler {
|
|
|
267
272
|
};
|
|
268
273
|
});
|
|
269
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* LAUNCH-STALL (10-06): members whose total allocatable memory is below one Job's limit can never take it —
|
|
277
|
+
* waiting for them is an endless silent stall (minio 12.5GB vs a 16Gi standard Job). A member whose memory
|
|
278
|
+
* could not be read is not judged unfit (unknown ≠ too small).
|
|
279
|
+
*/
|
|
280
|
+
async unfitMembers(limitBytes: number): Promise<Array<{ context: string; allocatableMemoryBytes: number }>> {
|
|
281
|
+
let rows: Array<{ context: string; allocatableMemoryBytes: number | null }>;
|
|
282
|
+
try { rows = await this.memberMemory(); } catch { return []; }
|
|
283
|
+
return rows.flatMap((r) => r.allocatableMemoryBytes !== null && r.allocatableMemoryBytes < limitBytes
|
|
284
|
+
? [{ context: r.context, allocatableMemoryBytes: r.allocatableMemoryBytes }] : []);
|
|
285
|
+
}
|
|
286
|
+
/** Why admission is still waiting: the pool-wide lease measurement (null before any reading). Slot waits get their reason from tryAcquire's onEmpty. */
|
|
287
|
+
waitReason(): string | null {
|
|
288
|
+
const r = this.lastRaw;
|
|
289
|
+
if (!r) return null;
|
|
290
|
+
return `recommended=${r.recommended ?? 'unknown'} limitedBy=${r.limitedBy ?? '-'} memorySlots=${r.memorySlots ?? '?'}${r.reason ? ` reason=${r.reason}` : ''}`;
|
|
291
|
+
}
|
|
270
292
|
admissionSnapshot(): ReturnType<PodLeaseAdmission['snapshot']> {
|
|
271
293
|
return this.admission.snapshot();
|
|
272
294
|
}
|
|
@@ -277,7 +299,7 @@ export class PodPoolScheduler {
|
|
|
277
299
|
* A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
|
|
278
300
|
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
279
301
|
*/
|
|
280
|
-
async tryAcquire(): Promise<PodPoolMember | null> {
|
|
302
|
+
async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void): Promise<PodPoolMember | null> {
|
|
281
303
|
let reading: Record<string, MemberOccupancy>;
|
|
282
304
|
try { reading = await this.occupancy(); }
|
|
283
305
|
catch (error) {
|
|
@@ -293,6 +315,8 @@ export class PodPoolScheduler {
|
|
|
293
315
|
|| (cand.measured !== best.measured ? cand.measured
|
|
294
316
|
: cand.slots / cand.member.capacity > best.slots / best.member.capacity);
|
|
295
317
|
for (const m of this.members) {
|
|
318
|
+
// LAUNCH-STALL: a member this launch cannot fit is skipped for this call only (other launches may fit it).
|
|
319
|
+
if (skip?.has(m.context)) continue;
|
|
296
320
|
const inflight = this.inflight.get(m.context) ?? 0;
|
|
297
321
|
const row = reading[m.context];
|
|
298
322
|
const ok = !!row && row.occupied !== null;
|
|
@@ -304,7 +328,10 @@ export class PodPoolScheduler {
|
|
|
304
328
|
const cand = { member: m, slots, measured: ok };
|
|
305
329
|
if (better(cand)) best = cand;
|
|
306
330
|
}
|
|
307
|
-
if (!best)
|
|
331
|
+
if (!best) {
|
|
332
|
+
onEmpty?.(`no free slot · free=${JSON.stringify(free)} · measured=${JSON.stringify(measured)}${skip?.size ? ` · skipped=${[...skip].join(',')}` : ''}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
308
335
|
this.inflight.set(best.member.context, (this.inflight.get(best.member.context) ?? 0) + 1);
|
|
309
336
|
const share = Object.fromEntries(this.members.map((m) => [m.context, Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100]));
|
|
310
337
|
debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured, share });
|
|
@@ -396,13 +423,14 @@ export function podImageBuildScript(cwd: string = process.cwd()): string | null
|
|
|
396
423
|
return null;
|
|
397
424
|
}
|
|
398
425
|
|
|
399
|
-
|
|
426
|
+
/** packedTgz = 풀 동기화가 한 번 만든 패키지 묶음(있으면 build.sh 가 pack 을 다시 하지 않는다). */
|
|
427
|
+
type RemoteBuild = (host: string, cluster: string, registry?: string, packedTgz?: string) => Promise<{ ok: boolean; detail: string }>;
|
|
400
428
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
429
|
+
/** 같은 트리의 패키지 묶음(tgz)을 «한 번» 만든다 — 멤버들이 같이 쓴다. cleanup 은 동기화가 끝나면 부른다. */
|
|
430
|
+
type PoolPack = () => Promise<{ ok: boolean; tgz?: string; detail: string; cleanup?: () => void }>;
|
|
431
|
+
|
|
432
|
+
function runBuildScript(script: string, env: NodeJS.ProcessEnv): Promise<{ ok: boolean; detail: string }> {
|
|
433
|
+
return new Promise((done) => {
|
|
406
434
|
const child = spawn('bash', [script], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
407
435
|
let out = '';
|
|
408
436
|
child.stdout.on('data', (d) => { out += d; }); child.stderr.on('data', (d) => { out += d; });
|
|
@@ -411,18 +439,53 @@ function defaultRemoteBuild(script: string, image: string): RemoteBuild {
|
|
|
411
439
|
});
|
|
412
440
|
}
|
|
413
441
|
|
|
442
|
+
/**
|
|
443
|
+
* 🩸 2026-10-06: 멤버마다 build.sh 가 같은 트리에서 `bun pm pack` 을 «동시에» 돌렸다 — prepack 이 트리에 쓰는
|
|
444
|
+
* src/version/packed-revision.json 을 다른 쪽 postpack 이 지워 한쪽이 «⛔ pack» 으로 죽었다(node-c 가 매 발사 빠졌다).
|
|
445
|
+
* ⇒ 원격 멤버가 둘 이상이면 pack 은 여기서 한 번만 하고 그 tgz 를 나눠 준다.
|
|
446
|
+
*/
|
|
447
|
+
function defaultPoolPack(script: string): PoolPack {
|
|
448
|
+
return async () => {
|
|
449
|
+
const dir = mkdtempSync(join(tmpdir(), 'elanous-pool-pack-'));
|
|
450
|
+
const tgz = join(dir, 'elanous.tgz');
|
|
451
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ELANOUS_BUILD_PACK_ONLY: tgz };
|
|
452
|
+
delete env.ELANOUS_BUILD_PACKED_TGZ;
|
|
453
|
+
delete env.ELANOUS_BUILD_REMOTE;
|
|
454
|
+
const r = await runBuildScript(script, env);
|
|
455
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
456
|
+
if (!r.ok || !existsSync(tgz)) { cleanup(); return { ok: false, detail: r.detail }; }
|
|
457
|
+
return { ok: true, tgz, detail: r.detail, cleanup };
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function defaultRemoteBuild(script: string, image: string): RemoteBuild {
|
|
462
|
+
return (host, cluster, registry, packedTgz) => {
|
|
463
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ELANOUS_BUILD_REMOTE: host, ELANOUS_L2_CLUSTER: cluster, ELANOUS_BUILD_IMAGE: image === 'elanous-harness-lite:local' ? 'lite' : 'full' };
|
|
464
|
+
delete env.ELANOUS_BUILD_REGISTRY;
|
|
465
|
+
if (registry) env.ELANOUS_BUILD_REGISTRY = registry;
|
|
466
|
+
delete env.ELANOUS_BUILD_PACK_ONLY;
|
|
467
|
+
delete env.ELANOUS_BUILD_PACKED_TGZ;
|
|
468
|
+
if (packedTgz) env.ELANOUS_BUILD_PACKED_TGZ = packedTgz;
|
|
469
|
+
return runBuildScript(script, env);
|
|
470
|
+
};
|
|
471
|
+
}
|
|
414
472
|
/**
|
|
415
473
|
* 풀의 원격 노드들을 «동시에» 이 트리의 판으로 맞춘다 (09-25 개선).
|
|
416
474
|
* 1순위 = 노드 «쪽에서» 빌드(build.sh ELANOUS_BUILD_REMOTE) — 빌드 재료(수십 MB)만 보내고 무거운 층은 그 노드의 캐시가 재사용한다.
|
|
417
475
|
* 📏 커밋이 바뀐 뒤 두 노드를 올리는 데 벽시계 55초(종전: 다시 굽기 1분 40초 ⊕ 4GB 차례 전송 5분 48초).
|
|
418
476
|
* 실패하면 = 종전의 통째 전송(syncPoolImage)으로 떨어진다.
|
|
419
477
|
*/
|
|
420
|
-
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>> {
|
|
478
|
+
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; pack?: PoolPack | null } = {}): Promise<Map<string, PoolImageSync>> {
|
|
421
479
|
const run = deps.run ?? defaultRemoteRun;
|
|
422
480
|
const script = deps.buildScript === undefined ? podImageBuildScript() : deps.buildScript;
|
|
423
481
|
const remoteBuild = deps.remoteBuild ?? (script ? defaultRemoteBuild(script, image) : null);
|
|
424
482
|
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 }));
|
|
425
483
|
const results = new Map<string, PoolImageSync>();
|
|
484
|
+
// pack 한 번 ⊕ 나눠 쓰기 — 원격 멤버가 둘 이상일 때만(하나뿐이면 종전대로 build.sh 가 스스로 pack 한다).
|
|
485
|
+
// 주입된 remoteBuild(시험)에는 진짜 pack 을 붙이지 않는다 — pack 도 주입해야 쓴다.
|
|
486
|
+
const pack = deps.pack !== undefined ? deps.pack : (!deps.remoteBuild && script ? defaultPoolPack(script) : null);
|
|
487
|
+
const sharePack = pack && members.filter((m) => m.sshHost).length >= 2 ? pack : null;
|
|
488
|
+
let packed: ReturnType<PoolPack> | null = null;
|
|
426
489
|
await Promise.all(members.map(async (m) => {
|
|
427
490
|
const t0 = Date.now();
|
|
428
491
|
if (!m.sshHost) { results.set(m.context, { ok: true, action: 'local', detail: 'this machine', ms: 0 }); return; }
|
|
@@ -437,7 +500,12 @@ export async function syncPoolImages(members: readonly PodPoolMember[], image: s
|
|
|
437
500
|
return;
|
|
438
501
|
}
|
|
439
502
|
if (remoteBuild) {
|
|
440
|
-
|
|
503
|
+
// 같은 판이 아닌 멤버가 처음 여기 닿을 때 pack 한다(전부 fresh 면 pack 도 없다).
|
|
504
|
+
const shared = sharePack ? await (packed ??= sharePack()) : null;
|
|
505
|
+
if (shared && !shared.ok) debug.log('self-implement.pod', 'pool-image-pack-failed', { context: m.context, detail: shared.detail });
|
|
506
|
+
const b = shared && !shared.ok
|
|
507
|
+
? { ok: false, detail: shared.detail }
|
|
508
|
+
: await remoteBuild(m.sshHost, m.k3dCluster, m.registry, shared?.tgz);
|
|
441
509
|
const after = remoteImageCommit(m, image, run);
|
|
442
510
|
const refAfter = registryImageRef(m, localCommit ?? after, run, image);
|
|
443
511
|
// ⭐ 레지스트리 노드면 «이 판 커밋 태그가 레지스트리에 있나»로 판정한다 — Pod 는 그 커밋 태그를 pull 한다.
|
|
@@ -462,6 +530,6 @@ export async function syncPoolImages(members: readonly PodPoolMember[], image: s
|
|
|
462
530
|
return;
|
|
463
531
|
}
|
|
464
532
|
results.set(m.context, { ...ship(m, image, localCommit, run), ms: Date.now() - t0 });
|
|
465
|
-
}));
|
|
533
|
+
})).finally(async () => { if (packed) (await packed).cleanup?.(); });
|
|
466
534
|
return results;
|
|
467
535
|
}
|