elanous 0.2.14 → 0.2.16
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/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +82 -31
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +437 -0
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +82 -14
- package/scripts/release-loop/vault-note-node.ts +257 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/monthly.ts +97 -0
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +276 -0
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +66 -29
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/discord-self-message.ts +9 -0
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +147 -17
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +67 -8
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +218 -0
- package/src/harness/helper-scan.ts +27 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +70 -6
- package/src/hq/lease.ts +9 -7
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +189 -13
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +65 -21
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +107 -29
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +4 -3
- package/src/maturity/tui-slash-availability.ts +40 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +19 -5
- package/src/release-loop/feature-store.ts +59 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +98 -29
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +215 -12
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +14 -9
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +50 -6
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +325 -47
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +6 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +45 -16
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +20 -2
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
- package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +72 -4
- package/src/user-config.ts +64 -7
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
10
11
|
import { isAbsolute, resolve } from 'node:path';
|
|
11
12
|
import { getUserConfig } from '../../user-config.js';
|
|
12
13
|
import { debug } from '../../debug/log.js';
|
|
@@ -35,6 +36,14 @@ export const POD_LITE_IMAGE = 'elanous-harness-lite:local';
|
|
|
35
36
|
|
|
36
37
|
export const POD_LITE_MEMORY_LIMIT = '2Gi';
|
|
37
38
|
|
|
39
|
+
/** 요청은 한도를 넘을 수 없다(쿠버네티스가 Job 을 거부한다) — lite 2Gi 처럼 한도가 기본 요청보다 작으면 한도로 맞춘다. */
|
|
40
|
+
export function memoryRequestWithin(limit: string | undefined): string {
|
|
41
|
+
const gi = (value: string | undefined) => { const m = /^(\d+)Gi$/.exec(value ?? ''); return m ? Number(m[1]) : null; };
|
|
42
|
+
const request = gi(POD_CHILD_REQUESTS.memory);
|
|
43
|
+
const max = gi(limit);
|
|
44
|
+
return request !== null && max !== null && request > max ? limit! : POD_CHILD_REQUESTS.memory;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/** POD7 must-fix: the skill list alone can't tell a network-only job from one that needs ffmpeg/browser — so lite is
|
|
39
48
|
* only ever chosen when the caller says so (`lite`), and then only for network skills without a clone. Anything we
|
|
40
49
|
* can't judge keeps the full image. A lite request that doesn't fit is refused with the reason, not silently widened. */
|
|
@@ -50,6 +59,26 @@ export function podCommandImageFor(skills: readonly string[], clone: boolean, li
|
|
|
50
59
|
|
|
51
60
|
const COMMAND_LOG_CHARS = 80;
|
|
52
61
|
|
|
62
|
+
/** 자식 로그 전문 상한(바이트) — 넘으면 끝부분을 남긴다. config `pod.childLogMaxBytes` 가 양의 정수일 때만 그 값. */
|
|
63
|
+
export const POD_CHILD_LOG_MAX_BYTES_DEFAULT = 1_048_576;
|
|
64
|
+
|
|
65
|
+
export function podChildLogMaxBytes(read: () => number | undefined = () => getUserConfig().pod?.childLogMaxBytes): number {
|
|
66
|
+
const configured = read();
|
|
67
|
+
return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0
|
|
68
|
+
? configured
|
|
69
|
+
: POD_CHILD_LOG_MAX_BYTES_DEFAULT;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 크기 상한을 넘으면 끝부분을 남긴다(잘린 앞은 표시). */
|
|
73
|
+
export function tailBytes(text: string, maxBytes: number): string {
|
|
74
|
+
const bytes = Buffer.from(text);
|
|
75
|
+
if (bytes.length <= maxBytes) return text;
|
|
76
|
+
const cut = bytes.subarray(bytes.length - maxBytes);
|
|
77
|
+
let start = 0;
|
|
78
|
+
while (start < cut.length && (cut[start]! & 0xc0) === 0x80) start += 1;
|
|
79
|
+
return `…[앞 ${bytes.length - maxBytes}바이트 생략]\n${cut.subarray(start).toString('utf8')}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
const GATE = `ok=0
|
|
54
83
|
for i in $(seq 1 60); do
|
|
55
84
|
if curl -s -m 1 -o /dev/null http://host.orb.internal:31415/health || curl -s -m 1 -o /dev/null http://core.elanous-prod:8080/; then ok=0; else ok=$((ok+1)); fi
|
|
@@ -156,7 +185,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
156
185
|
initContainers: [{ name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] }],
|
|
157
186
|
containers: [{
|
|
158
187
|
name: 'child', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never',
|
|
159
|
-
resources: { requests: { ...POD_CHILD_REQUESTS }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
|
|
188
|
+
resources: { requests: { ...POD_CHILD_REQUESTS, memory: memoryRequestWithin(o.memoryLimit) }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
|
|
160
189
|
command: ['bash', '-c'], args: [script],
|
|
161
190
|
env: [
|
|
162
191
|
...(o.runId ? [{ name: 'ELANOUS_RUN_ID', value: o.runId }] : []),
|
|
@@ -214,12 +243,23 @@ export function podCommandSecretKeys(secret: Record<string, unknown> | null): st
|
|
|
214
243
|
return Object.keys((secret.stringData as Record<string, string> | undefined) ?? {});
|
|
215
244
|
}
|
|
216
245
|
|
|
246
|
+
export interface PodArtifactCollection {
|
|
247
|
+
/** 회수한 산출 파일 수(child.log 제외). 로그 조회 실패면 null. */
|
|
248
|
+
files: number | null;
|
|
249
|
+
/** 회수한 산출 파일의 상대 경로. */
|
|
250
|
+
names: string[];
|
|
251
|
+
/** 로그 조회 자체가 실패했을 때의 사유. 성공이면 null. */
|
|
252
|
+
error: string | null;
|
|
253
|
+
}
|
|
254
|
+
|
|
217
255
|
export interface PodCommandResult {
|
|
218
256
|
exitCode: number;
|
|
219
257
|
artifactsDir: string;
|
|
220
258
|
job: string;
|
|
221
259
|
/** 이 런이 실제로 쓴 이미지(레지스트리 판 태그 또는 로컬 이름). */
|
|
222
260
|
image?: string;
|
|
261
|
+
/** collectPodArtifacts 가 회수한 파일. 로그 조회 실패면 files=null · error=사유. runPodCommand 는 항상 채운다. */
|
|
262
|
+
artifacts?: PodArtifactCollection;
|
|
223
263
|
}
|
|
224
264
|
|
|
225
265
|
export interface RunPodCommandOptions {
|
|
@@ -255,6 +295,8 @@ export interface RunPodCommandOptions {
|
|
|
255
295
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
256
296
|
env?: NodeJS.ProcessEnv;
|
|
257
297
|
configHostMirror?: () => string | undefined;
|
|
298
|
+
/** 자식 로그 전문 상한(바이트). 생략 시 config `pod.childLogMaxBytes`, 없으면 1MiB. 넘으면 끝부분 우선. */
|
|
299
|
+
childLogMaxBytes?: number;
|
|
258
300
|
}
|
|
259
301
|
|
|
260
302
|
function jobFinished(types: string): 'complete' | 'failed' | null {
|
|
@@ -263,6 +305,19 @@ function jobFinished(types: string): 'complete' | 'failed' | null {
|
|
|
263
305
|
return null;
|
|
264
306
|
}
|
|
265
307
|
|
|
308
|
+
function artifactNames(logs: string): string[] {
|
|
309
|
+
const names = new Set<string>();
|
|
310
|
+
for (const line of logs.split(/\r?\n/)) {
|
|
311
|
+
const match = /^ELANOUS_POD_ARTIFACT (\S+) \d+\/\d+ /.exec(line);
|
|
312
|
+
if (!match) continue;
|
|
313
|
+
try {
|
|
314
|
+
const path = Buffer.from(match[1]!, 'base64url').toString('utf8');
|
|
315
|
+
if (path && !path.split('/').some((part) => part === '' || part === '.' || part === '..')) names.add(path);
|
|
316
|
+
} catch { /* 회수 쪽(collectPodArtifacts)이 불완전 사유를 남긴다 */ }
|
|
317
|
+
}
|
|
318
|
+
return [...names];
|
|
319
|
+
}
|
|
320
|
+
|
|
266
321
|
function exitCodeFromPod(kubectl: Kubectl, namespace: string, job: string, state: 'complete' | 'failed'): number {
|
|
267
322
|
const got = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${job}`, '-o', 'jsonpath={.items[0].status.containerStatuses[?(@.name=="child")].state.terminated.exitCode}']);
|
|
268
323
|
const code = Number(got.stdout.trim());
|
|
@@ -316,7 +371,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
316
371
|
}
|
|
317
372
|
if (pool) {
|
|
318
373
|
for (;;) {
|
|
319
|
-
member = pool.tryAcquire();
|
|
374
|
+
member = await pool.tryAcquire();
|
|
320
375
|
if (member) break;
|
|
321
376
|
await sleep(options.pollMs ?? 15_000);
|
|
322
377
|
}
|
|
@@ -387,12 +442,29 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
387
442
|
if (finished) { state = finished; break; }
|
|
388
443
|
await sleep(options.pollMs ?? 15_000);
|
|
389
444
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
445
|
+
const logsResult = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child']);
|
|
446
|
+
const logsFailed = logsResult.status !== 0;
|
|
447
|
+
const logs = logsFailed ? '' : logsResult.stdout;
|
|
448
|
+
const artifactDir = options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`;
|
|
449
|
+
let artifacts: PodArtifactCollection;
|
|
450
|
+
if (logsFailed) {
|
|
451
|
+
const reason = logsResult.stderr.trim() || `kubectl logs exit ${logsResult.status}`;
|
|
452
|
+
artifacts = { files: null, names: [], error: reason };
|
|
453
|
+
log('pod.command', 'artifact-collect-failed', { job: name, reason });
|
|
454
|
+
} else {
|
|
455
|
+
collectPodArtifacts(logs, {
|
|
456
|
+
dir: artifactDir,
|
|
457
|
+
job: name,
|
|
458
|
+
log: (c, e, d) => log(c, e, d),
|
|
459
|
+
});
|
|
460
|
+
const names = artifactNames(logs);
|
|
461
|
+
artifacts = { files: names.length, names, error: null };
|
|
462
|
+
}
|
|
463
|
+
// 표지가 없어도 알리는 경로는 실제로 존재해야 한다. 자식 로그 전문은 child.log (상한 넘으면 끝부분).
|
|
464
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
465
|
+
const maxBytes = options.childLogMaxBytes ?? podChildLogMaxBytes();
|
|
466
|
+
const childLog = logsFailed ? `산출 회수 못 함: ${artifacts.error}` : tailBytes(logs, maxBytes);
|
|
467
|
+
writeFileSync(resolve(artifactsDir, 'child.log'), childLog);
|
|
396
468
|
const exitCode = exitCodeFromPod(kubectl, namespace, name, state);
|
|
397
469
|
cleanupSecret();
|
|
398
470
|
log('pod.command', 'job-finished', {
|
|
@@ -401,8 +473,11 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
401
473
|
skills,
|
|
402
474
|
llm: llm ?? null,
|
|
403
475
|
command: command.join(' ').slice(0, COMMAND_LOG_CHARS),
|
|
476
|
+
artifactsDir,
|
|
477
|
+
artifacts,
|
|
478
|
+
...(logsFailed ? { artifactError: `산출 회수 못 함: ${artifacts.error}` } : {}),
|
|
404
479
|
});
|
|
405
|
-
return { exitCode, artifactsDir, job: name, image: imageRef ?? image };
|
|
480
|
+
return { exitCode, artifactsDir, job: name, image: imageRef ?? image, artifacts };
|
|
406
481
|
} catch (err) {
|
|
407
482
|
cleanupSecret();
|
|
408
483
|
throw err;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Extract a human-readable reason from a failed child Pod without treating transfer markers as child errors. */
|
|
2
|
+
export function extractPodFailureReason(input: {
|
|
3
|
+
logs: string;
|
|
4
|
+
logTailReason?: string;
|
|
5
|
+
containerReason?: string | null;
|
|
6
|
+
jobReason?: string;
|
|
7
|
+
deadlineSeconds?: number;
|
|
8
|
+
}): string {
|
|
9
|
+
if (input.jobReason === 'DeadlineExceeded') {
|
|
10
|
+
return `DeadlineExceeded: Job 수명 상한 ${input.deadlineSeconds ?? 'unknown'}초 초과`;
|
|
11
|
+
}
|
|
12
|
+
if (input.containerReason === 'OOMKilled') return 'OOMKilled: 컨테이너 메모리 한도 초과';
|
|
13
|
+
|
|
14
|
+
const isBookkeeping = (line: string): boolean =>
|
|
15
|
+
/^ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
16
|
+
const isError = (line: string): boolean =>
|
|
17
|
+
/\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
|
|
18
|
+
const meaningful: Array<{ text: string; terminal: boolean }> = [];
|
|
19
|
+
for (const line of input.logs.split(/\r\n|\n|\r/u)) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || isBookkeeping(trimmed)) continue;
|
|
22
|
+
if (trimmed.startsWith('{')) {
|
|
23
|
+
let parsed: unknown;
|
|
24
|
+
try { parsed = JSON.parse(trimmed); } catch { /* An ordinary line may begin with a brace. */ }
|
|
25
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
26
|
+
const row = parsed as Record<string, unknown>;
|
|
27
|
+
if (typeof row.ok === 'boolean' && typeof row.stage === 'string') {
|
|
28
|
+
meaningful.length = 0;
|
|
29
|
+
if (row.ok === false && typeof row.error === 'string') {
|
|
30
|
+
const errorLines = row.error.split(/\r\n|\n|\r/u).map((part) => part.trim())
|
|
31
|
+
.filter((part) => part && !isBookkeeping(part));
|
|
32
|
+
const errorLine = [...errorLines].reverse().find(isError) ?? errorLines.at(-1);
|
|
33
|
+
if (errorLine) meaningful.push({ text: errorLine, terminal: true });
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// A JSON transfer/observation is not a plain-text failure reason.
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
meaningful.push({ text: trimmed, terminal: false });
|
|
42
|
+
}
|
|
43
|
+
const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
|
|
44
|
+
const reason = errorLine?.text.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
|
|
45
|
+
if (reason) return reason;
|
|
46
|
+
const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
|
|
47
|
+
return `사유 못 읽음: ${why.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240)}`;
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
3
3
|
import { debug } from '../../debug/log.js';
|
|
4
4
|
import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
|
|
5
5
|
import { ledgerLineToLogEvent } from './pod-ledger-events.js';
|
|
6
|
+
import { defaultPodReemit } from './pod-ledger-prod-sink.js';
|
|
6
7
|
|
|
7
8
|
type Ledger = { runId: string; jsonl: string };
|
|
8
9
|
type Incomplete = { runId: string; reason: string };
|
|
@@ -94,7 +95,7 @@ export function parsePodLedgerChunks(logs: string): ParseResult {
|
|
|
94
95
|
* except a ledger that THIS run's live follower created (`replace`): that one is a partial copy and the final is complete. */
|
|
95
96
|
export function collectPodLedgers(
|
|
96
97
|
logs: string,
|
|
97
|
-
{ dir = runLedgerDir(), log = (
|
|
98
|
+
{ dir = runLedgerDir(), log = defaultPodReemit(), emit = defaultPodReemit(), replace = new Set<string>() }: {
|
|
98
99
|
dir?: string;
|
|
99
100
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
100
101
|
emit?: Emit;
|
|
@@ -146,8 +147,8 @@ export function createPodLedgerFollower(opts: {
|
|
|
146
147
|
onStall?: (message: string) => void;
|
|
147
148
|
}): { poll(): void; readonly owned: boolean } {
|
|
148
149
|
const dir = opts.dir ?? runLedgerDir();
|
|
149
|
-
const log = opts.log ?? (
|
|
150
|
-
const emit: Emit = opts.emit ?? (
|
|
150
|
+
const log = opts.log ?? defaultPodReemit();
|
|
151
|
+
const emit: Emit = opts.emit ?? defaultPodReemit();
|
|
151
152
|
const now = opts.now ?? Date.now;
|
|
152
153
|
const stallMinutes = opts.stallMinutes ?? 30;
|
|
153
154
|
if (!Number.isFinite(stallMinutes) || stallMinutes <= 0) throw new RangeError('stallMinutes must be positive');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { debug } from '../../debug/log.js';
|
|
2
|
+
import { LogStore, logsDbPath } from '../../mss/logging/log-store.js';
|
|
3
|
+
|
|
4
|
+
type Emit = (category: string, event: string, data: Record<string, unknown>) => void;
|
|
5
|
+
type Store = Pick<LogStore, 'insertBatch'>;
|
|
6
|
+
|
|
7
|
+
/** 발사한 부모(`harness say --config-dir …`)가 정한 집의 logs.db — 부모가 자식 `self orchestrate` 에 env 로 넘긴다. */
|
|
8
|
+
export const POD_REEMIT_LOGS_DB_ENV = 'ELANOUS_POD_REEMIT_LOGS_DB';
|
|
9
|
+
export function launchHomeLogsDb(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
10
|
+
return env[POD_REEMIT_LOGS_DB_ENV]?.trim() || undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let cached: { path: string; store: Store } | null = null;
|
|
14
|
+
let announced = '';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* POD-OBS(10-06 · 0.2.16 P0 · OP 사양): Pod 자식 원장 «재방출»만 부모가 발사 때 정한 집(A)의 logs.db 로 보낸다.
|
|
18
|
+
* 자식 orchestrate 는 트리 파생 test 우주(B)에서 돌아, 지금은 재방출이 B 에만 쌓였다(24h `ledger-collected` 운영 2 vs 다른 우주 250).
|
|
19
|
+
* 자식 우주 원장·격리는 그대로 둔다. A 를 못 열면 조용히 버리지 않고 B 로 떨어뜨리며 경고한다.
|
|
20
|
+
* env 가 없거나(옛 발사) A == B 이거나 시험 프로세스면 null → 호출측이 지금처럼 B 에만 쓴다.
|
|
21
|
+
*/
|
|
22
|
+
export function operationalReemit(opts: {
|
|
23
|
+
currentDb?: string;
|
|
24
|
+
operationalDb?: string;
|
|
25
|
+
open?: (path: string) => Store;
|
|
26
|
+
testProcess?: boolean;
|
|
27
|
+
fallback?: Emit;
|
|
28
|
+
} = {}): Emit | null {
|
|
29
|
+
const operationalDb = opts.operationalDb ?? launchHomeLogsDb();
|
|
30
|
+
if (!operationalDb) return null;
|
|
31
|
+
const currentDb = opts.currentDb ?? logsDbPath();
|
|
32
|
+
const testProcess = opts.testProcess ?? (process.env.NODE_ENV === 'test' || !!process.env.ELANOUS_TEST_HOME);
|
|
33
|
+
if (testProcess && !opts.open) return null;
|
|
34
|
+
if (currentDb === operationalDb) return null;
|
|
35
|
+
const open = opts.open ?? ((path: string) => new LogStore(path, { instance: 'prod' }));
|
|
36
|
+
const fallback: Emit = opts.fallback ?? ((c, e, d) => debug.log(c, e, d));
|
|
37
|
+
const write = (category: string, event: string, data: Record<string, unknown>): void => {
|
|
38
|
+
if (!cached || cached.path !== operationalDb || opts.open) cached = { path: operationalDb, store: open(operationalDb) };
|
|
39
|
+
cached.store.insertBatch([{ rec: { ts: new Date().toISOString(), category, event, data }, surface: 'pod-reemit' }]);
|
|
40
|
+
};
|
|
41
|
+
let warned = false;
|
|
42
|
+
return (category, event, data) => {
|
|
43
|
+
try {
|
|
44
|
+
if (announced !== `${currentDb}->${operationalDb}` || opts.open) {
|
|
45
|
+
// 어느 우주로 보냈는지 한 줄(A 에) — 조회자가 «B 에 왜 없나»를 여기서 본다.
|
|
46
|
+
write('self-implement.pod', 'reemit-target', { targetUniverse: operationalDb, childUniverse: currentDb });
|
|
47
|
+
announced = `${currentDb}->${operationalDb}`;
|
|
48
|
+
}
|
|
49
|
+
write(category, event, data);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
fallback(category, event, data);
|
|
52
|
+
if (warned) return;
|
|
53
|
+
// 경고는 싱크(발사 하나)당 한 번 — 줄마다 찍으면 경고가 원장을 덮는다.
|
|
54
|
+
warned = true;
|
|
55
|
+
try {
|
|
56
|
+
fallback('self-implement.pod', 'reemit-target-unavailable', {
|
|
57
|
+
targetUniverse: operationalDb, childUniverse: currentDb, reason: error instanceof Error ? error.message : String(error),
|
|
58
|
+
});
|
|
59
|
+
} catch { /* nothing */ }
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let shared: { key: string; emit: Emit } | null = null;
|
|
65
|
+
|
|
66
|
+
/** 재방출 기본 경로: 집(A)이 정해져 있으면 A 에만, 아니면 지금처럼 지금 우주(B)의 debug.log.
|
|
67
|
+
* 프로세스(발사)당 싱크 하나 — collector 의 `log`·`emit`, follower 가 같은 싱크를 써서 경고·대상 줄이 한 번이다. */
|
|
68
|
+
export function defaultPodReemit(): Emit {
|
|
69
|
+
const key = `${launchHomeLogsDb() ?? ''}->${logsDbPath()}`;
|
|
70
|
+
if (shared?.key === key) return shared.emit;
|
|
71
|
+
const operational = operationalReemit();
|
|
72
|
+
const emit: Emit = operational ?? ((category, event, data) => { debug.log(category, event, data); });
|
|
73
|
+
shared = { key, emit };
|
|
74
|
+
return emit;
|
|
75
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - 컨텍스트 kubectl 컨텍스트 이름(이 맥의 kubeconfig)
|
|
9
9
|
* - @ssh호스트 원격 클러스터면 그 기계 — 이미지 판 대조·반입(`docker load` ⊕ `k3d image import`)에 쓴다
|
|
10
10
|
* - 상한 이 노드에 동시에 둘 Job 수(기본 2)
|
|
11
|
-
* Job 마다 앞
|
|
11
|
+
* Job 마다 «빈 자리 비율»이 가장 큰 노드를 고른다(동률은 앞 순위 · 측정 실패 노드는 측정된 노드가 다 찼을 때만). 다 차면 자리가 날 때까지 기다린다.
|
|
12
12
|
* 노드 선택의 진행 중 수는 로컬 프로세스 기준이다. 발사 허가는 별도의 lease 측정으로
|
|
13
13
|
* 다른 호스트의 Running/Pending 및 메모리 예약까지 고려한다.
|
|
14
14
|
* 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
|
|
@@ -20,7 +20,7 @@ import { debug } from '../../debug/log.js';
|
|
|
20
20
|
import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
|
|
21
21
|
import { predecessorState, type PredecessorState } from '../../pod-lease/dependency-state.js';
|
|
22
22
|
import { HostPoolLease, leaseHasPendingPod } from '../../pod-lease/host-lease.js';
|
|
23
|
-
import { measurePoolLease, recommendConcurrency, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
23
|
+
import { measurePoolLease, recommendConcurrency, type PodLeaseMember, type PoolDnsProbe, type PoolLeaseRecommendation } from './pod-lease.js';
|
|
24
24
|
|
|
25
25
|
export interface PodPoolMember {
|
|
26
26
|
readonly context: string;
|
|
@@ -142,7 +142,34 @@ export function genericConfigPodPool(config: { pod?: { pool?: string }; harness?
|
|
|
142
142
|
return config.pod?.pool;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Cluster occupancy of one pool member: Running + Pending harness goal Pods.
|
|
147
|
+
* `null` means the read failed — callers must not treat that as zero.
|
|
148
|
+
*/
|
|
149
|
+
export type MemberOccupancy = { occupied: number } | { occupied: null; reason: string };
|
|
150
|
+
|
|
151
|
+
/** Read each member's cluster occupancy (Running + Pending harness goal Pods). A failed member is `occupied: null`, never 0. */
|
|
152
|
+
export function measureMemberOccupancy(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; measure?: (members: readonly PodPoolMember[]) => { members: PodLeaseMember[] } } = {}): Record<string, MemberOccupancy> {
|
|
153
|
+
let measured: { members: PodLeaseMember[] };
|
|
154
|
+
try {
|
|
155
|
+
measured = deps.measure
|
|
156
|
+
? deps.measure(members)
|
|
157
|
+
: measurePoolLease(members, { ...(deps.kubectl ? { kubectl: deps.kubectl } : {}), ...(deps.dns ? { dns: deps.dns } : {}) });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
160
|
+
return Object.fromEntries(members.map((m) => [m.context, { occupied: null, reason }]));
|
|
161
|
+
}
|
|
162
|
+
const byContext = new Map(measured.members.map((m) => [m.context, m]));
|
|
163
|
+
return Object.fromEntries(members.map((member) => {
|
|
164
|
+
const row = byContext.get(member.context);
|
|
165
|
+
if (!row || row.running === null || row.pending === null) {
|
|
166
|
+
return [member.context, { occupied: null, reason: row?.reason ?? '측정 불가' }];
|
|
167
|
+
}
|
|
168
|
+
return [member.context, { occupied: row.running + row.pending }];
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 노드 자리 배분 — 클러스터 실측 점유가 있으면 남은 칸이 가장 큰 멤버. 측정이 없으면 우선순위 순 첫 빈 자리. */
|
|
146
173
|
export function podPoolHostLease(members: readonly PodPoolMember[]): HostPoolLease {
|
|
147
174
|
return new HostPoolLease([...members.map((m) => `${m.context}@${m.sshHost ?? ''}`)].sort().join(','));
|
|
148
175
|
}
|
|
@@ -158,7 +185,8 @@ export class PodPoolScheduler {
|
|
|
158
185
|
private lastRaw: PoolLeaseRecommendation | null = null;
|
|
159
186
|
private readonly hostLease: HostPoolLease;
|
|
160
187
|
private readonly pollMs: number;
|
|
161
|
-
|
|
188
|
+
private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
|
|
189
|
+
constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; occupancy?: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
|
|
162
190
|
this.pollMs = options.pollMs ?? 15_000;
|
|
163
191
|
// One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
|
|
164
192
|
this.hostLease = options.hostLease ?? podPoolHostLease(members);
|
|
@@ -167,6 +195,7 @@ export class PodPoolScheduler {
|
|
|
167
195
|
return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
|
|
168
196
|
});
|
|
169
197
|
this.measure = async () => measureStatus();
|
|
198
|
+
this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
|
|
170
199
|
this.admission = new PodLeaseAdmission({ status: async () => {
|
|
171
200
|
const measured = await measureStatus();
|
|
172
201
|
this.lastRaw = measured;
|
|
@@ -241,13 +270,45 @@ export class PodPoolScheduler {
|
|
|
241
270
|
admissionSnapshot(): ReturnType<PodLeaseAdmission['snapshot']> {
|
|
242
271
|
return this.admission.snapshot();
|
|
243
272
|
}
|
|
244
|
-
/**
|
|
245
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Pick the member with the largest free share: (capacity − measured cluster occupancy − this process's inflight) / capacity.
|
|
275
|
+
* POOL-SPREAD (10-06): comparing absolute free slots always picked the big member (node-b 25 vs node-c 4), so the small
|
|
276
|
+
* member idled until the big one was full. Comparing the free share fills members in proportion to their capacity.
|
|
277
|
+
* A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
|
|
278
|
+
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
279
|
+
*/
|
|
280
|
+
async tryAcquire(): Promise<PodPoolMember | null> {
|
|
281
|
+
let reading: Record<string, MemberOccupancy>;
|
|
282
|
+
try { reading = await this.occupancy(); }
|
|
283
|
+
catch (error) {
|
|
284
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
285
|
+
reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: null, reason }]));
|
|
286
|
+
}
|
|
287
|
+
const free: Record<string, number> = {};
|
|
288
|
+
const measured: Record<string, boolean> = {};
|
|
289
|
+
let best: { member: PodPoolMember; slots: number; measured: boolean } | null = null;
|
|
290
|
+
// A measured member always beats an unmeasured one (its share would read 100% and draw every Job to a node
|
|
291
|
+
// that did not answer); unmeasured members compete only when no measured member has a free slot.
|
|
292
|
+
const better = (cand: { member: PodPoolMember; slots: number; measured: boolean }) => !best
|
|
293
|
+
|| (cand.measured !== best.measured ? cand.measured
|
|
294
|
+
: cand.slots / cand.member.capacity > best.slots / best.member.capacity);
|
|
246
295
|
for (const m of this.members) {
|
|
247
|
-
const
|
|
248
|
-
|
|
296
|
+
const inflight = this.inflight.get(m.context) ?? 0;
|
|
297
|
+
const row = reading[m.context];
|
|
298
|
+
const ok = !!row && row.occupied !== null;
|
|
299
|
+
measured[m.context] = ok;
|
|
300
|
+
const occupied = ok ? row.occupied! : 0;
|
|
301
|
+
const slots = m.capacity - occupied - inflight;
|
|
302
|
+
free[m.context] = slots;
|
|
303
|
+
if (slots <= 0) continue;
|
|
304
|
+
const cand = { member: m, slots, measured: ok };
|
|
305
|
+
if (better(cand)) best = cand;
|
|
249
306
|
}
|
|
250
|
-
return null;
|
|
307
|
+
if (!best) return null;
|
|
308
|
+
this.inflight.set(best.member.context, (this.inflight.get(best.member.context) ?? 0) + 1);
|
|
309
|
+
const share = Object.fromEntries(this.members.map((m) => [m.context, Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100]));
|
|
310
|
+
debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured, share });
|
|
311
|
+
return best.member;
|
|
251
312
|
}
|
|
252
313
|
release(member: PodPoolMember): void {
|
|
253
314
|
this.inflight.set(member.context, Math.max(0, (this.inflight.get(member.context) ?? 0) - 1));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** Pod Job 원천 단계 — default clone · 커밋 · PR · 호스트 번들.
|
|
2
2
|
* sha·PR·커밋은 형식 검사 전에 스크립트에 넣지 않는다(셸 주입 차단). */
|
|
3
3
|
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
|
|
4
6
|
export type PodSource =
|
|
5
|
-
| { kind: 'default' }
|
|
7
|
+
| { kind: 'default'; base?: string }
|
|
6
8
|
| { kind: 'commit'; sha: string }
|
|
7
9
|
| { kind: 'pr'; number: number }
|
|
8
10
|
| { kind: 'bundle'; bundlePath: string; sha256: string; sizeBytes: number; headCommit: string };
|
|
@@ -29,6 +31,27 @@ function assertSize(value: number): number {
|
|
|
29
31
|
return value;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/** Check the requested branch on origin before creating a Pod Job, not in the launched job. */
|
|
35
|
+
function assertRemoteBase(repoUrl: string, base: string): void {
|
|
36
|
+
if (base.startsWith('-') || base === 'HEAD') throw new Error(`invalid Pod base ref: ${base}`);
|
|
37
|
+
try {
|
|
38
|
+
execFileSync('git', ['check-ref-format', '--branch', base], { stdio: 'pipe' });
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`invalid Pod base ref: ${base}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const advertised = execFileSync('git', ['ls-remote', '--exit-code', '--heads', repoUrl, `refs/heads/${base}`], {
|
|
44
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
|
+
}).trim();
|
|
46
|
+
const [sha, ref] = advertised.split('\t');
|
|
47
|
+
if (!/^[0-9a-f]{40,64}$/.test(sha ?? '') || ref !== `refs/heads/${base}`) {
|
|
48
|
+
throw new Error('origin did not advertise the requested branch');
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error(`Pod base ref not found on origin: ${base}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
/** Job 스크립트의 원천 단계. 성공 갈래는 `ELANOUS_POD_SOURCE <kind> <HEAD>` 로 끝난다. */
|
|
33
56
|
export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath = '/host-mirror'): string {
|
|
34
57
|
const quotedUrl = `'${repoUrl.replace(/'/g, `'\\''`)}'`;
|
|
@@ -48,6 +71,19 @@ export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath =
|
|
|
48
71
|
'cd repo || exit 5',
|
|
49
72
|
];
|
|
50
73
|
if (source.kind === 'default') {
|
|
74
|
+
if (source.base && source.base !== 'main') {
|
|
75
|
+
assertRemoteBase(repoUrl, source.base);
|
|
76
|
+
const base = source.base;
|
|
77
|
+
const quote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
78
|
+
const quotedTrackingRef = quote(`refs/remotes/origin/${base}`);
|
|
79
|
+
return [
|
|
80
|
+
...start,
|
|
81
|
+
`git fetch --depth 50 origin ${quote(`+refs/heads/${base}:refs/remotes/origin/${base}`)} || exit 5`,
|
|
82
|
+
`git checkout -q -B ${quote(base)} ${quotedTrackingRef} || exit 5`,
|
|
83
|
+
'head=$(git rev-parse HEAD)',
|
|
84
|
+
"printf 'ELANOUS_POD_SOURCE default %s\\n' \"$head\"",
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
51
87
|
return [
|
|
52
88
|
...start,
|
|
53
89
|
'if [ "$source_via" = mirror ]; then',
|