elanous 0.2.23 → 0.2.24
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/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
- package/apps/pwa/out/_next/static/css/72becfd42807b50d.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 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- 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/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/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/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
- package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
- package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
- package/catalog/providers/openrouter.yaml +2 -0
- package/graphs/implement-loop.yaml +31 -0
- package/graphs/ops/harness-gc.yaml +17 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/overlays/README.md +1 -1
- package/graphs/overlays/stop-heal-ladder.yaml +105 -0
- package/package.json +1 -1
- package/scripts/add-api-key.sh +1 -1
- package/scripts/ci-typecheck-changed.ts +71 -14
- package/scripts/fleet/bedrock-pod-secret.ts +122 -0
- package/scripts/fleet/node-setup-linux.sh +97 -0
- package/scripts/fleet/node-setup.sh +11 -0
- package/scripts/fleet/zai-pod-secret.ts +86 -0
- package/scripts/land-global-checks.ts +3 -0
- package/scripts/measure-log-diet-2.ts +213 -0
- package/scripts/measure-tsc-affected-roots.ts +289 -0
- package/scripts/release-loop/gate-executor.ts +28 -0
- package/scripts/release-loop/gate-node.ts +262 -26
- package/scripts/release-loop/resume-release.ts +9 -3
- package/scripts/release-loop/resume.ts +41 -0
- package/scripts/release-loop/unattended-release.ts +22 -7
- package/scripts/tsc-baseline-cache.ts +147 -0
- package/scripts/usage-rollup.ts +58 -6
- package/src/agent/run-context.ts +10 -0
- package/src/agent-mission/driver.ts +18 -3
- package/src/agent-mission/resource-ladder.ts +8 -3
- package/src/agent-mission/review-loop.ts +8 -2
- package/src/agent-substrate/acp-implementer.ts +139 -0
- package/src/agent-substrate/pr-reviewer.ts +31 -4
- package/src/agent-substrate/self-review-cli.ts +48 -9
- package/src/autopilot/mission-critique.ts +18 -5
- package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
- package/src/autopilot/mission-trace.ts +12 -1
- package/src/autopilot/system-lookback-run.ts +16 -10
- package/src/bench/bench.ts +183 -0
- package/src/budget/fetchers/grok.ts +15 -5
- package/src/budget/llm-cost.ts +37 -5
- package/src/budget/types.ts +8 -2
- package/src/budget/unified-usage.ts +88 -6
- package/src/cli/bench-cli.ts +34 -0
- package/src/cli/guardian-cli.ts +26 -1
- package/src/cli/image-cli.ts +101 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +62 -5
- package/src/cli/pr-land-overlap-evidence.ts +160 -12
- package/src/cli/release-cli.ts +18 -9
- package/src/cli/rfc-status.ts +52 -2
- package/src/cli/self-update.ts +104 -0
- package/src/cli/tasks-cli.ts +109 -4
- package/src/cli/usage-cli.ts +91 -8
- package/src/core-turn/run-goal-loop.ts +5 -4
- package/src/dashboard/ad-run-setup-bindings.ts +279 -0
- package/src/dashboard/index.ts +18 -276
- package/src/dashboard/stale-install-check.ts +11 -0
- package/src/debug/log-diet.ts +38 -7
- package/src/debug/log.ts +19 -1
- package/src/domains/release-tool.ts +39 -0
- package/src/error-report/report.ts +21 -4
- package/src/github/pr-labels.ts +1 -0
- package/src/graph-runner/graph-cli.ts +42 -1
- package/src/graph-runner/runner.ts +167 -6
- package/src/grok/usage.ts +23 -2
- package/src/grounding/ref-index.ts +253 -0
- package/src/harness/dispatch-backpressure.ts +10 -2
- package/src/harness/harness-clean.ts +199 -78
- package/src/harness/harness-cli-command.ts +122 -7
- package/src/harness/harness-gc.ts +331 -0
- package/src/harness/harness-stop.ts +4 -2
- package/src/harness/harness-substrate-default.ts +142 -7
- package/src/harness/launch-registry.ts +6 -0
- package/src/harness/worktree-branch-prefix.ts +9 -0
- package/src/hq/fence.ts +187 -0
- package/src/hq/seats.ts +38 -22
- package/src/images/image-ledger.ts +179 -0
- package/src/index.ts +93 -11
- package/src/instance/child-scope.ts +32 -1
- package/src/instance/dev-project.ts +133 -0
- package/src/instance/resolve.ts +24 -0
- package/src/instance/spawn-elanous.ts +45 -0
- package/src/intake-plane/ref-prefetch.ts +110 -0
- package/src/intelligence-map/model-catalog.ts +72 -0
- package/src/llm/bedrock-eventstream.ts +215 -0
- package/src/llm/bedrock-provider.ts +114 -41
- package/src/llm/bedrock.ts +174 -28
- package/src/llm/openrouter-byok-cost.ts +96 -0
- package/src/llm/openrouter-deepseek.ts +151 -0
- package/src/llm/openrouter-upstream-pins.config.ts +25 -0
- package/src/llm/zai-coding-plan.ts +700 -0
- package/src/llm.ts +87 -16
- package/src/loops/checker.ts +148 -9
- package/src/loops/orchestrator/resync-shadow.ts +34 -7
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/model-tier/llm-tier-map.ts +9 -5
- package/src/models/prompts.ts +2 -1
- package/src/mss/logging/log-store.ts +58 -11
- package/src/nexus/api/missions.ts +73 -2
- package/src/nexus/api/ops-api.ts +18 -0
- package/src/nexus/index.ts +7 -75
- package/src/nexus/tools-kind.ts +78 -0
- package/src/oauth/codex-account-rotation.ts +39 -2
- package/src/oauth/codex-account-store.ts +323 -24
- package/src/oauth/fallback-chain.ts +53 -15
- package/src/onboarding/first-run.ts +37 -18
- package/src/onboarding/web-first.ts +20 -2
- package/src/ops/machine-heartbeat.ts +185 -0
- package/src/ops/overview-cache.ts +175 -0
- package/src/ops/overview.ts +8 -0
- package/src/prompt-cache/openai.ts +3 -0
- package/src/prompt-cache/types.ts +5 -0
- package/src/prompt-enhance/enhance.ts +23 -0
- package/src/release-loop/placement.ts +16 -1
- package/src/requirements/requirement-ledger.ts +536 -0
- package/src/retro/daily.ts +5 -5
- package/src/seat-loop/seat-loop.ts +32 -10
- package/src/self-dev/dev-cli.ts +44 -10
- package/src/self-dev/dev-pipeline.ts +7 -2
- package/src/self-dev/draft-residue.ts +14 -2
- package/src/self-dev/draft-sweep.ts +103 -0
- package/src/self-dev/graph-journey-route.ts +6 -0
- package/src/self-dev/orchestrate-cli.ts +25 -3
- package/src/self-dev/orchestrate.ts +41 -6
- package/src/self-dev/run-supervisor.ts +188 -15
- package/src/self-dev/run-types.ts +13 -0
- package/src/self-dev/supervisor-verdict-edges.ts +1 -0
- package/src/self-implement/budget-gate.ts +53 -13
- package/src/self-implement/child-context-composition.ts +139 -0
- package/src/self-implement/child-llm-preference.ts +13 -5
- package/src/self-implement/claude-acp-implement.ts +67 -0
- package/src/self-implement/flex-signals.ts +59 -0
- package/src/self-implement/frozen-merges.ts +8 -3
- package/src/self-implement/goal-author-decompose.ts +10 -2
- package/src/self-implement/goal-author.ts +24 -9
- package/src/self-implement/graph-overlay-condition.ts +28 -0
- package/src/self-implement/graph-overlay-yaml.ts +41 -7
- package/src/self-implement/graph-templates.ts +33 -1
- package/src/self-implement/graph-yaml.ts +56 -0
- package/src/self-implement/heal-intake.ts +5 -1
- package/src/self-implement/orchestrator.ts +144 -15
- package/src/self-implement/running-runs.ts +75 -5
- package/src/self-implement/seams.ts +47 -1
- package/src/self-implement/self-implement-cli.ts +9 -4
- package/src/self-implement/stop-autoheal.ts +69 -2
- package/src/session-runtime/retry-policy.ts +2 -1
- package/src/steward/launch.ts +5 -4
- package/src/steward/triage-budget.ts +62 -0
- package/src/steward/triage.ts +83 -22
- package/src/task-agent/actions.ts +62 -5
- package/src/task-agent/completion-evidence.ts +5 -0
- package/src/task-agent/cover.ts +101 -3
- package/src/task-agent/judge.ts +16 -0
- package/src/task-agent/land-order-ledger.ts +95 -0
- package/src/task-agent/live-move-history.ts +87 -1
- package/src/task-agent/live-moves.ts +350 -20
- package/src/task-agent/rejudge-on-head.ts +75 -20
- package/src/task-agent/review-prior-fix.ts +146 -0
- package/src/task-agent/run-adopt.ts +136 -0
- package/src/task-agent/shadow.ts +53 -9
- package/src/task-agent/task-hand.ts +54 -7
- package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
- package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
- package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
- package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
- package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
- package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
- package/src/typecheck-ratchet.ts +1 -0
- package/src/user-config.ts +51 -11
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/triggers/registry.ts +59 -0
- package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
- package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
- /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
|
@@ -16,6 +16,8 @@ import { HARNESS_RUN_ID_ENV, mintRunId, normalizeRunId } from '../harness/harnes
|
|
|
16
16
|
import { debug } from '../debug/log.js';
|
|
17
17
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
18
18
|
import { withFileLockSync } from '../storage/file-lock.js';
|
|
19
|
+
import { checkCompletionEvidence, deliverableReceiptEvidence } from './completion-evidence.js';
|
|
20
|
+
import { judgeNextMove } from './judge.js';
|
|
19
21
|
|
|
20
22
|
export interface ResolveHandTextDeps {
|
|
21
23
|
cwd: string;
|
|
@@ -83,7 +85,7 @@ export function taskCardLabelContext(card: Pick<TaskCard, 'id'>): TaskCardLabelC
|
|
|
83
85
|
* 과제 종결 종류(RFC-loop-agent-map §A4b③) — 비면 code-pr(PR 병합)로 읽는다.
|
|
84
86
|
* code-pr 밖의 종류는 «확인 증거»(`judge.ts` TaskJudgeInput.evidence)로 종결한다 — 증거를 만드는 일은 DEFAULT-HANDS 몫.
|
|
85
87
|
*/
|
|
86
|
-
export const COMPLETION_KINDS = ['code-pr', 'artifact', 'research-report', 'ops-action', 'content', 'watch-brief', 'decision-support'] as const;
|
|
88
|
+
export const COMPLETION_KINDS = ['code-pr', 'artifact', 'research-report', 'ops-action', 'content', 'watch-brief', 'decision-support', 'deliverable'] as const;
|
|
87
89
|
export type CompletionKind = typeof COMPLETION_KINDS[number];
|
|
88
90
|
|
|
89
91
|
/**
|
|
@@ -92,6 +94,8 @@ export type CompletionKind = typeof COMPLETION_KINDS[number];
|
|
|
92
94
|
*/
|
|
93
95
|
export interface TaskCardEvent {
|
|
94
96
|
at: string; event: string; detail?: string; runId?: string; pr?: number;
|
|
97
|
+
/** `closed-by-hand` 줄만 — 닫은 사유 · 닫은 자리(`closeTaskCard`). */
|
|
98
|
+
reason?: string; by?: string;
|
|
95
99
|
kind?: string; executed?: boolean; ok?: boolean; executorResult?: 'shadow' | 'live'; head?: string; effect?: string;
|
|
96
100
|
/** TA-REVIEW-RESULT-CAPTURE — review live-move 줄 · 결과 줄(`live-move-result`)의 결과 파일 경로. */
|
|
97
101
|
resultPath?: string;
|
|
@@ -102,6 +106,8 @@ export interface TaskCardEvent {
|
|
|
102
106
|
export const LIVE_MOVE_HISTORY_EVENT = 'live-move';
|
|
103
107
|
/** TA-REVIEW-RESULT-CAPTURE — 떼어 띄운 리뷰의 결과(또는 «결과 없음») 줄 — live-move 줄처럼 상태 사유 판정에서 뺀다. */
|
|
104
108
|
export const LIVE_MOVE_RESULT_EVENT = 'live-move-result';
|
|
109
|
+
/** HAND-MOVE-LEDGER — 자리가 손으로 마무리한 수 줄(`recordHandMove`) — 관측 기록이라 live-move 줄처럼 상태 사유 판정에서 뺀다. */
|
|
110
|
+
export const HAND_MOVE_HISTORY_EVENT = 'hand-move';
|
|
105
111
|
export interface TaskCardMove { kind: 'launch' | 'wait'; command?: string[]; reason: string }
|
|
106
112
|
export interface TaskCard {
|
|
107
113
|
id: string;
|
|
@@ -159,9 +165,17 @@ export interface TaskCard {
|
|
|
159
165
|
* TA-REJUDGE-ON-HEAD — 런 멈춤 «뒤» 머리마다 한 번 돌린 호스트 재게이트(병합 없음 · 떼어 띄움). 결과는 `resultPath` 파일에서
|
|
160
166
|
* 다음 틱이 회수해 passed·status·baseCommit·detail 을 채운다(비어 있으면 아직 도는 중 · 없는 칸을 «통과»로 읽지 않는다).
|
|
161
167
|
*/
|
|
162
|
-
regates?: Array<{ pr: number; head: string; at: string; resultPath?: string; passed?: boolean; status?: string; baseCommit?: string; detail?: string
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
regates?: Array<{ pr: number; head: string; at: string; resultPath?: string; passed?: boolean; status?: string; baseCommit?: string; detail?: string;
|
|
169
|
+
/** REJUDGE base-stale — 같은 머리를 새 base 위에서 다시 잰 재게이트면, land 가 낡았다고 거절한 앞 재게이트의 base(멱등 키 = 머리 ⊕ 이 base). */
|
|
170
|
+
staleBase?: string }>;
|
|
171
|
+
/**
|
|
172
|
+
* Live land attempts, claimed before invocation, once per PR head even if the command fails.
|
|
173
|
+
* TA-LAND-RETRY-ON-INFRA — a failed attempt carries `class`/`reason`: `infra` (TA-side, allow-listed) leaves one more attempt on
|
|
174
|
+
* the same head (`LAND_ATTEMPTS_PER_HEAD_ON_INFRA`); `judgement` · unknown reason · no class (older record) closes the head.
|
|
175
|
+
* REJUDGE base-stale — `base-stale` (overlap evidence gate base went stale with PR-file overlap) waits for a re-gate on a new base;
|
|
176
|
+
* only after that re-gate passes is the attempt consumed (`openLandAttempts`) and the same head may land once more.
|
|
177
|
+
*/
|
|
178
|
+
landAttempts?: Array<{ pr: number; head: string; at: string; ok?: boolean; detail?: string; class?: 'infra' | 'judgement' | 'base-stale'; reason?: string }>;
|
|
165
179
|
/** TA-LIVE-LAND-2 — `tasks hand --ta-land`: this card's run leaves the merge to the task agent (propose-land). */
|
|
166
180
|
taLand?: true;
|
|
167
181
|
/** TASKS-HAND-CHILD-LLM — `tasks hand --child-llm <provider>/<model> [--child-llm-effort <level>]` → 발사 인자 `--child-llm-provider/-model/-effort`. */
|
|
@@ -279,6 +293,16 @@ export function readTaskCard(id: string, path = taskAgentStatePath()): TaskCard
|
|
|
279
293
|
return readState(path).tasks?.[id];
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
export const CLOSED_BY_HAND_EVENT = 'closed-by-hand';
|
|
297
|
+
|
|
298
|
+
/** 카드를 손으로 닫는다 — 상태를 `failed` 로 두고 history 에 한 줄(`closed-by-hand`)을 남긴다. 이미 손으로 닫은 카드는 그대로 돌려준다(멱등). */
|
|
299
|
+
export function closeTaskCard(card: TaskCard, input: { reason: string; by: string; at: string }): TaskCard {
|
|
300
|
+
const reason = input.reason.trim();
|
|
301
|
+
if (!reason) throw new Error('닫는 사유(reason)가 비어 있다');
|
|
302
|
+
if (card.status === 'failed' && card.history.some((item) => item.event === CLOSED_BY_HAND_EVENT)) return card;
|
|
303
|
+
return { ...card, status: 'failed', history: [...card.history, { at: input.at, event: CLOSED_BY_HAND_EVENT, kind: CLOSED_BY_HAND_EVENT, detail: reason, reason, by: input.by }] };
|
|
304
|
+
}
|
|
305
|
+
|
|
282
306
|
/** 첫 수의 elanous 인자(진입점·--config-dir 앞붙임은 launcher 몫). */
|
|
283
307
|
export function launchArgs(card: Pick<TaskCard, 'text' | 'seat' | 'childLlm'>): string[] {
|
|
284
308
|
const child = card.childLlm
|
|
@@ -298,8 +322,8 @@ export function nextMoveFor(card: TaskCard, landed?: ReadonlySet<string>): TaskC
|
|
|
298
322
|
if (card.status !== 'launched' && unmet.length > 0) return { kind: 'wait', reason: `after ${unmet.join(', ')}${landed ? '' : ' · 착지 미확인'}` };
|
|
299
323
|
if (card.status === 'handed') return { kind: 'launch', command: launchArgs(card), reason: '넘겨받은 과제 — 첫 발사' };
|
|
300
324
|
if (card.status === 'launch-failed') return { kind: 'launch', command: launchArgs(card), reason: '직전 발사 실패 — 다시 발사' };
|
|
301
|
-
// live-move 줄(관측 기록)은 실패 사유가 아니다 — 그 줄을 뺀 마지막 칸이 사유다(live-move 줄 없는 카드는 종전과 같다).
|
|
302
|
-
if (card.status === 'failed') return { kind: 'wait', reason: card.history.filter((item) => item.event !== LIVE_MOVE_HISTORY_EVENT && item.event !== LIVE_MOVE_RESULT_EVENT).at(-1)?.detail ?? '런 실패 — PR·수확 가지 없음' };
|
|
325
|
+
// live-move·hand-move 줄(관측 기록)은 실패 사유가 아니다 — 그 줄을 뺀 마지막 칸이 사유다(live-move 줄 없는 카드는 종전과 같다).
|
|
326
|
+
if (card.status === 'failed') return { kind: 'wait', reason: card.history.filter((item) => item.event !== LIVE_MOVE_HISTORY_EVENT && item.event !== LIVE_MOVE_RESULT_EVENT && item.event !== HAND_MOVE_HISTORY_EVENT).at(-1)?.detail ?? '런 실패 — PR·수확 가지 없음' };
|
|
303
327
|
return { kind: 'wait', reason: '발사됨 — 런 멈춤을 슈퍼바이저 그림자 판단(task-agent.shadow-move)이 받는다' };
|
|
304
328
|
}
|
|
305
329
|
|
|
@@ -370,6 +394,29 @@ export class TaskLaunchError extends Error {
|
|
|
370
394
|
|
|
371
395
|
export interface HandTaskResult { card: TaskCard; move: TaskCardMove; mode: 'shadow' | 'live'; launched: boolean; cwd?: string }
|
|
372
396
|
|
|
397
|
+
/** Dispatch evidence for a handed deliverable card; unlike handTask, this never launches work. */
|
|
398
|
+
export function dispatchDeliverableCompletion(id: string, input: { ref: string; receipt?: boolean }, path: string, now: () => Date = () => new Date()): boolean {
|
|
399
|
+
const card = readTaskCard(id, path);
|
|
400
|
+
if (card?.completion !== 'deliverable' || !card.project?.target || card.pr !== undefined || card.status === 'failed' || card.status === 'launch-failed') return false;
|
|
401
|
+
const evidence = checkCompletionEvidence('deliverable', { dir: card.project.target, ...input });
|
|
402
|
+
if (judgeNextMove({ completion: card.completion, evidence }).move !== 'propose-green') {
|
|
403
|
+
try { debug.log('task-agent', 'deliverable-rejected', { taskId: id, ref: input.ref, missing: evidence.missing }); } catch { /* fail-soft */ }
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
const receiptEvidence = deliverableReceiptEvidence(evidence.ref);
|
|
407
|
+
const at = now().toISOString();
|
|
408
|
+
let closed = false;
|
|
409
|
+
updateTaskCard(path, id, (current) => {
|
|
410
|
+
if (current?.completion !== 'deliverable' || current.pr !== undefined || current.greenProposal || !current.project || current.project.target !== card.project?.target || current.status === 'failed' || current.status === 'launch-failed') return undefined;
|
|
411
|
+
const fresh = checkCompletionEvidence('deliverable', { dir: current.project.target, ...input });
|
|
412
|
+
if (judgeNextMove({ completion: current.completion, evidence: fresh }).move !== 'propose-green') return undefined;
|
|
413
|
+
closed = true;
|
|
414
|
+
return { ...current, greenProposal: { at, checklistId: current.checklistId ?? null, evidence: { [id]: receiptEvidence } } };
|
|
415
|
+
});
|
|
416
|
+
try { debug.log('task-agent', closed ? 'deliverable-closed' : 'deliverable-rejected', { taskId: id, ref: evidence.ref, ...(closed ? {} : { missing: ['already-proposed-or-changed'] }) }); } catch { /* fail-soft */ }
|
|
417
|
+
return closed;
|
|
418
|
+
}
|
|
419
|
+
|
|
373
420
|
export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
|
|
374
421
|
const text = opts.text.trim();
|
|
375
422
|
if (!text) throw new Error('과제 한 줄이 비어 있다');
|
|
@@ -440,7 +487,7 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
|
|
|
440
487
|
if (!opts.launcher) throw new Error('--live 인데 launcher 가 없다');
|
|
441
488
|
const runId = mintRunId();
|
|
442
489
|
const launchId = `tl-${randomBytes(6).toString('hex')}`;
|
|
443
|
-
const context: TaskLaunchContext = { runId, launchId, env: { [HARNESS_RUN_ID_ENV]: runId, ...(card.taLand ? { [TA_LAND_ENV]: '1' } : {}) }, cardLabelContext: taskCardLabelContext(card) };
|
|
490
|
+
const context: TaskLaunchContext = { runId, launchId, env: { [HARNESS_RUN_ID_ENV]: runId, ELANOUS_TASK_CARD_ID: card.id, ...(card.taLand ? { [TA_LAND_ENV]: '1' } : {}) }, cardLabelContext: taskCardLabelContext(card) };
|
|
444
491
|
let receipt: void | TaskLaunchReceipt;
|
|
445
492
|
try {
|
|
446
493
|
receipt = await opts.launcher(move.command!, project?.target, context);
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
// «모른다»라서 뒤로 보내되 빼지 않는다(못 읽었다고 갈 곳을 없애지 않는다 — 회전 규칙과 같다).
|
|
6
6
|
// ⛔ `--pod-account` 를 명시하면 브로커를 쓰지 않는다 — 사람의 의도가 이긴다.
|
|
7
7
|
|
|
8
|
-
import { codexAccountThresholdPercent, type RotationCandidate } from '../../oauth/codex-account-rotation.js';
|
|
8
|
+
import { CODEX_CREDIT_RESERVE_DEFAULT, codexAccountThresholdPercent, creditCandidatesByBalance, type RotationCandidate } from '../../oauth/codex-account-rotation.js';
|
|
9
|
+
import { debug } from '../../debug/log.js';
|
|
9
10
|
|
|
10
11
|
export const POD_ACCOUNT_EXCLUDE_AT_PERCENT = 95;
|
|
11
12
|
|
|
12
13
|
export interface PodAccountPlan { usable: string[]; excluded: Array<{ name: string; why: string }>; /** 크레딧으로 쓰는 계정(구독 잔량 0 · 대표 허가) — 로그에 «크레딧» 으로 보이게. */ creditAccounts?: string[] }
|
|
13
14
|
|
|
14
15
|
export type PodProviderPlan =
|
|
15
|
-
| { provider: 'openai-codex'; accounts: string[]; excluded: PodAccountPlan['excluded']; grokSubscriptionEligible: boolean; creditAccounts?: string[] }
|
|
16
|
+
| { provider: 'openai-codex'; accounts: string[]; excluded: PodAccountPlan['excluded']; grokSubscriptionEligible: boolean; creditAccounts?: string[]; /** CODEX-CREDITS-STEP — 체인 마지막 칸으로 크레딧에 왔다. */ codexCreditsStep?: true; /** 그 칸에 온 이유(관측). */ skipped?: string[] }
|
|
16
17
|
| { provider: 'grok'; excluded: PodAccountPlan['excluded']; grokSubscriptionEligible: boolean }
|
|
17
18
|
| { provider: null; reasons: string[]; grokSubscriptionEligible: boolean };
|
|
18
19
|
|
|
@@ -26,14 +27,39 @@ export function planPodProvider(input: {
|
|
|
26
27
|
thresholdPercentByAccount?: Readonly<Record<string, unknown>>;
|
|
27
28
|
/** 대표 크레딧 허가(`llm.codexCreditsAllowed`) — 구독 잔량 계정이 없으면 찬 계정을 크레딧으로 쓴다. */
|
|
28
29
|
creditsAllowed?: boolean;
|
|
30
|
+
/** SUBSCRIPTION-EXHAUST-REROUTE — `llm.codexCreditReserve`. 잔액이 «알려져» 이 값 이하인 계정은 크레딧으로 쓰지 않는다. */
|
|
31
|
+
creditReserve?: number;
|
|
32
|
+
/** CODEX-CREDITS-STEP — `llm.fallbackChain` 에 `codex-credits` 칸이 있고 정책이 폴백을 허용한다(호출자가 확정). */
|
|
33
|
+
codexCreditsStep?: boolean;
|
|
34
|
+
/** grok «잔량» — `exhausted` 일 때만 grok 을 건너뛴다(「모른다」는 종전대로 grok · 돈을 쓰는 쪽으로 기울지 않는다). */
|
|
35
|
+
grokQuota?: 'usable' | 'exhausted' | 'unknown';
|
|
29
36
|
}): PodProviderPlan {
|
|
30
37
|
const accounts = planPodAccounts(input.codexCandidates, {
|
|
31
38
|
excludeAt: input.excludeAt,
|
|
32
39
|
thresholdPercentByAccount: input.thresholdPercentByAccount,
|
|
33
40
|
creditsAllowed: input.creditsAllowed,
|
|
41
|
+
...(input.creditReserve !== undefined ? { creditReserve: input.creditReserve } : {}),
|
|
34
42
|
});
|
|
35
43
|
if (accounts.usable.length) return { provider: 'openai-codex', accounts: accounts.usable, excluded: accounts.excluded, grokSubscriptionEligible: input.grokSubscription, ...(accounts.creditAccounts ? { creditAccounts: accounts.creditAccounts } : {}) };
|
|
36
|
-
|
|
44
|
+
const grokEligible = input.grokSubscription || (input.grokApiKey && input.grokApiKeyOptIn);
|
|
45
|
+
const grokSkippedAsExhausted = input.codexCreditsStep === true && input.grokQuota === 'exhausted';
|
|
46
|
+
if (grokEligible && !grokSkippedAsExhausted) return { provider: 'grok', excluded: accounts.excluded, grokSubscriptionEligible: input.grokSubscription };
|
|
47
|
+
if (input.codexCreditsStep === true) {
|
|
48
|
+
// 체인 마지막 칸 — 크레딧 잔액이 «확인된» 계정만, 많은 순(모르면 안 쓴다 · 돈이라 fail-closed).
|
|
49
|
+
const credit = creditCandidatesByBalance(input.codexCandidates, input.creditReserve ?? CODEX_CREDIT_RESERVE_DEFAULT).map((c) => c.name); // 회전 수집기와 같은 자
|
|
50
|
+
if (credit.length > 0) {
|
|
51
|
+
return {
|
|
52
|
+
// 구독 기준 제외 사유는 진단용으로 남긴다(계정 선택은 credit 순서가 정한다).
|
|
53
|
+
provider: 'openai-codex', accounts: credit, excluded: accounts.excluded, creditAccounts: credit, codexCreditsStep: true,
|
|
54
|
+
grokSubscriptionEligible: input.grokSubscription,
|
|
55
|
+
skipped: ['codex-rotate:no-candidate', grokEligible ? 'grok:grok-exhausted' : 'grok:grok-unavailable'],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// 칸에 «닿았지만» 잔액 확인된 계정이 없다 — 실패한 도달도 남긴다.
|
|
59
|
+
try { debug.log('self-implement.pod', 'codex-credits-step', { outcome: 'skipped', why: 'codex-credits-unavailable', grokEligible }); } catch { /* observation is fail-soft */ }
|
|
60
|
+
// 크레딧도 못 쓰면 종전 그대로 — 소진 신호가 있어도 grok 으로(새로 막지 않는다).
|
|
61
|
+
if (grokEligible) return { provider: 'grok', excluded: accounts.excluded, grokSubscriptionEligible: input.grokSubscription };
|
|
62
|
+
}
|
|
37
63
|
return {
|
|
38
64
|
provider: null,
|
|
39
65
|
grokSubscriptionEligible: false,
|
|
@@ -50,10 +76,11 @@ export function planPodAccounts(
|
|
|
50
76
|
thresholdPercentByAccount?: Readonly<Record<string, unknown>>;
|
|
51
77
|
excludeAt?: number;
|
|
52
78
|
creditsAllowed?: boolean;
|
|
79
|
+
creditReserve?: number;
|
|
53
80
|
} = {},
|
|
54
81
|
): PodAccountPlan {
|
|
55
|
-
const { thresholdPercentByAccount, excludeAt = POD_ACCOUNT_EXCLUDE_AT_PERCENT, creditsAllowed = false } =
|
|
56
|
-
typeof options === 'number' ? { excludeAt: options } : options;
|
|
82
|
+
const { thresholdPercentByAccount, excludeAt = POD_ACCOUNT_EXCLUDE_AT_PERCENT, creditsAllowed = false, creditReserve } =
|
|
83
|
+
typeof options === 'number' ? { excludeAt: options } as { thresholdPercentByAccount?: undefined; excludeAt: number; creditsAllowed?: boolean; creditReserve?: number } : options;
|
|
57
84
|
const excluded: PodAccountPlan['excluded'] = [];
|
|
58
85
|
const known: RotationCandidate[] = [];
|
|
59
86
|
const unknown: RotationCandidate[] = [];
|
|
@@ -70,11 +97,17 @@ export function planPodAccounts(
|
|
|
70
97
|
const usable = [...known, ...unknown].map((c) => c.name);
|
|
71
98
|
// 대표 크레딧 허가: 구독 잔량이 남은 계정이 «하나도» 없을 때만 찬 계정을 크레딧으로 쓴다(사용률 낮은 순) — 잔량 계정이 있으면 그쪽이 먼저다.
|
|
72
99
|
if (usable.length === 0 && creditsAllowed) {
|
|
100
|
+
// SUBSCRIPTION-EXHAUST-REROUTE — 잔액이 «알려져» 예비분 이하인 계정은 크레딧 후보가 아니다(모르면 종전대로 남긴다).
|
|
101
|
+
const reserve = typeof creditReserve === 'number' && Number.isFinite(creditReserve) && creditReserve >= 0 ? creditReserve : undefined;
|
|
73
102
|
const credit = candidates
|
|
74
103
|
.filter((c) => excluded.some((e) => e.name === c.name))
|
|
104
|
+
.filter((c) => reserve === undefined || typeof c.creditBalance !== 'number' || c.creditBalance > reserve)
|
|
75
105
|
// 크레딧 잔액이 많은 계정부터(09-28 · 한 계정 크레딧만 타던 것) — 잔액을 모르면 사용률 낮은 순.
|
|
76
106
|
.sort((a, b) => ((b.creditBalance ?? -1) - (a.creditBalance ?? -1)) || ((a.usedPercent ?? 100) - (b.usedPercent ?? 100)) || a.name.localeCompare(b.name))
|
|
77
107
|
.map((c) => c.name);
|
|
108
|
+
if (credit.length === 0 && reserve !== undefined && excluded.length > 0) {
|
|
109
|
+
return { usable, excluded: excluded.map((e) => ({ ...e, why: `${e.why} · credits ≤ reserve ${reserve}` })) };
|
|
110
|
+
}
|
|
78
111
|
return { usable: credit, excluded: [], creditAccounts: credit };
|
|
79
112
|
}
|
|
80
113
|
return { usable, excluded };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEDROCK-PROVIDER ② (0.2.24 · MK) — bedrock 자식 Pod 의 자격·리전·이그레스.
|
|
3
|
+
*
|
|
4
|
+
* 대표 10-10 승인: AWS 자격은 «bedrock Pod 에만» `secretKeyRef` 로 싣는다 · 값은 어디에도 내보내지 않는다 ·
|
|
5
|
+
* 다른 Pod 의 매니페스트는 바이트 하나 안 바뀐다.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ 자격은 «작업별 creds Secret» 이 아니라 클러스터에 미리 둔 «이름 있는 Secret»(`elanous-bedrock-aws`)에서 온다.
|
|
8
|
+
* 호스트는 그 Secret 의 «키 이름»만 읽는다(값은 kubectl 밖으로 안 나온다) — 만들기는 `scripts/fleet/bedrock-pod-secret.ts`(사람 승인 뒤).
|
|
9
|
+
* ⛔ SSO 캐시는 Pod 안에서 못 쓴다 — Pod 용 정적 키(또는 베어러 토큰)가 그 Secret 에 있어야 한다.
|
|
10
|
+
* ⛔ Secret 이 없거나 자격 키가 모자라면 «발사 전에» 거부한다(아무것도 apply 하지 않는다 · fail-closed).
|
|
11
|
+
* ⛔ 이 모듈은 가볍게 둔다(무거운 import 0).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 클러스터(풀 노드마다)·네임스페이스(Pod 와 같은 곳)에 미리 둔 Secret 이름. */
|
|
15
|
+
export const BEDROCK_POD_SECRET_NAME = 'elanous-bedrock-aws';
|
|
16
|
+
|
|
17
|
+
/** bedrock Pod 만 다는 이그레스 라벨 — `docker/h1/policy-bedrock.yaml` 이 이 라벨에만 443 을 연다. */
|
|
18
|
+
export const BEDROCK_POD_EGRESS_LABEL = 'elanous.egress/bedrock';
|
|
19
|
+
|
|
20
|
+
/** Secret 에서 Pod env 로 «이름 그대로» 싣는 키(있는 것만). 값은 secretKeyRef 로만 간다. */
|
|
21
|
+
export const BEDROCK_POD_SECRET_KEYS = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_BEARER_TOKEN_BEDROCK', 'AWS_REGION'] as const;
|
|
22
|
+
export type BedrockPodSecretKey = typeof BEDROCK_POD_SECRET_KEYS[number];
|
|
23
|
+
|
|
24
|
+
/** Secret 의 «키 이름»만 뽑는 kubectl 인자 — go-template 이 값을 찍지 않는다. */
|
|
25
|
+
export function bedrockPodSecretKeyNamesArgs(namespace: string, secretName = BEDROCK_POD_SECRET_NAME): string[] {
|
|
26
|
+
return ['-n', namespace, 'get', 'secret', secretName, '-o', 'go-template={{range $k, $v := .data}}{{$k}}{{"\\n"}}{{end}}'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BedrockPodSpec {
|
|
30
|
+
secretName: string;
|
|
31
|
+
/** Secret 에 «있는» 자격 키 이름(값 아님). */
|
|
32
|
+
keys: BedrockPodSecretKey[];
|
|
33
|
+
/** 리전 — Secret 의 `AWS_REGION` 키가 있으면 그것(secretKeyRef), 없으면 호스트가 푼 리전(평문 · 비밀 아님). */
|
|
34
|
+
region: { from: 'secret' } | { from: 'host'; value: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type BedrockPodCheck = { ok: true; spec: BedrockPodSpec } | { ok: false; reason: string };
|
|
38
|
+
|
|
39
|
+
type KubectlLike = (args: string[], stdin?: string) => { status: number | null; stdout: string; stderr: string };
|
|
40
|
+
|
|
41
|
+
const AWS_REGION_SHAPE = /^[a-z]{2,4}(?:-[a-z]+)+-\d{1,2}$/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 발사 «전» 판정 — Secret 이 있나 · 자격 키가 갖춰졌나 · 리전이 풀리나. 어느 하나라도 아니면 거부 사유.
|
|
45
|
+
* ⛔ 사유 문면에 값·kubectl stderr 원문을 싣지 않는다(이름·고정 문면만).
|
|
46
|
+
*/
|
|
47
|
+
export function checkBedrockPodSecret(kubectl: KubectlLike, namespace: string, hostRegion: string | undefined, secretName = BEDROCK_POD_SECRET_NAME): BedrockPodCheck {
|
|
48
|
+
const fix = `scripts/fleet/bedrock-pod-secret.ts 로 ${namespace}/${secretName} 을 만든다(승인 뒤)`;
|
|
49
|
+
const got = kubectl(bedrockPodSecretKeyNamesArgs(namespace, secretName));
|
|
50
|
+
if (got.status !== 0) {
|
|
51
|
+
return { ok: false, reason: /not ?found/i.test(got.stderr) ? `pod: bedrock 자격 Secret 없음 — ${namespace}/${secretName} · ${fix}` : `pod: bedrock 자격 Secret 을 못 읽었다 — ${namespace}/${secretName} (kubectl rc=${got.status}) · 발사하지 않는다` };
|
|
52
|
+
}
|
|
53
|
+
const present = new Set(got.stdout.split('\n').map((line) => line.trim()).filter(Boolean));
|
|
54
|
+
const keys = BEDROCK_POD_SECRET_KEYS.filter((key) => present.has(key));
|
|
55
|
+
const staticPair = keys.includes('AWS_ACCESS_KEY_ID') && keys.includes('AWS_SECRET_ACCESS_KEY');
|
|
56
|
+
const bearer = keys.includes('AWS_BEARER_TOKEN_BEDROCK');
|
|
57
|
+
if (!staticPair && !bearer) {
|
|
58
|
+
return { ok: false, reason: `pod: bedrock 자격 Secret ${namespace}/${secretName} 에 자격 키가 없다 — AWS_ACCESS_KEY_ID ⊕ AWS_SECRET_ACCESS_KEY 또는 AWS_BEARER_TOKEN_BEDROCK 필요 · ${fix}` };
|
|
59
|
+
}
|
|
60
|
+
const host = hostRegion?.trim();
|
|
61
|
+
const region: BedrockPodSpec['region'] | undefined = keys.includes('AWS_REGION') ? { from: 'secret' }
|
|
62
|
+
: host && AWS_REGION_SHAPE.test(host) ? { from: 'host', value: host } : undefined;
|
|
63
|
+
if (!region) return { ok: false, reason: `pod: bedrock 리전 없음 — Secret ${namespace}/${secretName} 의 AWS_REGION 또는 호스트 AWS_REGION(·~/.aws/config) 필요` };
|
|
64
|
+
return { ok: true, spec: { secretName, keys: keys.filter((key) => key !== 'AWS_REGION' || region.from === 'secret'), region } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** bedrock 자식 컨테이너 env — 자격은 secretKeyRef, 리전은 secretKeyRef 또는 평문. IMDS 탐색은 끈다(Pod 에 인스턴스 역할 없음). */
|
|
68
|
+
export function bedrockPodEnv(spec: BedrockPodSpec): Array<Record<string, unknown>> {
|
|
69
|
+
return [
|
|
70
|
+
...spec.keys.map((key) => ({ name: key, valueFrom: { secretKeyRef: { name: spec.secretName, key } } })),
|
|
71
|
+
...(spec.region.from === 'host' ? [{ name: 'AWS_REGION', value: spec.region.value }] : []),
|
|
72
|
+
{ name: 'AWS_EC2_METADATA_DISABLED', value: 'true' },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** SUBSCRIPTION-EXHAUST-REROUTE (0.2.24 · P0) — Pod 구현 자식의 «기본» 모델을 codex 밖으로 뺀다.
|
|
2
|
+
*
|
|
3
|
+
* 🩸 10-11 실측(apiEq 24h): codex 소모의 ~95% 가 node-b Pod 구현 자식이었다($2,633 · 107런). 원인은
|
|
4
|
+
* 자식 모델을 안 준 Pod 발사가 `planPodProvider` 로 codex(gpt-6-sol)에 떨어진 것이다.
|
|
5
|
+
* ⭐ 결정 지점은 «호스트가 발사 인자를 만드는 곳»(`self orchestrate --substrate pod`) — 여기서 «자식 모델 없음 → 기본값»을
|
|
6
|
+
* `--child-llm-provider/--child-llm-model` 과 «같은 모양»으로 채운다. 그 뒤 경로(키 확인·passEnv·Job 인자)는 명시 발사와 같다.
|
|
7
|
+
* ⛔ 명시(`--child-llm-*`)가 있으면 손대지 않는다 · `--bench-arms`(팔마다 모델) · `--pod-account`(codex 계정 고정 = 사람의 선택)도 손대지 않는다.
|
|
8
|
+
* ⛔ 기본값이 «못 서면»(OpenRouter 키 없음) 종전 codex 경로로 간다 — 기본값 때문에 발사가 죽지 않는다(관측은 남긴다).
|
|
9
|
+
* config = `tools.selfImplement.podChildDefault`: `{ provider, model? }` · `'off'`(종전 codex 기본). 없으면 아래 상수. */
|
|
10
|
+
|
|
11
|
+
/** 대표 라우팅 «중» 시작 모델 — OpenRouter 키는 Pod 에 이미 있다(새 자격 0). */
|
|
12
|
+
export const POD_CHILD_DEFAULT = { provider: 'openrouter', model: 'openrouter/z-ai/glm-5.3' } as const;
|
|
13
|
+
|
|
14
|
+
export type PodChildDefaultDecision =
|
|
15
|
+
| { apply: true; provider: string; model?: string; source: 'config' | 'default' }
|
|
16
|
+
| { apply: false; reason: 'flag' | 'bench-arms' | 'pod-account' | 'config-off' | 'no-openrouter-key'; provider?: string; model?: string; source?: 'config' | 'default' };
|
|
17
|
+
|
|
18
|
+
function parseConfigValue(raw: unknown): { value: { provider: string; model?: string } | 'off'; source: 'config' } | { value: typeof POD_CHILD_DEFAULT; source: 'default'; invalid?: unknown } {
|
|
19
|
+
if (raw === undefined) return { value: POD_CHILD_DEFAULT, source: 'default' };
|
|
20
|
+
if (raw === 'off' || raw === false) return { value: 'off', source: 'config' };
|
|
21
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
22
|
+
const v = raw as Record<string, unknown>;
|
|
23
|
+
const provider = typeof v.provider === 'string' ? v.provider.trim() : '';
|
|
24
|
+
const model = typeof v.model === 'string' ? v.model.trim() : '';
|
|
25
|
+
if (provider) return { value: { provider, ...(model ? { model } : {}) }, source: 'config' };
|
|
26
|
+
}
|
|
27
|
+
return { value: POD_CHILD_DEFAULT, source: 'default', invalid: raw };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 순수 판정 — 「무엇을 보고 정했나」가 인자에 다 있다. */
|
|
31
|
+
export function resolvePodChildDefault(input: {
|
|
32
|
+
flags: { provider?: string; model?: string; effort?: string };
|
|
33
|
+
benchArms: boolean;
|
|
34
|
+
explicitPodAccount?: string;
|
|
35
|
+
config: unknown;
|
|
36
|
+
/** OpenRouter 키가 호스트에 «있나»(값은 안 본다) — openrouter 기본일 때만 묻는다. */
|
|
37
|
+
openrouterKeyPresent: () => boolean;
|
|
38
|
+
}): PodChildDefaultDecision & { invalidConfig?: unknown } {
|
|
39
|
+
if (input.flags.provider?.trim() || input.flags.model?.trim() || input.flags.effort?.trim()) return { apply: false, reason: 'flag' };
|
|
40
|
+
if (input.benchArms) return { apply: false, reason: 'bench-arms' };
|
|
41
|
+
if (input.explicitPodAccount?.trim()) return { apply: false, reason: 'pod-account' };
|
|
42
|
+
const parsed = parseConfigValue(input.config);
|
|
43
|
+
const invalid = 'invalid' in parsed ? { invalidConfig: parsed.invalid } : {};
|
|
44
|
+
if (parsed.value === 'off') return { apply: false, reason: 'config-off', source: 'config' };
|
|
45
|
+
const { provider, model } = parsed.value as { provider: string; model?: string };
|
|
46
|
+
if (provider === 'openrouter' && !input.openrouterKeyPresent()) {
|
|
47
|
+
return { apply: false, reason: 'no-openrouter-key', provider, ...(model ? { model } : {}), source: parsed.source, ...invalid };
|
|
48
|
+
}
|
|
49
|
+
return { apply: true, provider, ...(model ? { model } : {}), source: parsed.source, ...invalid };
|
|
50
|
+
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* 목록 밖 provider 는 발사 «전»에 같은 문면으로 거부된다.
|
|
8
8
|
* ⛔ 이 모듈은 가볍게 둔다(무거운 import 0) — 하니스 CLI 가 Pod 표면 전체를 끌어오지 않게.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
// BEDROCK-PROVIDER ② (0.2.24) — bedrock 은 «이름 있는 AWS Secret» 이 클러스터에 있을 때만 뜬다(pod-bedrock-child.ts · 발사 전 거부).
|
|
11
|
+
// ZAI-POD-CHILD (0.2.24) — glm 은 Z.ai GLM Coding Plan(정액 구독) · «이름 있는 ZAI Secret» 이 클러스터에 있을 때만 뜬다(pod-zai-child.ts · 발사 전 거부 · 동시 상한 넘치면 정책대로).
|
|
12
|
+
export const POD_CHILD_PROVIDERS = ['openai-codex', 'grok', 'openrouter', 'anthropic', 'bedrock', 'glm'] as const;
|
|
11
13
|
export type PodChildProvider = typeof POD_CHILD_PROVIDERS[number];
|
|
12
14
|
|
|
13
15
|
/** 사람이 쓰는 별칭 → 정식 이름. */
|
|
@@ -27,3 +29,14 @@ export function podChildProviderRefusal(named: string | undefined): string | und
|
|
|
27
29
|
if (!id || podChildProviderOf(id)) return undefined;
|
|
28
30
|
return `pod: Pod 자식 provider 는 ${POD_CHILD_PROVIDERS.join('|')} 만 — 받음 ${id}`;
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* BEDROCK-POD-PROVIDER-DROP (0.2.24) — 발사(`index.ts`)가 고른 Pod provider 를 Pod 옵션(`provider` · `account`)으로 옮기는 «한 곳».
|
|
35
|
+
* 🩸 10-11: podBase 가 grok·openrouter·anthropic 만 손으로 나열해 `bedrock` 이 떨어졌다 — Pod 자식은 codex(team) 로 돌았다.
|
|
36
|
+
* ⭐ codex 가 아닌 provider 는 «그 이름»이 곧 account 다(Pod 표면 `plannedAccount` 와 같은 규칙) · codex 만 명시 계정 ?? team.
|
|
37
|
+
*/
|
|
38
|
+
export function podLaunchProviderOptions(podProvider: PodChildProvider, o: { explicitPodAccount?: string; grokApiKeyOptIn: boolean }): { account: string; provider: PodChildProvider; grokApiKeyOptIn?: boolean } {
|
|
39
|
+
// provider 는 여섯 전부 싣는다 — Pod 의 podChildLlmArgs 는 명시 발사에 provider 가 비면 거부한다(조용한 codex 금지).
|
|
40
|
+
if (podProvider === 'openai-codex') return { account: o.explicitPodAccount ?? 'team', provider: 'openai-codex' };
|
|
41
|
+
return { account: podProvider, provider: podProvider, ...(podProvider === 'grok' ? { grokApiKeyOptIn: o.grokApiKeyOptIn } : {}) };
|
|
42
|
+
}
|
|
@@ -44,6 +44,81 @@ export function podTerminalRow(row: Record<string, unknown>): { stage: string; o
|
|
|
44
44
|
return nestedPodResult(row);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export const POD_EXIT_CLASSES = ['oom-killed', 'deadline-exceeded', 'gh-login-failed', 'pod-secret', 'ask-reprompt', 'child-unconverged', 'child-error', 'no-result-line', 'unknown'] as const;
|
|
48
|
+
|
|
49
|
+
export type PodExitClass = typeof POD_EXIT_CLASSES[number];
|
|
50
|
+
|
|
51
|
+
const isPodBookkeepingLine = (line: string): boolean =>
|
|
52
|
+
/^(?:ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)|\[graph\] |\[ask\]\s+\d+\)\s)/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
53
|
+
|
|
54
|
+
const isPodErrorLine = (line: string): boolean =>
|
|
55
|
+
/\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
|
|
56
|
+
|
|
57
|
+
const normalizePodLogLine = (line: string): string => line.replace(/\/home\/[^/\s]+\//gu, '~/').slice(0, 240);
|
|
58
|
+
|
|
59
|
+
/** Classify a failed Pod using its terminal metadata and child output, without side effects. */
|
|
60
|
+
export function classifyPodExit(input: {
|
|
61
|
+
errorCode?: string | null;
|
|
62
|
+
containerReason?: string | null;
|
|
63
|
+
containerExitCode?: number | null;
|
|
64
|
+
jobReason?: string | null;
|
|
65
|
+
logs: string;
|
|
66
|
+
result?: Record<string, unknown> | null;
|
|
67
|
+
}): { class: PodExitClass; errorCode: string | null; containerReason: string | null; exitCode: number | null; jobReason: string | null; tail: string[] } {
|
|
68
|
+
const logLines = input.logs.split(/\r\n|\n|\r/u).map((line) => line.trim()).filter(Boolean);
|
|
69
|
+
let terminalResult: { stage: string; ok: boolean; error?: string } | null = null;
|
|
70
|
+
for (const line of logLines) {
|
|
71
|
+
if (!line.startsWith('{')) continue;
|
|
72
|
+
let parsed: unknown;
|
|
73
|
+
try { parsed = JSON.parse(line); } catch { continue; }
|
|
74
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
|
|
75
|
+
terminalResult = podTerminalRow(parsed as Record<string, unknown>) ?? terminalResult;
|
|
76
|
+
}
|
|
77
|
+
if (!terminalResult && input.result) terminalResult = podTerminalRow(input.result);
|
|
78
|
+
const hasResultLine = terminalResult !== null;
|
|
79
|
+
const askReprompt = logLines.some((line) =>
|
|
80
|
+
/^\[ask\](?!\s+\d+\)\s).*(?:implementation[_ -]?target|reprompt|re-prompt|되묻)/iu.test(line));
|
|
81
|
+
// 파싱되는 JSON 줄(진행·결과 행)은 오류 «줄»로 세지 않는다 — tail 과 같은 기준.
|
|
82
|
+
const isParsedJsonLine = (line: string): boolean => {
|
|
83
|
+
if (!line.startsWith('{')) return false;
|
|
84
|
+
try { const parsed = JSON.parse(line); return !!parsed && typeof parsed === 'object' && !Array.isArray(parsed); } catch { return false; }
|
|
85
|
+
};
|
|
86
|
+
const hasErrorLine = logLines.some((line) => !isPodBookkeepingLine(line) && !isParsedJsonLine(line) && isPodErrorLine(line));
|
|
87
|
+
let exitClass: PodExitClass;
|
|
88
|
+
if (input.errorCode === 'pod-secret') exitClass = 'pod-secret';
|
|
89
|
+
else if (input.jobReason === 'DeadlineExceeded') exitClass = 'deadline-exceeded';
|
|
90
|
+
else if (input.containerReason === 'OOMKilled') exitClass = 'oom-killed';
|
|
91
|
+
else if (input.containerExitCode === 7) exitClass = 'gh-login-failed';
|
|
92
|
+
else if (askReprompt) exitClass = 'ask-reprompt';
|
|
93
|
+
else if (terminalResult?.stage && terminalResult.ok === false) exitClass = 'child-unconverged';
|
|
94
|
+
else if (!hasResultLine && hasErrorLine) exitClass = 'child-error';
|
|
95
|
+
else if (!hasResultLine) exitClass = 'no-result-line';
|
|
96
|
+
else exitClass = 'unknown';
|
|
97
|
+
|
|
98
|
+
const tail = logLines.flatMap((line) => {
|
|
99
|
+
if (line.startsWith('{')) {
|
|
100
|
+
let parsed: unknown;
|
|
101
|
+
try { parsed = JSON.parse(line); } catch { parsed = undefined; }
|
|
102
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
103
|
+
const terminal = podTerminalRow(parsed as Record<string, unknown>);
|
|
104
|
+
if (terminal?.error) return terminal.error.split(/\r\n|\n|\r/u).map((part) => part.trim())
|
|
105
|
+
.filter((part) => part && !isPodBookkeepingLine(part)).map(normalizePodLogLine);
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return isPodBookkeepingLine(line) ? [] : [normalizePodLogLine(line)];
|
|
110
|
+
}).slice(-5);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
class: exitClass,
|
|
114
|
+
errorCode: input.errorCode ?? null,
|
|
115
|
+
containerReason: input.containerReason ?? null,
|
|
116
|
+
exitCode: input.containerExitCode ?? null,
|
|
117
|
+
jobReason: input.jobReason ?? null,
|
|
118
|
+
tail,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
47
122
|
/** Extract a human-readable reason from a failed child Pod without treating transfer markers as child errors. */
|
|
48
123
|
export function extractPodFailureReason(input: {
|
|
49
124
|
logs: string;
|
|
@@ -59,10 +134,8 @@ export function extractPodFailureReason(input: {
|
|
|
59
134
|
}
|
|
60
135
|
if (input.containerReason === 'OOMKilled') return 'OOMKilled: 컨테이너 메모리 한도 초과';
|
|
61
136
|
|
|
62
|
-
const isBookkeeping =
|
|
63
|
-
|
|
64
|
-
const isError = (line: string): boolean =>
|
|
65
|
-
/\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
|
|
137
|
+
const isBookkeeping = isPodBookkeepingLine;
|
|
138
|
+
const isError = isPodErrorLine;
|
|
66
139
|
const meaningful: Array<{ text: string; terminal: boolean }> = [];
|
|
67
140
|
for (const line of input.logs.split(/\r\n|\n|\r/u)) {
|
|
68
141
|
const trimmed = line.trim();
|
|
@@ -90,7 +163,7 @@ export function extractPodFailureReason(input: {
|
|
|
90
163
|
meaningful.push({ text: trimmed, terminal: false });
|
|
91
164
|
}
|
|
92
165
|
const errorLine = [...meaningful].reverse().find((line) => line.terminal || isError(line.text));
|
|
93
|
-
const reason = errorLine
|
|
166
|
+
const reason = errorLine ? normalizePodLogLine(errorLine.text) : undefined;
|
|
94
167
|
if (reason) return reason;
|
|
95
168
|
// POD-NORESULT: an unconverged child (exit 2 → BackoffLimitExceeded under backoffLimit 0) often leaves a result row
|
|
96
169
|
// with a stage and PR but no error text — report that instead of «unreadable».
|
|
@@ -101,5 +174,5 @@ export function extractPodFailureReason(input: {
|
|
|
101
174
|
return pr ? `수확 가능(${stage}) · ${pr}` : `수렴 못 함(단계 ${stage})`;
|
|
102
175
|
}
|
|
103
176
|
const why = input.logTailReason?.trim() || (input.logs.trim() ? '로그에 읽을 수 있는 오류 줄 없음' : '자식 로그 비어 있음');
|
|
104
|
-
return `사유 못 읽음: ${why
|
|
177
|
+
return `사유 못 읽음: ${normalizePodLogLine(why)}`;
|
|
105
178
|
}
|
|
@@ -40,6 +40,8 @@ export function leaseKubectl(args: readonly string[], input?: string): { status:
|
|
|
40
40
|
export interface PodLeaseMember {
|
|
41
41
|
context: string;
|
|
42
42
|
capacity: number;
|
|
43
|
+
/** POD-POOL-UNCAPPED-SPEC — `capacity` came from the spec default (`:N` absent), not from the operator. */
|
|
44
|
+
capacityDefaulted?: boolean;
|
|
43
45
|
running: number | null;
|
|
44
46
|
pending: number | null;
|
|
45
47
|
/** Job names of Pending harness Pods already included in pending (used to avoid counting the same Job's host lease twice). */
|
|
@@ -94,7 +96,12 @@ export interface PoolLeaseMeasure {
|
|
|
94
96
|
|
|
95
97
|
export interface PoolLeaseRecommendation {
|
|
96
98
|
recommended: number | null;
|
|
97
|
-
|
|
99
|
+
/**
|
|
100
|
+
* What stopped admission: `memory` = node memory headroom · `capacity` = the pool slot budget ·
|
|
101
|
+
* `pool-cap` = POD-POOL-UNCAPPED-SPEC — a member whose cap came from the spec default (`:N` absent → 2) is
|
|
102
|
+
* already at/over that cap, so the pool spec itself (not memory, not a configured budget) is the bound.
|
|
103
|
+
*/
|
|
104
|
+
limitedBy: 'capacity' | 'memory' | 'pool-cap' | null;
|
|
98
105
|
reason: string | null;
|
|
99
106
|
capacitySlots: number | null;
|
|
100
107
|
memorySlots: number | null;
|
|
@@ -392,7 +399,7 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
392
399
|
const kubectl = deps.kubectl ?? leaseKubectl;
|
|
393
400
|
return { members: members.map((member) => {
|
|
394
401
|
const base = ['--context', member.context, '--request-timeout=10s'];
|
|
395
|
-
const result: PodLeaseMember = { context: member.context, capacity: member.capacity, running: null, pending: null, unleasedRunning: null, memoryLimitBytes: null, allocatableMemoryBytes: null, allocatableCpuMillicores: null, availableMemoryByNodeBytes: null, reason: null };
|
|
402
|
+
const result: PodLeaseMember = { context: member.context, capacity: member.capacity, ...(member.capacityDefaulted ? { capacityDefaulted: true } : {}), running: null, pending: null, unleasedRunning: null, memoryLimitBytes: null, allocatableMemoryBytes: null, allocatableCpuMillicores: null, availableMemoryByNodeBytes: null, reason: null };
|
|
396
403
|
try {
|
|
397
404
|
const nodes = kubectl([...base, 'get', 'nodes', '-o', 'json']);
|
|
398
405
|
const jobs = kubectl([...base, '-n', 'elanous-test', 'get', 'jobs', '-l', 'elanous.substrate=pod', '-o', 'json']);
|
|
@@ -635,7 +642,7 @@ export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capac
|
|
|
635
642
|
if (goalBytes === null || goalBytes <= 0) return { ...empty, reason: '측정 불가: perGoalMemory' };
|
|
636
643
|
const admission = admissionByUsage(measure, goalBytes, options.perGoalMemory !== undefined);
|
|
637
644
|
if (!measure.members.length || measure.members.some((m) => m.running === null || m.pending === null || m.unleasedRunning === null || m.allocatableMemoryBytes === null || m.memoryLimitBytes === null || m.allocatableCpuMillicores === null || m.availableMemoryByNodeBytes === null)) {
|
|
638
|
-
debug.log('pod-lease', 'admit-by-usage', { samples: admission.samples, p95Bytes: admission.p95Bytes, admitBytes: admission.admitBytes, fallback: admission.fallback, recommended: null, limitedBy: null });
|
|
645
|
+
debug.log('pod-lease', 'admit-by-usage', { samples: admission.samples, p95Bytes: admission.p95Bytes, admitBytes: admission.admitBytes, fallback: admission.fallback, recommended: null, limitedBy: null, capacity: options.capacity, capacitySlots: null, memorySlots: null, running: null, pending: null });
|
|
639
646
|
return { ...empty, reason: `측정 불가: cluster${measure.members.map((m) => m.reason ? ` ${m.context}: ${m.reason}` : '').join('')}` };
|
|
640
647
|
}
|
|
641
648
|
if (!Number.isSafeInteger(options.capacity) || options.capacity < 0) return { ...empty, reason: '측정 불가: capacity' };
|
|
@@ -680,9 +687,14 @@ export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capac
|
|
|
680
687
|
Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) / liteBytes))), 0);
|
|
681
688
|
const memoryBound = Math.min(memorySlots, placeableSlots);
|
|
682
689
|
const recommended = Math.max(0, Math.min(capacitySlots, memoryBound) - eligibleUnleasedRunning);
|
|
683
|
-
|
|
690
|
+
// POD-POOL-UNCAPPED-SPEC — `capacity ≤ occupied` on a member whose cap is the spec default (`:N` absent → 2) means
|
|
691
|
+
// the «uncapped» pool spec is what admits nothing. Name it `pool-cap`, not the generic `capacity`, so a launch that
|
|
692
|
+
// sits on `recommended=0` says in its first admit line that the pool spec never declared a cap of its own.
|
|
693
|
+
const defaultCapFull = capacitySlots <= 0 && measure.members.some((m) => m.capacityDefaulted === true
|
|
694
|
+
&& m.reason !== 'dns' && m.running !== null && m.pending !== null && m.running + m.pending >= m.capacity);
|
|
695
|
+
const limitedBy: PoolLeaseRecommendation['limitedBy'] = memoryBound < capacitySlots ? 'memory' : defaultCapFull ? 'pool-cap' : 'capacity';
|
|
684
696
|
if (unleasedRunning > 0) debug.log('pod-lease', 'unleased', { unleasedRunning, running, recommended });
|
|
685
|
-
debug.log('pod-lease', 'admit-by-usage', { samples: admission.samples, p95Bytes: admission.p95Bytes, admitBytes: admission.admitBytes, fallback: admission.fallback, recommended, limitedBy });
|
|
697
|
+
debug.log('pod-lease', 'admit-by-usage', { samples: admission.samples, p95Bytes: admission.p95Bytes, admitBytes: admission.admitBytes, fallback: admission.fallback, recommended, limitedBy, capacity: options.capacity, capacitySlots, memorySlots, running, pending });
|
|
686
698
|
return { recommended, limitedBy, reason: measure.members.some((m) => m.reason === 'dns') ? 'dns' : null, capacitySlots, memorySlots, liteMemorySlots, placeableSlots, accountSlots, running, pending, pendingJobs, unleasedRunning, admission };
|
|
687
699
|
}
|
|
688
700
|
|
|
@@ -14,6 +14,8 @@ type ParseResult = Ledger[] | { error: Incomplete[]; ledgers: Ledger[] };
|
|
|
14
14
|
const NON_PROGRESS_EVENTS = new Set(['progress-delivery-outcome']);
|
|
15
15
|
const REEMIT_EXCLUDED_PREFIX = 'progress-delivery';
|
|
16
16
|
type Emit = (category: string, event: string, data: Record<string, unknown>) => void;
|
|
17
|
+
/** Heal-loop starter seam — defaults to the real `startHealLoop` inside `recordFailureEvent`. */
|
|
18
|
+
type StartHealLoop = Parameters<typeof recordFailureEvent>[2];
|
|
17
19
|
|
|
18
20
|
/** Reemit only complete ledger records; offsets name the first byte of each line in the Pod ledger. */
|
|
19
21
|
function reemitLedgerLines(jsonl: string, runId: string, startOffset: number, emit: Emit): number {
|
|
@@ -93,7 +95,7 @@ export function parsePodLedgerChunks(logs: string): ParseResult {
|
|
|
93
95
|
return error.length ? { error, ledgers } : ledgers;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function intakeCollectedFailure(runId: string, dir: string, log: Emit): void {
|
|
98
|
+
function intakeCollectedFailure(runId: string, dir: string, log: Emit, startHealLoop?: StartHealLoop): void {
|
|
97
99
|
let ledger: ReturnType<typeof loadRunLedger>;
|
|
98
100
|
try { ledger = loadRunLedger(runId, dir); }
|
|
99
101
|
catch { return; }
|
|
@@ -104,8 +106,10 @@ function intakeCollectedFailure(runId: string, dir: string, log: Emit): void {
|
|
|
104
106
|
if (terminal?.data.runStatus !== 'failed' || !terminal.timestamp || !Number.isFinite(Date.parse(terminal.timestamp))
|
|
105
107
|
|| ledger.slice(terminalIndex + 1).some(entry => !TRAILING_BOOKKEEPING_EVENTS.includes(entry.event as typeof TRAILING_BOOKKEEPING_EVENTS[number]))) return;
|
|
106
108
|
try {
|
|
109
|
+
// POD-HEAL-HOST — the recovered Pod failure is recorded into the HOST universe (`dirname(dir)`),
|
|
110
|
+
// so the host heal loop triages it (harvest + narrowed relaunch) instead of dying inside the Pod.
|
|
107
111
|
recordFailureEvent({ source: 'harness-run', kind: 'pod-run', ref: runId,
|
|
108
|
-
summary: `Pod run ${runId} failed`, at: terminal.timestamp }, dirname(dir));
|
|
112
|
+
summary: `Pod run ${runId} failed`, at: terminal.timestamp }, dirname(dir), startHealLoop);
|
|
109
113
|
} catch (error) {
|
|
110
114
|
log('self-implement.pod', 'heal-intake-failed', { runId, reason: error instanceof Error ? error.message : String(error) });
|
|
111
115
|
}
|
|
@@ -115,11 +119,13 @@ function intakeCollectedFailure(runId: string, dir: string, log: Emit): void {
|
|
|
115
119
|
* except a ledger that THIS run's live follower created (`replace`): that one is a partial copy and the final is complete. */
|
|
116
120
|
export function collectPodLedgers(
|
|
117
121
|
logs: string,
|
|
118
|
-
{ dir = runLedgerDir(), log = defaultPodReemit(), emit = defaultPodReemit(), replace = new Set<string>() }: {
|
|
122
|
+
{ dir = runLedgerDir(), log = defaultPodReemit(), emit = defaultPodReemit(), replace = new Set<string>(), startHealLoop }: {
|
|
119
123
|
dir?: string;
|
|
120
124
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
121
125
|
emit?: Emit;
|
|
122
126
|
replace?: ReadonlySet<string>;
|
|
127
|
+
/** Which starter receives a recovered Pod failure (POD-HEAL-HOST 판정선 measurement); default = real `startHealLoop`. */
|
|
128
|
+
startHealLoop?: StartHealLoop;
|
|
123
129
|
} = {},
|
|
124
130
|
): void {
|
|
125
131
|
const parsed = parsePodLedgerChunks(logs);
|
|
@@ -136,7 +142,7 @@ export function collectPodLedgers(
|
|
|
136
142
|
const newLines = Buffer.from(jsonl).subarray(previousBytes).toString('utf8');
|
|
137
143
|
reemitLedgerLines(newLines, runId, previousBytes, emit);
|
|
138
144
|
log('self-implement.pod', 'ledger-collected', { runId, lines: jsonl.split('\n').filter(Boolean).length, bytes: Buffer.byteLength(jsonl) });
|
|
139
|
-
intakeCollectedFailure(runId, dir, log);
|
|
145
|
+
intakeCollectedFailure(runId, dir, log, startHealLoop);
|
|
140
146
|
} catch (e) {
|
|
141
147
|
if ((e as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
142
148
|
// A reattached Job may have already delivered this exact complete snapshot.
|
|
@@ -145,7 +151,7 @@ export function collectPodLedgers(
|
|
|
145
151
|
const existing = readFileSync(runLedgerPath(runId, dir));
|
|
146
152
|
const identical = existing.equals(Buffer.from(jsonl));
|
|
147
153
|
log('self-implement.pod', identical ? 'ledger-collect-already-complete' : 'ledger-collect-skipped', { runId, reason: 'exists' });
|
|
148
|
-
if (identical) intakeCollectedFailure(runId, dir, log);
|
|
154
|
+
if (identical) intakeCollectedFailure(runId, dir, log, startHealLoop);
|
|
149
155
|
} catch (readError) {
|
|
150
156
|
log('self-implement.pod', 'ledger-collect-incomplete', { runId, reason: readError instanceof Error ? readError.message : String(readError) });
|
|
151
157
|
}
|