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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAGE-LEDGER-TAGS (RFC-adaptive-graph-ops §9.11) — first slice: the image ledger.
|
|
3
|
+
*
|
|
4
|
+
* The registry tag itself carries only the 12-char commit (docker/harness/build.sh:276), so the
|
|
5
|
+
* identity of an image (why it exists, how long to keep it) lives in a small append-only ledger
|
|
6
|
+
* next to the config that pins it (`<config dir>/image-ledger.jsonl`). Writers append events;
|
|
7
|
+
* readers fold them per tag. A registry tag with no ledger row is `transient` (RFC §9.11).
|
|
8
|
+
*
|
|
9
|
+
* Writers in this slice: the baseline pin (`config set harness.imageCommit <commit>` — the rail
|
|
10
|
+
* pins through that command). Launch · gate · release writers and `image tag` are later slices.
|
|
11
|
+
*
|
|
12
|
+
* Unreadable is never "none": a ledger line that fails to parse is counted, and a registry that
|
|
13
|
+
* cannot be read is reported as `null` (not as an empty tag list).
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { hostname } from 'node:os';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
20
|
+
|
|
21
|
+
export const IMAGE_ROLES = ['baseline', 'release', 'gate', 'demo', 'candidate', 'transient'] as const;
|
|
22
|
+
export type ImageRole = typeof IMAGE_ROLES[number];
|
|
23
|
+
export const IMAGE_KEEPS = ['pin', 'long', 'short', 'gc'] as const;
|
|
24
|
+
export type ImageKeep = typeof IMAGE_KEEPS[number];
|
|
25
|
+
export const IMAGE_ORIGIN_REASONS = ['rail', 'launch', 'gate', 'release', 'manual', 'config-set'] as const;
|
|
26
|
+
export type ImageOriginReason = typeof IMAGE_ORIGIN_REASONS[number];
|
|
27
|
+
|
|
28
|
+
export interface ImageLedgerEvent {
|
|
29
|
+
readonly v: 1;
|
|
30
|
+
readonly ts: string;
|
|
31
|
+
/** Registry tag = first 12 hex chars of the commit (build.sh:276). */
|
|
32
|
+
readonly tag: string;
|
|
33
|
+
readonly commit: string;
|
|
34
|
+
readonly role: ImageRole;
|
|
35
|
+
readonly keep: ImageKeep;
|
|
36
|
+
readonly origin: { readonly host: string; readonly reason: ImageOriginReason };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ImageLedgerRead {
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly exists: boolean;
|
|
42
|
+
readonly events: readonly ImageLedgerEvent[];
|
|
43
|
+
/** Lines that were present but did not parse as a ledger event. */
|
|
44
|
+
readonly unreadableLines: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ImageLedgerRow {
|
|
48
|
+
readonly tag: string;
|
|
49
|
+
readonly commit: string;
|
|
50
|
+
readonly role: ImageRole;
|
|
51
|
+
readonly keep: ImageKeep;
|
|
52
|
+
/** Origin of the first event for this tag · null for a registry-only (unledgered) tag — its origin is unknown. */
|
|
53
|
+
readonly origin: { readonly host: string; readonly reason: ImageOriginReason } | null;
|
|
54
|
+
readonly firstTs: string;
|
|
55
|
+
readonly lastTs: string;
|
|
56
|
+
/** For `baseline`: true only for the most recently pinned baseline. */
|
|
57
|
+
readonly current: boolean;
|
|
58
|
+
/** true when the tag is on the registry · false when not · null when the registry was not read. */
|
|
59
|
+
readonly inRegistry: boolean | null;
|
|
60
|
+
/** true when the row comes only from the registry (no ledger event) — always role `transient`. */
|
|
61
|
+
readonly unledgered: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const HEX = /^[0-9a-f]{12,40}$/u;
|
|
65
|
+
|
|
66
|
+
export function imageLedgerPath(configDir: string = getElanousConfigDir()): string {
|
|
67
|
+
return join(configDir, 'image-ledger.jsonl');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function imageTag(commit: string): string {
|
|
71
|
+
return commit.trim().toLowerCase().slice(0, 12);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isEvent(value: unknown): value is ImageLedgerEvent {
|
|
75
|
+
if (!value || typeof value !== 'object') return false;
|
|
76
|
+
const e = value as Record<string, unknown>;
|
|
77
|
+
const origin = e.origin as Record<string, unknown> | undefined;
|
|
78
|
+
return e.v === 1 && typeof e.ts === 'string' && typeof e.tag === 'string' && /^[0-9a-f]{12}$/u.test(e.tag)
|
|
79
|
+
&& typeof e.commit === 'string' && HEX.test(e.commit)
|
|
80
|
+
&& (IMAGE_ROLES as readonly unknown[]).includes(e.role) && (IMAGE_KEEPS as readonly unknown[]).includes(e.keep)
|
|
81
|
+
&& !!origin && typeof origin.host === 'string' && (IMAGE_ORIGIN_REASONS as readonly unknown[]).includes(origin.reason);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readImageLedger(path: string = imageLedgerPath()): ImageLedgerRead {
|
|
85
|
+
if (!existsSync(path)) return { path, exists: false, events: [], unreadableLines: 0 };
|
|
86
|
+
const events: ImageLedgerEvent[] = [];
|
|
87
|
+
let unreadableLines = 0;
|
|
88
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
89
|
+
if (line.trim() === '') continue;
|
|
90
|
+
try {
|
|
91
|
+
const parsed: unknown = JSON.parse(line);
|
|
92
|
+
if (isEvent(parsed)) events.push(parsed); else unreadableLines += 1;
|
|
93
|
+
} catch { unreadableLines += 1; }
|
|
94
|
+
}
|
|
95
|
+
return { path, exists: true, events, unreadableLines };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function appendImageLedgerEvent(event: ImageLedgerEvent, path: string = imageLedgerPath()): void {
|
|
99
|
+
if (!isEvent(event)) throw new Error(`invalid image ledger event: ${JSON.stringify(event)}`);
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
+
appendFileSync(path, `${JSON.stringify(event)}\n`, { mode: 0o600 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The baseline pin event — what `config set harness.imageCommit <commit>` records. */
|
|
105
|
+
export function baselinePinEvent(commit: string, deps: { now?: () => Date; host?: string } = {}): ImageLedgerEvent {
|
|
106
|
+
const c = commit.trim().toLowerCase();
|
|
107
|
+
if (!HEX.test(c)) throw new Error(`not a commit (hex 12~40): ${commit}`);
|
|
108
|
+
return {
|
|
109
|
+
v: 1,
|
|
110
|
+
ts: (deps.now?.() ?? new Date()).toISOString(),
|
|
111
|
+
tag: imageTag(c),
|
|
112
|
+
commit: c,
|
|
113
|
+
role: 'baseline',
|
|
114
|
+
keep: 'pin',
|
|
115
|
+
origin: { host: deps.host ?? hostname(), reason: 'config-set' },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fold events per tag (the last event decides role and keep), then add registry tags that have no
|
|
121
|
+
* ledger event as `transient`. `registryTags === null` means the registry was not read.
|
|
122
|
+
* Non-commit registry tags (`latest`) are moving aliases and are not rows.
|
|
123
|
+
*/
|
|
124
|
+
export function foldImageLedger(events: readonly ImageLedgerEvent[], registryTags: readonly string[] | null): ImageLedgerRow[] {
|
|
125
|
+
const byTag = new Map<string, { first: ImageLedgerEvent; last: ImageLedgerEvent }>();
|
|
126
|
+
// Events for one tag may carry the 12-char tag or the full 40-char commit; the row keeps the longest one seen.
|
|
127
|
+
const fullCommit = new Map<string, string>();
|
|
128
|
+
for (const e of events) {
|
|
129
|
+
const seen = byTag.get(e.tag);
|
|
130
|
+
if (!seen) byTag.set(e.tag, { first: e, last: e });
|
|
131
|
+
else if (e.ts >= seen.last.ts) seen.last = e;
|
|
132
|
+
if (e.commit.length > (fullCommit.get(e.tag)?.length ?? 0)) fullCommit.set(e.tag, e.commit);
|
|
133
|
+
}
|
|
134
|
+
let currentBaseline: string | null = null;
|
|
135
|
+
let currentTs = '';
|
|
136
|
+
for (const e of events) if (e.role === 'baseline' && e.ts >= currentTs) { currentBaseline = e.tag; currentTs = e.ts; }
|
|
137
|
+
const registry = registryTags === null ? null : new Set(registryTags.filter((t) => /^[0-9a-f]{12}$/u.test(t)));
|
|
138
|
+
const rows: ImageLedgerRow[] = [...byTag.values()].map(({ first, last }) => ({
|
|
139
|
+
tag: last.tag,
|
|
140
|
+
commit: fullCommit.get(last.tag) ?? last.commit,
|
|
141
|
+
role: last.role,
|
|
142
|
+
keep: last.keep,
|
|
143
|
+
origin: first.origin,
|
|
144
|
+
firstTs: first.ts,
|
|
145
|
+
lastTs: last.ts,
|
|
146
|
+
current: last.role === 'baseline' && last.tag === currentBaseline,
|
|
147
|
+
inRegistry: registry === null ? null : registry.has(last.tag),
|
|
148
|
+
unledgered: false,
|
|
149
|
+
}));
|
|
150
|
+
if (registry) for (const tag of [...registry].sort()) {
|
|
151
|
+
if (byTag.has(tag)) continue;
|
|
152
|
+
rows.push({ tag, commit: tag, role: 'transient', keep: 'short', origin: null, firstTs: '', lastTs: '', current: false, inRegistry: true, unledgered: true });
|
|
153
|
+
}
|
|
154
|
+
return rows.sort((a, b) => (b.lastTs || '').localeCompare(a.lastTs || '') || a.tag.localeCompare(b.tag));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type RegistryRead = { readonly ok: true; readonly tags: readonly string[] } | { readonly ok: false; readonly reason: string };
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Read `elanous-harness` tags from the registry the way the rail does (`/v2/elanous-harness/tags/list`
|
|
161
|
+
* on the registry node's localhost port). `sshHost` runs it on that node; otherwise on this machine.
|
|
162
|
+
*/
|
|
163
|
+
export function readRegistryTags(opts: { port: string; sshHost?: string; run?: (cmd: string, args: readonly string[]) => { status: number | null; stdout: string; stderr?: string } }): RegistryRead {
|
|
164
|
+
const url = `http://localhost:${opts.port}/v2/elanous-harness/tags/list`;
|
|
165
|
+
const run = opts.run ?? ((cmd, args) => {
|
|
166
|
+
const r = spawnSync(cmd, [...args], { encoding: 'utf8', timeout: 25_000 });
|
|
167
|
+
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? (r.error ? String(r.error) : '') };
|
|
168
|
+
});
|
|
169
|
+
const r = opts.sshHost
|
|
170
|
+
? run('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', opts.sshHost, `curl -fsS --max-time 15 ${url}`])
|
|
171
|
+
: run('curl', ['-fsS', '--max-time', '15', url]);
|
|
172
|
+
if (r.status !== 0) return { ok: false, reason: `registry read failed (exit ${r.status ?? 'timeout'})${r.stderr ? `: ${r.stderr.trim().slice(0, 200)}` : ''}` };
|
|
173
|
+
try {
|
|
174
|
+
const tags = (JSON.parse(r.stdout) as { tags?: unknown }).tags;
|
|
175
|
+
if (tags === null) return { ok: true, tags: [] };
|
|
176
|
+
if (!Array.isArray(tags) || !tags.every((t) => typeof t === 'string')) return { ok: false, reason: 'registry answered without a tags list' };
|
|
177
|
+
return { ok: true, tags };
|
|
178
|
+
} catch { return { ok: false, reason: 'registry answered non-JSON' }; }
|
|
179
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { SelfDevRunParticipant, SelfDevRunState } from './self-dev/run-stor
|
|
|
7
7
|
import { applyConfigDirFlagFromArgv } from './cli/config-dir-flag.js';
|
|
8
8
|
import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
|
|
9
9
|
import { registerDocsCommands } from './cli/docs-cli.js';
|
|
10
|
+
import { registerBenchCommands } from './cli/bench-cli.js';
|
|
10
11
|
import { registerRetroCommands } from './cli/retro-cli.js';
|
|
11
12
|
import { registerA2ACommands } from './cli/a2a-cli.js';
|
|
12
13
|
import { registerPluginCommands } from './cli/plugin-cli.js';
|
|
@@ -51,6 +52,7 @@ import { registerPublishCommands } from './cli/publish-cli.js';
|
|
|
51
52
|
import { registerAutopilotCommands } from './cli/autopilot-cli.js';
|
|
52
53
|
import { registerRoleCommands } from './cli/role-cli.js';
|
|
53
54
|
import { registerMachineCommands } from './cli/machine-cli.js';
|
|
55
|
+
import { recordImagePinFromConfigSet, registerImageCommands } from './cli/image-cli.js';
|
|
54
56
|
import { registerAgentEnvCommands } from './cli/agent-env-cli.js';
|
|
55
57
|
import { registerScheduleCommands } from './cli/schedule-cli.js';
|
|
56
58
|
import { registerGuardianCommands } from './cli/guardian-cli.js';
|
|
@@ -899,9 +901,11 @@ registerPrCommands(program);
|
|
|
899
901
|
registerRepoCommands(program);
|
|
900
902
|
registerRoleCommands(program);
|
|
901
903
|
registerMachineCommands(program);
|
|
904
|
+
registerImageCommands(program);
|
|
902
905
|
registerAgentEnvCommands(program);
|
|
903
906
|
// CLI entry: where action → resolveCurrentInstance → resolveInstance (instance universe).
|
|
904
907
|
registerWhereCommand(program);
|
|
908
|
+
registerBenchCommands(program);
|
|
905
909
|
registerShadowCommand(program);
|
|
906
910
|
registerLlmDetectCommand(program);
|
|
907
911
|
registerStorageDetectCommand(program);
|
|
@@ -4713,7 +4717,7 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4713
4717
|
.option('--no-pod-rebuild', 'pod: 이미지 판(elanous.commit)이 HEAD 와 달라도 다시 굽지 않는다 — 측정은 «이미지 판»을 잰다')
|
|
4714
4718
|
.option('--pod-pass-env <keys>', 'pod: 호스트 env 에서 Pod 로 넘길 키(쉼표) — 예 OPENROUTER_API_KEY,ANTHROPIC_API_KEY(벤치마크 과금 경로)')
|
|
4715
4719
|
.option('--bench-arms <spec>', 'pod 벤치마크: 골 1개를 팔마다 «라벨 한 줄만 다르게» 복제해 동시에 — "id=provider[:model][@KEY+KEY];…" (예 codex=openai-codex;or-kimi=openrouter:openrouter/moonshotai/kimi-k3@OPENROUTER_API_KEY) · --auto-merge 거부 · RFC fleet 슈퍼바이저 §A3')
|
|
4716
|
-
.addOption(new Option('--child-llm-provider <id>', 'pod: 구현 자식 LLM provider(openai-codex|grok|openrouter|anthropic) — 하니스 --child-llm-provider 가 이 자리로 온다(10-05)').hideHelp())
|
|
4720
|
+
.addOption(new Option('--child-llm-provider <id>', 'pod: 구현 자식 LLM provider(openai-codex|grok|openrouter|anthropic|bedrock|glm) — 하니스 --child-llm-provider 가 이 자리로 온다(10-05)').hideHelp())
|
|
4717
4721
|
.addOption(new Option('--child-llm-model <id>', 'pod: 구현 자식 LLM model(--child-llm-provider 와 함께)').hideHelp())
|
|
4718
4722
|
.addOption(new Option('--child-llm-effort <level>', 'pod: 구현 자식 추론 노력(--child-llm-provider 와 함께)').hideHelp())
|
|
4719
4723
|
.option('--help-all', '모든 orchestrate 옵션 표시')
|
|
@@ -5000,6 +5004,33 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5000
5004
|
let rotationAccounts: readonly string[] | undefined;
|
|
5001
5005
|
let podProvider: import('./task-orchestrator/surfaces/pod-child-providers.js').PodChildProvider = 'openai-codex';
|
|
5002
5006
|
let grokApiKeyOptIn = false;
|
|
5007
|
+
{
|
|
5008
|
+
// SUBSCRIPTION-EXHAUST-REROUTE (0.2.24): a Pod launch with no child model gets the configured default
|
|
5009
|
+
// (tools.selfImplement.podChildDefault · default openrouter/z-ai/glm-5.3) instead of falling to codex.
|
|
5010
|
+
// It fills the same flags a named launch carries, so every check below runs unchanged.
|
|
5011
|
+
const child = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
|
|
5012
|
+
const { resolvePodChildDefault } = await import('./task-orchestrator/surfaces/pod-child-default.js');
|
|
5013
|
+
const { homedir: defaultKeyHome } = await import('node:os');
|
|
5014
|
+
const childDefault = resolvePodChildDefault({
|
|
5015
|
+
flags: { provider: child.childLlmProvider, model: child.childLlmModel, effort: child.childLlmEffort },
|
|
5016
|
+
benchArms: Boolean(benchArms),
|
|
5017
|
+
explicitPodAccount: (opts as { podAccount?: string }).podAccount,
|
|
5018
|
+
config: (launchPolicy.tools?.selfImplement as { podChildDefault?: unknown } | undefined)?.podChildDefault,
|
|
5019
|
+
openrouterKeyPresent: () => {
|
|
5020
|
+
if (process.env.OPENROUTER_API_KEY?.trim()) return true;
|
|
5021
|
+
try { return Boolean(readFileSync(_joinPath(defaultKeyHome(), '.cache', 'openrouter_api_key'), 'utf8').trim()); } catch { return false; }
|
|
5022
|
+
},
|
|
5023
|
+
});
|
|
5024
|
+
if (childDefault.apply) {
|
|
5025
|
+
child.childLlmProvider = childDefault.provider;
|
|
5026
|
+
if (childDefault.model) child.childLlmModel = childDefault.model;
|
|
5027
|
+
debug.log('self-implement.pod', 'child-default-applied', { provider: childDefault.provider, model: childDefault.model ?? null, source: childDefault.source, ...(childDefault.invalidConfig !== undefined ? { invalidConfig: String(JSON.stringify(childDefault.invalidConfig)) } : {}) });
|
|
5028
|
+
if (!opts.json) ui.info(`[pod] child 기본 모델 = ${childDefault.provider}${childDefault.model ? `/${childDefault.model}` : ''} (${childDefault.source === 'config' ? 'tools.selfImplement.podChildDefault' : '기본값'}) — --child-llm-* 로 바꾼다`);
|
|
5029
|
+
} else if (childDefault.reason !== 'flag') {
|
|
5030
|
+
debug.log('self-implement.pod', 'child-default-skipped', { reason: childDefault.reason, provider: childDefault.provider ?? null, model: childDefault.model ?? null, ...(childDefault.invalidConfig !== undefined ? { invalidConfig: String(JSON.stringify(childDefault.invalidConfig)) } : {}) }, { level: childDefault.reason === 'no-openrouter-key' ? 'warn' : 'debug' });
|
|
5031
|
+
if (childDefault.reason === 'no-openrouter-key' && !opts.json) ui.warn('[pod] child 기본 모델(openrouter)을 못 쓴다 — 호스트에 OpenRouter 키 없음 · 종전 codex 경로로 간다');
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5003
5034
|
{
|
|
5004
5035
|
// 10-05 PODPROVIDER: refuse child choices this Pod launch cannot honour instead of dropping them.
|
|
5005
5036
|
const child = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
|
|
@@ -5008,7 +5039,7 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5008
5039
|
? 'pod: --child-llm-model/--child-llm-effort 는 --child-llm-provider 와 함께'
|
|
5009
5040
|
: named && benchArms
|
|
5010
5041
|
? 'pod: --child-llm-provider 는 --bench-arms 와 함께 못 쓴다(팔마다 모델이 하나다)'
|
|
5011
|
-
: (named === 'grok' || named === 'openrouter' || named === 'anthropic') && explicitPodAccount
|
|
5042
|
+
: (named === 'grok' || named === 'openrouter' || named === 'anthropic' || named === 'bedrock' || named === 'glm') && explicitPodAccount
|
|
5012
5043
|
? `pod: --child-llm-provider ${named} 은 --pod-account 와 함께 못 쓴다(그것은 codex 계정을 고정한다)`
|
|
5013
5044
|
: undefined;
|
|
5014
5045
|
if (refuse) { ui.error(refuse); debug.log('self-implement.pod', 'child-provider-refused', { provider: named ?? null, reason: refuse }); process.exit(2); }
|
|
@@ -5026,6 +5057,12 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5026
5057
|
const { normalizeFallbackChain } = await import('./oauth/fallback-chain.js');
|
|
5027
5058
|
const fallbackEnabled = codexPolicyAllowsFallback(quotaPolicy)
|
|
5028
5059
|
&& normalizeFallbackChain(launchPolicy.llm?.fallbackChain).chain.includes('grok');
|
|
5060
|
+
// CODEX-CREDITS-STEP — 체인 마지막 칸(크레딧). 켜져 있을 때만 grok 잔량을 잰다(발사 관문과 같은 80% 상한).
|
|
5061
|
+
// Pod 는 «grok 뒤» 순서(문서화된 배치)만 따른다 — grok 앞에 둔 체인은 Pod 계획에서 크레딧 칸을 쓰지 않는다.
|
|
5062
|
+
const launchChain = normalizeFallbackChain(launchPolicy.llm?.fallbackChain).chain;
|
|
5063
|
+
const codexCreditsStep = codexPolicyAllowsFallback(quotaPolicy)
|
|
5064
|
+
&& launchChain.includes('codex-credits')
|
|
5065
|
+
&& (!launchChain.includes('grok') || launchChain.indexOf('codex-credits') > launchChain.indexOf('grok'));
|
|
5029
5066
|
// POL1 — say which policy this launch uses and where it came from; warn when a test universe disagrees with production.
|
|
5030
5067
|
const { effectiveInstanceRoot, prodInstanceRoot } = await import('./instance/resolve.js');
|
|
5031
5068
|
const root = effectiveInstanceRoot();
|
|
@@ -5074,7 +5111,7 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5074
5111
|
if (namedChildDecision.refuse) { ui.error(namedChildDecision.refuse); debug.log('self-implement.pod', 'child-provider-refused', { provider: namedChild, reason: namedChildDecision.refuse }); process.exit(2); }
|
|
5075
5112
|
// A named codex child must stay codex — the plan may not fall back to grok (post-review must-fix).
|
|
5076
5113
|
const namedCodex = namedChildDecision.provider === 'openai-codex';
|
|
5077
|
-
const plan: import('./task-orchestrator/surfaces/pod-account-broker.js').PodProviderPlan | null = namedChildDecision.provider === 'openrouter' || namedChildDecision.provider === 'anthropic' ? null : namedChildDecision.provider === 'grok'
|
|
5114
|
+
const plan: import('./task-orchestrator/surfaces/pod-account-broker.js').PodProviderPlan | null = namedChildDecision.provider === 'openrouter' || namedChildDecision.provider === 'anthropic' || namedChildDecision.provider === 'bedrock' || namedChildDecision.provider === 'glm' ? null : namedChildDecision.provider === 'grok'
|
|
5078
5115
|
? { provider: 'grok', excluded: [], grokSubscriptionEligible: credential?.kind === 'subscription' }
|
|
5079
5116
|
: planPodProvider({
|
|
5080
5117
|
codexCandidates: inspectCodexRotation().candidates,
|
|
@@ -5082,18 +5119,28 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5082
5119
|
thresholdPercentByAccount: launchPolicy.llm?.codexAccountRotationThresholdPercentByAccount,
|
|
5083
5120
|
// 대표 한도 정책 한 값(`llm.codexQuotaPolicy`) — 크레딧·폴백을 같이 정한다.
|
|
5084
5121
|
creditsAllowed: codexPolicyAllowsCredits(quotaPolicy),
|
|
5122
|
+
// SUBSCRIPTION-EXHAUST-REROUTE — 예비분 이하 잔액(«알려진» 값)의 계정은 크레딧으로 쓰지 않는다.
|
|
5123
|
+
creditReserve: typeof launchPolicy.llm?.codexCreditReserve === 'number' && Number.isFinite(launchPolicy.llm.codexCreditReserve) && launchPolicy.llm.codexCreditReserve >= 0
|
|
5124
|
+
? launchPolicy.llm.codexCreditReserve : (await import('./oauth/codex-account-store.js')).DEFAULT_CODEX_CREDIT_RESERVE,
|
|
5085
5125
|
...podFallbackCredentials(namedChildDecision.provider, {
|
|
5086
5126
|
grokSubscription: fallbackEnabled && grokSubscriptionUsable,
|
|
5087
5127
|
grokApiKey: fallbackEnabled && credential?.kind === 'api_key',
|
|
5088
5128
|
}),
|
|
5089
5129
|
grokApiKeyOptIn,
|
|
5130
|
+
...(codexCreditsStep ? { codexCreditsStep: true, grokQuota: await (async () => {
|
|
5131
|
+
const { readLiveGrokUsedPercent } = await import('./self-implement/budget-gate.js');
|
|
5132
|
+
const { LAUNCH_GROK_USED_PERCENT_CAP } = await import('./user-config.js');
|
|
5133
|
+
const used = await readLiveGrokUsedPercent();
|
|
5134
|
+
return used === undefined ? 'unknown' as const : used >= LAUNCH_GROK_USED_PERCENT_CAP ? 'exhausted' as const : 'usable' as const;
|
|
5135
|
+
})() } : {}),
|
|
5090
5136
|
});
|
|
5091
5137
|
if (plan?.provider === null) { ui.error(`pod: 쓸 codex 계정이 없다 — ${plan.reasons.join(' · ')}${namedCodex ? ' · child provider 를 openai-codex 로 지정해 grok 으로 넘기지 않는다' : ''} · 계정을 명시하려면 --pod-account <이름>`); process.exit(2); }
|
|
5092
|
-
podProvider = namedChildDecision.provider === 'openrouter' || namedChildDecision.provider === 'anthropic' ? namedChildDecision.provider : plan!.provider!;
|
|
5138
|
+
podProvider = namedChildDecision.provider === 'openrouter' || namedChildDecision.provider === 'anthropic' || namedChildDecision.provider === 'bedrock' || namedChildDecision.provider === 'glm' ? namedChildDecision.provider : plan!.provider!;
|
|
5093
5139
|
if (plan?.provider === 'openai-codex') {
|
|
5094
5140
|
accountBroker = makePodAccountBroker({ usable: plan.accounts, excluded: plan.excluded });
|
|
5095
5141
|
rotationAccounts = plan.accounts;
|
|
5096
5142
|
debug.log('self-implement.pod', 'account-plan', { usable: plan.accounts, excluded: plan.excluded });
|
|
5143
|
+
if (plan.codexCreditsStep) debug.log('self-implement.pod', 'codex-credits-step', { outcome: 'credits', accounts: plan.accounts, skipped: plan.skipped ?? [] }, { level: 'warn' });
|
|
5097
5144
|
{ const { emitDecision } = await import('./live/detail-switch.js');
|
|
5098
5145
|
emitDecision({ kind: 'ROUTE', what: `Pod 계정 배분 ${plan.accounts.join(' → ')}`, reason: plan.excluded.length ? `뺌 ${plan.excluded.map((x) => `${x.name}(${x.why})`).join(', ')}` : '모든 계정 여유', purpose: plan.creditAccounts ? '구독 잔량 0 — 정책상 크레딧으로 병렬 조각을 돌린다' : '병렬 조각이 한 계정에 몰리지 않게 잔량 순으로 돌려 준다', phase: 'dispatch', target: `Pod ${plan.accounts.length}계정`, paths: plan.accounts.length + plan.excluded.length }); }
|
|
5099
5146
|
if (!opts.json) ui.info(`[pod] 계정 배분(잔량 순 · 돌려 가며): ${plan.accounts.join(' → ')}${plan.creditAccounts ? ' · 💳 크레딧(구독 잔량 0 · 허가됨)' : ''}${plan.excluded.length ? ` · 뺌 ${plan.excluded.map((x) => `${x.name}(${x.why})`).join(', ')}` : ''}`);
|
|
@@ -5106,6 +5153,17 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5106
5153
|
// ⓔ 과금 관측 — anthropic 은 API 키 과금(구독 아님). 새 예산 체계는 없다 · 관측 한 줄만.
|
|
5107
5154
|
debug.log('self-implement.pod', 'provider-named', { provider: 'anthropic', model, anthropicKeyPresent: true, billing: 'api-key' });
|
|
5108
5155
|
if (!opts.json) ui.info(`[pod] child provider = anthropic(모델 ${model ?? '기본'}) — 발사 인자로 지정됨 · 💳 API 키 과금(구독 아님)`);
|
|
5156
|
+
} else if (namedChildDecision.provider === 'bedrock') {
|
|
5157
|
+
const model = (opts as { childLlmModel?: string }).childLlmModel?.trim() || null;
|
|
5158
|
+
// BEDROCK-PROVIDER ② — 자격은 클러스터의 이름 있는 AWS Secret(발사 전 판정 · 없으면 apply 0 으로 거부). 과금 = AWS 계정.
|
|
5159
|
+
debug.log('self-implement.pod', 'provider-named', { provider: 'bedrock', model, billing: 'aws-account' });
|
|
5160
|
+
if (!opts.json) ui.info(`[pod] child provider = bedrock(모델 ${model ?? '기본'}) — 발사 인자로 지정됨 · 💳 AWS 계정 과금(구독 아님)`);
|
|
5161
|
+
} else if (namedChildDecision.provider === 'glm') {
|
|
5162
|
+
const model = (opts as { childLlmModel?: string }).childLlmModel?.trim() || null;
|
|
5163
|
+
// ZAI-POD-CHILD — 자격은 클러스터의 이름 있는 ZAI Secret(발사 전 판정 · 없으면 apply 0 으로 거부) · 과금 = GLM Coding Plan 정액.
|
|
5164
|
+
// 동시 상한(llm.zaiCodingPlan.maxConcurrent)을 넘는 Pod 는 정책(overage)대로 — openrouter·balance 면 OpenRouter GLM · stop 이면 거부.
|
|
5165
|
+
debug.log('self-implement.pod', 'provider-named', { provider: 'glm', model, billing: 'zai-coding-plan' });
|
|
5166
|
+
if (!opts.json) ui.info(`[pod] child provider = glm(모델 ${model ?? '기본'}) — 발사 인자로 지정됨 · GLM Coding Plan(정액) · 동시 상한을 넘는 Pod 는 llm.zaiCodingPlan.overage 대로`);
|
|
5109
5167
|
} else if (namedChildDecision.provider === 'grok') {
|
|
5110
5168
|
const model = (opts as { childLlmModel?: string }).childLlmModel?.trim() || defaultGrokModel().id;
|
|
5111
5169
|
debug.log('self-implement.pod', 'provider-named', { provider: 'grok', model, credential: credential?.kind ?? null });
|
|
@@ -5140,7 +5198,9 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5140
5198
|
const childFlags = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
|
|
5141
5199
|
const podChildLlm = childFlags.childLlmProvider?.trim() ? { childProviderExplicit: true, ...(childFlags.childLlmModel?.trim() ? { childModel: childFlags.childLlmModel.trim() } : {}), ...(childFlags.childLlmEffort?.trim() ? { childEffort: childFlags.childLlmEffort.trim() } : {}) } : {};
|
|
5142
5200
|
if (childFlags.childLlmProvider?.trim()) debug.log('self-implement.pod', 'child-provider-named', { provider: childFlags.childLlmProvider.trim(), model: childFlags.childLlmModel ?? null, podProvider });
|
|
5143
|
-
|
|
5201
|
+
// BEDROCK-POD-PROVIDER-DROP — provider·account 는 한 곳(podLaunchProviderOptions)에서 — 손 나열이 bedrock 을 떨궈 codex(team)로 돌았다(10-11).
|
|
5202
|
+
const podProviderOptions = (await import('./task-orchestrator/surfaces/pod-child-providers.js')).podLaunchProviderOptions(podProvider, { explicitPodAccount, grokApiKeyOptIn });
|
|
5203
|
+
const podBase = { hostSupervised: opts.supervise === true, ...podChildLlm, ...(remoteOnlyPool ? { imageCommit: image.imageCommit } : {}), ...podProviderOptions, ...(accountBroker ? { accountBroker, rotationAccounts } : {}), passEnv, ...(pool ? { pool } : {}), ...((opts as { podSkillEnv?: boolean }).podSkillEnv ? { skillEnv: true } : {}), ...(podSource ? { source: podSource } : {}) };
|
|
5144
5204
|
if (benchArms) {
|
|
5145
5205
|
const { benchPodSpawn } = await import('./task-orchestrator/surfaces/self-implement-pod.js');
|
|
5146
5206
|
podSpawn = benchPodSpawn(benchArms, podBase);
|
|
@@ -5151,7 +5211,7 @@ const selfOrchestrateCmd = selfCmd
|
|
|
5151
5211
|
? poolMembers.map(({ context }) => ({ context, namespace: 'elanous-test' }))
|
|
5152
5212
|
: [{ context: defaultKubectl(['config', 'current-context']).stdout.trim(), namespace: 'elanous-test' }];
|
|
5153
5213
|
// ⭐ 팔 선언은 «runId 가 붙는» 이 줄에 싣는다 — 위의 `bench-arms` 줄은 runId 해석 «전»이라 비어 있다(09-25 실측) · 보고서(scripts/bench-report.ts)가 이 줄로 잇는다.
|
|
5154
|
-
debug.log('self-dev.orchestrate', 'substrate', { substrate, account:
|
|
5214
|
+
debug.log('self-dev.orchestrate', 'substrate', { substrate, account: podProviderOptions.account, passEnv, context: ready.reason, imageCommit: image.imageCommit, imageFresh: image.fresh, ...(benchArms ? { benchArms: benchArms.map((a) => ({ id: a.id, provider: a.provider, model: a.model ?? null, modelSource: a.modelSource ?? null, passEnv: a.passEnv })) } : {}) });
|
|
5155
5215
|
} else if (substrate !== 'local') {
|
|
5156
5216
|
ui.error(`--substrate: local | pod (받은 값: ${substrate})`); process.exit(2);
|
|
5157
5217
|
}
|
|
@@ -5763,19 +5823,21 @@ selfCmd
|
|
|
5763
5823
|
const { spawnSync } = await import('node:child_process');
|
|
5764
5824
|
const { reviewPullRequest, renderReview } = await import('./agent-substrate/pr-reviewer.js');
|
|
5765
5825
|
const { createRepositoryReferencedFileReader } = await import('./self-implement/goal-file-reader.js');
|
|
5766
|
-
const { streamLLM } = await import('./llm.js');
|
|
5826
|
+
const { streamLLM, getProvider } = await import('./llm.js');
|
|
5767
5827
|
const { registerStandaloneLogSink } = await import('./domains/standalone-log-sink.js');
|
|
5768
5828
|
const { debug } = await import('./debug/log.js');
|
|
5769
5829
|
const { runSelfReviewCliCommand, reviewerContextArgs, selfReviewJsonLine } = await import('./agent-substrate/self-review-cli.js');
|
|
5770
5830
|
const { subscriptionReviewerAvailability } = await import('./self-dev/review-provider-fallback.js');
|
|
5771
5831
|
const { makeLazyAcpReviewLLM: makeRoleAcpReviewLLM } = await import('./agent-substrate/acp-reviewer.js');
|
|
5832
|
+
const { getOpenRouterApiKey: getOpenRouterApiKeyForReview } = await import('./config.js');
|
|
5833
|
+
const { ROLE_REVIEWER_FALLBACK_MODEL } = await import('./agent-substrate/self-review-cli.js');
|
|
5772
5834
|
const contextOrder = reviewerContextArgs(process.argv.slice(2));
|
|
5773
5835
|
const configuredAcpBackend = getUserConfig().acp.reviewBackend;
|
|
5774
5836
|
// ⭐ Same reviewer row the harness reads (`dev-pipeline` → `resolveSubscriptionReviewer`).
|
|
5775
5837
|
// The seam decides whether it applies (only without an explicit reviewer flag).
|
|
5776
5838
|
const { loadLlmPolicy, resolveSubscriptionReviewer } = await import('./policy/llm-policy.js');
|
|
5777
5839
|
const roleReviewerSelection = resolveSubscriptionReviewer(loadLlmPolicy(), getUserConfig().roleLlm);
|
|
5778
|
-
const { results } = await runSelfReviewCliCommand(prArgs, {
|
|
5840
|
+
const { results, failed } = await runSelfReviewCliCommand(prArgs, {
|
|
5779
5841
|
...opts,
|
|
5780
5842
|
...(configuredAcpBackend ? { configuredAcpBackend } : {}),
|
|
5781
5843
|
...(roleReviewerSelection ? { roleReviewer: roleReviewerSelection.spec } : {}),
|
|
@@ -5797,10 +5859,16 @@ selfCmd
|
|
|
5797
5859
|
renderReview,
|
|
5798
5860
|
// ⛔⭐ 하드코딩 금지(🩸 2026-09-23 실측: 여기 'medium' 이 박혀 있었다) — 무인 리뷰와 «같은» 규칙.
|
|
5799
5861
|
// 대표 결정: 리뷰는 high. 역할 티어의 effort 를 쓰고, 티어가 없으면 high.
|
|
5800
|
-
|
|
5862
|
+
// 명시 `--model` 이면 provider 를 그 모델의 것으로 «고정»한다 — 라우터 폴백(grok 등)으로 조용히 넘어가지 않고 오류로 낸다.
|
|
5863
|
+
makeApiLlm: (model, o) => {
|
|
5801
5864
|
const effort = reviewReasoningEffort(resolveRoleLlm('review'));
|
|
5802
5865
|
return (prompt: string) =>
|
|
5803
|
-
streamLLM([{ role: 'user', content: prompt }], () => {}, {
|
|
5866
|
+
streamLLM([{ role: 'user', content: prompt }], () => {}, {
|
|
5867
|
+
model,
|
|
5868
|
+
...(o?.pinProvider ? { provider: getProvider(model) } : {}),
|
|
5869
|
+
...(o?.onResolved ? { onResolvedProvider: o.onResolved } : {}),
|
|
5870
|
+
...(effort ? { reasoningEffort: effort } : {}),
|
|
5871
|
+
});
|
|
5804
5872
|
},
|
|
5805
5873
|
makeAcpLlm: (o) => {
|
|
5806
5874
|
// ⚠️ 동기 팩토리라 여기서 동적 import 를 쓸 수 없다 — 지연 로드해 첫 호출에 붙인다.
|
|
@@ -5829,6 +5897,9 @@ selfCmd
|
|
|
5829
5897
|
llm: makeRoleAcpReviewLLM({ cwd: process.cwd(), backend: 'claude', backendSpec: spawn.backendSpec, env: spawn.env, timeoutMs: o.timeoutMs }),
|
|
5830
5898
|
};
|
|
5831
5899
|
},
|
|
5900
|
+
// SUBSCRIPTION-EXHAUST-REROUTE — role reviewer unavailable ⇒ GLM on OpenRouter (the review policy's first
|
|
5901
|
+
// fallback · same pin as POD_OPENROUTER_REVIEWER_PINS.glm), not the codex ladder. No key ⇒ today's default.
|
|
5902
|
+
roleFallbackModel: () => (getOpenRouterApiKeyForReview()?.trim() ? ROLE_REVIEWER_FALLBACK_MODEL : undefined),
|
|
5832
5903
|
registerSink: async (surface) => { await registerStandaloneLogSink(surface); },
|
|
5833
5904
|
log: (event, data, o) => debug.log('self-review', event, data, o ?? {}),
|
|
5834
5905
|
info: (t) => ui.info(t),
|
|
@@ -5838,6 +5909,7 @@ selfCmd
|
|
|
5838
5909
|
envModel: () => process.env.ELANOUS_PR_REVIEW_MODEL,
|
|
5839
5910
|
});
|
|
5840
5911
|
if (opts.json) await writeStdoutJson(selfReviewJsonLine(results));
|
|
5912
|
+
if (failed >= 1) process.exitCode = 1;
|
|
5841
5913
|
});
|
|
5842
5914
|
|
|
5843
5915
|
selfCmd
|
|
@@ -7868,6 +7940,8 @@ configCmd
|
|
|
7868
7940
|
} else if (credential?.source === 'none' && !isKeylessProvider(nextProvider)) {
|
|
7869
7941
|
ui.warn(`provider credential unavailable: ${parsed}; keeping existing llm.apiKey (subscription or OAuth may be normal)`);
|
|
7870
7942
|
}
|
|
7943
|
+
// IMAGE-LEDGER-TAGS: the baseline pin (the rail sets it through this command) goes into the image ledger.
|
|
7944
|
+
if (recordImagePinFromConfigSet(path, parsed) === 'recorded') ui.info(`image ledger: baseline ${String(parsed).slice(0, 12)} · keep=pin`);
|
|
7871
7945
|
ui.info(`set ${path} = ${JSON.stringify(parsed)}`);
|
|
7872
7946
|
});
|
|
7873
7947
|
|
|
@@ -11768,7 +11842,7 @@ async function main(): Promise<void> {
|
|
|
11768
11842
|
if (needsFirstRun(cfg)) {
|
|
11769
11843
|
if (cfg.onboarding.webFirst !== false) {
|
|
11770
11844
|
const { runWebFirstSetup } = await import('./onboarding/web-first.js');
|
|
11771
|
-
if (await runWebFirstSetup({ config: cfg, startedAt: firstRunStartedAt }) === 'link-shown') return;
|
|
11845
|
+
if (await runWebFirstSetup({ config: cfg, startedAt: firstRunStartedAt, startIfMissing: false }) === 'link-shown') return;
|
|
11772
11846
|
}
|
|
11773
11847
|
const { runFirstRun } = await import('./onboarding/first-run.js');
|
|
11774
11848
|
if ((await runFirstRun({ config: cfg })).outcome !== 'ready') return;
|
|
@@ -11778,6 +11852,14 @@ async function main(): Promise<void> {
|
|
|
11778
11852
|
// answer their first prompt. Printed above the dashboard altscreen
|
|
11779
11853
|
// so it stays visible on quit (scrolls back into the terminal).
|
|
11780
11854
|
console.log(`[elanous] ${oneLineProvider()}`);
|
|
11855
|
+
const noticeConfig = getUserConfig();
|
|
11856
|
+
// Only a run that will draw the dashboard (stdin TTY) counts as a person having seen the notice;
|
|
11857
|
+
// a piped/CI run that stops at the TTY gate must not mark it shown.
|
|
11858
|
+
const noticeSeen = (await import('./dashboard/tty-required.js')).dashboardCanUseTty();
|
|
11859
|
+
if (noticeSeen && noticeConfig.errorReports?.enabled !== false && noticeConfig.errorReports?.noticeShown !== true) {
|
|
11860
|
+
console.log((await import('./error-report/report.js')).errorReportNoticeLine(noticeConfig.errorReports));
|
|
11861
|
+
saveUserConfig({ ...noticeConfig, errorReports: { ...noticeConfig.errorReports, noticeShown: true } }, userConfigPath());
|
|
11862
|
+
}
|
|
11781
11863
|
if (remote) {
|
|
11782
11864
|
const authNote = remote.token ? 'token' : 'no-auth';
|
|
11783
11865
|
console.log(`[elanous] remote daemon: ${remote.url} (${authNote})`);
|
|
@@ -2,7 +2,7 @@ import { debug } from '../debug/log.js';
|
|
|
2
2
|
import { getUserConfig, type SelfImplementChildInstanceMode } from '../user-config.js';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
-
import { effectiveInstanceRoot, prodInstanceRoot, treeDerivedRootFor } from './resolve.js';
|
|
5
|
+
import { effectiveInstanceRoot, prodInstanceRoot, measureUniverse, treeDerivedRootFor, universeMismatch, type UniverseMeasure } from './resolve.js';
|
|
6
6
|
|
|
7
7
|
interface ChildInstanceScope {
|
|
8
8
|
configDir?: string;
|
|
@@ -69,6 +69,33 @@ function instanceScopeLog(event: string, data: Record<string, unknown>, warn = f
|
|
|
69
69
|
debug.log('instance.identity', event, data, warn ? { level: 'warn' } : undefined);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* 부모가 도장(`ELANOUS_STATE_DIR`)으로 물려준 우주와 이 프로세스가 실제로 쓰는 우주를 대조한다.
|
|
74
|
+
* 다르면 `instance.identity universe-mismatch` 경고 한 줄을 남긴다 — 막지 않고 적기만 한다.
|
|
75
|
+
* 도장이 없으면(사람이 직접 띄운 입구) 비교할 부모가 없으므로 조용하다.
|
|
76
|
+
*/
|
|
77
|
+
export function observeUniverseMismatch(
|
|
78
|
+
deps: { stamp?: string | undefined; effectiveRoot?: () => string; log?: ChildInstanceScopeDeps['log'] } = {},
|
|
79
|
+
): { parentRoot: string; childRoot: string } | null {
|
|
80
|
+
const stamp = 'stamp' in deps ? deps.stamp : process.env.ELANOUS_STATE_DIR;
|
|
81
|
+
const mismatch = universeMismatch(stamp, (deps.effectiveRoot ?? effectiveInstanceRoot)());
|
|
82
|
+
if (mismatch) (deps.log ?? instanceScopeLog)('universe-mismatch', { ...mismatch, why: 'child resolved a different universe than the one its parent stamped' }, true);
|
|
83
|
+
return mismatch;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 읽을 우주가 비어 있으면 «0» 이 아니라 «측정 불가(빈 우주)» 라고 `instance.identity universe-unmeasurable` 로 적는다.
|
|
88
|
+
* 새 test 우주는 늘 비어 있으므로 경고(warn)가 아니라 정보 한 줄이다 — 막지 않고 적기만 한다.
|
|
89
|
+
*/
|
|
90
|
+
export function observeEmptyUniverse(
|
|
91
|
+
root: string,
|
|
92
|
+
deps: { entries?: (dir: string) => string[]; log?: ChildInstanceScopeDeps['log'] } = {},
|
|
93
|
+
): UniverseMeasure {
|
|
94
|
+
const measure = measureUniverse(root, deps.entries);
|
|
95
|
+
if (!measure.measurable) (deps.log ?? instanceScopeLog)('universe-unmeasurable', { root, reason: measure.reason, why: '읽을 우주가 비어 있어 0 이 아니라 측정 불가로 취급한다' });
|
|
96
|
+
return measure;
|
|
97
|
+
}
|
|
98
|
+
|
|
72
99
|
/** Resolve the current universe once; operational parents derive an isolated child universe by default. */
|
|
73
100
|
export function childInstanceScope(deps: ChildInstanceScopeDeps = {}): ChildInstanceScope {
|
|
74
101
|
const elanousBinRoot = resolve(import.meta.dir, '../..');
|
|
@@ -77,6 +104,10 @@ export function childInstanceScope(deps: ChildInstanceScopeDeps = {}): ChildInst
|
|
|
77
104
|
const parentIsProduction = root === prodRoot;
|
|
78
105
|
const mode = (deps.childInstanceMode ?? configuredChildInstanceMode)();
|
|
79
106
|
const log = deps.log ?? instanceScopeLog;
|
|
107
|
+
if (!deps.effectiveRoot) {
|
|
108
|
+
observeUniverseMismatch({ effectiveRoot: () => root, log });
|
|
109
|
+
observeEmptyUniverse(root, { log });
|
|
110
|
+
}
|
|
80
111
|
|
|
81
112
|
const readProvider = deps.rawProviderAt ?? rawLlmProviderAt;
|
|
82
113
|
/** 자식이 갈 우주의 provider 가 운영과 어긋나는지 «한 칸»으로 남긴다. 판정하지 않고 적기만 한다. */
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
6
|
+
|
|
7
|
+
export interface DevProjectDeps {
|
|
8
|
+
git?: (dir: string, args: readonly string[]) => string | null;
|
|
9
|
+
stateDir?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DevProjectRegistry {
|
|
13
|
+
projects: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DevProjectCache {
|
|
17
|
+
identityKey: string;
|
|
18
|
+
devProject: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ULID_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
22
|
+
|
|
23
|
+
export function resolveDevProject(dir: string, deps: DevProjectDeps = {}): string {
|
|
24
|
+
const suppliedDir = resolve(dir);
|
|
25
|
+
const git = deps.git ?? runGit;
|
|
26
|
+
const gitRoot = git(suppliedDir, ['rev-parse', '--show-toplevel'])?.trim();
|
|
27
|
+
const root = gitRoot ? resolve(gitRoot) : suppliedDir;
|
|
28
|
+
let identityKey: string;
|
|
29
|
+
// A repo with no commit yet (`git init` only) has no first commit to anchor on — it falls back to
|
|
30
|
+
// the path identity like a plain folder until its first commit exists.
|
|
31
|
+
const rootCommits = gitRoot
|
|
32
|
+
? git(root, ['rev-list', '--max-parents=0', 'HEAD'])?.trim().split(/\s+/).filter(Boolean) ?? []
|
|
33
|
+
: [];
|
|
34
|
+
|
|
35
|
+
if (rootCommits.length > 0) {
|
|
36
|
+
const firstCommit = rootCommits.length === 1 ? rootCommits[0]! : rootCommits
|
|
37
|
+
.map((commit) => ({
|
|
38
|
+
commit,
|
|
39
|
+
timestamp: Number(git(root, ['show', '-s', '--format=%ct', commit])?.trim()),
|
|
40
|
+
}))
|
|
41
|
+
.filter(({ timestamp }) => Number.isFinite(timestamp))
|
|
42
|
+
.sort((left, right) => left.timestamp - right.timestamp || left.commit.localeCompare(right.commit))[0]?.commit;
|
|
43
|
+
if (!firstCommit) throw new Error(`Unable to resolve oldest root commit for git project at ${root}`);
|
|
44
|
+
const remote = git(root, ['remote', 'get-url', 'origin']);
|
|
45
|
+
identityKey = `git:${firstCommit}:${normalizeRemoteUrl(remote ?? '')}`;
|
|
46
|
+
} else {
|
|
47
|
+
identityKey = `path:${createHash('sha256').update(root).digest('hex')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stateDir = resolve(deps.stateDir ?? elanousStateRoot());
|
|
51
|
+
const registryPath = join(stateDir, 'dev-projects.json');
|
|
52
|
+
const registry = readRegistry(registryPath);
|
|
53
|
+
const id = registry.projects[identityKey] ?? newDevProjectId();
|
|
54
|
+
if (!registry.projects[identityKey]) {
|
|
55
|
+
registry.projects[identityKey] = id;
|
|
56
|
+
writeJsonAtomically(registryPath, registry);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The cache is disposable (RFC §4f): a read-only root must not stop the id from resolving.
|
|
60
|
+
try {
|
|
61
|
+
writeJsonAtomically(join(root, '.elanous', 'project.json'), { identityKey, devProject: id } satisfies DevProjectCache);
|
|
62
|
+
} catch { /* cache only */ }
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runGit(dir: string, args: readonly string[]): string | null {
|
|
67
|
+
try {
|
|
68
|
+
return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeRemoteUrl(remote: string): string {
|
|
75
|
+
const value = remote.trim();
|
|
76
|
+
if (!value) return '';
|
|
77
|
+
|
|
78
|
+
const scp = /^(?![a-z][a-z0-9+.-]*:\/\/)(?:[^@/]+@)?([^:/]+):(.+)$/.exec(value);
|
|
79
|
+
if (scp) return normalizeRemotePath(`${scp[1]!.toLowerCase()}/${scp[2]}`);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(value);
|
|
83
|
+
return normalizeRemotePath(`${url.hostname.toLowerCase()}${url.pathname}`);
|
|
84
|
+
} catch {
|
|
85
|
+
return normalizeRemotePath(resolve(value));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeRemotePath(value: string): string {
|
|
90
|
+
return value.replace(/\\/g, '/').replace(/\/+$/, '').replace(/\.git$/i, '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The registry is the source of truth for prj- ids. A missing file starts a fresh registry; an
|
|
95
|
+
* existing but unreadable/corrupt one throws instead of silently re-issuing every project a new id.
|
|
96
|
+
*/
|
|
97
|
+
function readRegistry(path: string): DevProjectRegistry {
|
|
98
|
+
let raw: string;
|
|
99
|
+
try {
|
|
100
|
+
raw = readFileSync(path, 'utf8');
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { projects: {} };
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
let parsed: Partial<DevProjectRegistry> | undefined;
|
|
106
|
+
try { parsed = JSON.parse(raw) as Partial<DevProjectRegistry>; } catch { parsed = undefined; }
|
|
107
|
+
if (parsed?.projects && typeof parsed.projects === 'object' && !Array.isArray(parsed.projects)) {
|
|
108
|
+
return { projects: parsed.projects as Record<string, string> };
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`dev-projects registry is corrupt: ${path}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function writeJsonAtomically(path: string, value: unknown): void {
|
|
114
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
115
|
+
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${newDevProjectId()}.tmp`);
|
|
116
|
+
writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
|
|
117
|
+
renameSync(temporaryPath, path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function newDevProjectId(): string {
|
|
121
|
+
const now = BigInt(Date.now());
|
|
122
|
+
let timestamp = '';
|
|
123
|
+
for (let index = 9; index >= 0; index -= 1) {
|
|
124
|
+
timestamp += ULID_ALPHABET[Number((now >> BigInt(index * 5)) & 31n)];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const entropy = BigInt(`0x${randomBytes(10).toString('hex')}`);
|
|
128
|
+
let random = '';
|
|
129
|
+
for (let index = 15; index >= 0; index -= 1) {
|
|
130
|
+
random += ULID_ALPHABET[Number((entropy >> BigInt(index * 5)) & 31n)];
|
|
131
|
+
}
|
|
132
|
+
return `prj-${timestamp}${random}`;
|
|
133
|
+
}
|