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
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
* - 무엇을: `--overlap-evidence <file>` 의 JSON 을 «믿지 않고» 잰다.
|
|
8
8
|
* ① 재기반 — 원격 base 의 «지금» 끝(`git ls-remote`)이 고정 머리의 조상인가(`git merge-base --is-ancestor`).
|
|
9
9
|
* ② 재게이트 — 증거의 게이트 기록이 «그 머리»를 «그 base 끝» 위에서 통과했다고 말하는가(머리·base 끝이 ① 과 맞물린다).
|
|
10
|
+
* ②' 재게이트 뒤 base 가 앞섰다(OVERLAP-EVIDENCE-BASE-WINDOW) — `gate.baseCommit ≠ 지금 끝`이면 «창»을 잰다:
|
|
11
|
+
* ⓐ `gate.baseCommit` 이 고정 머리의 조상 ⊕ 지금 끝의 조상이고 ⓑ 창 `gate.baseCommit..지금 끝`의 커밋이 «하나도»
|
|
12
|
+
* PR 의 바뀐 파일(머리 vs merge-base)을 안 건드리면 통과(관측 reason `base-advanced-disjoint`).
|
|
13
|
+
* 겹치면 `base-stale-overlap`(REJUDGE 가 재게이트를 청한다) · 못 재면 `base-window-unmeasured` · ⓐ 가 아니면 종전 사유.
|
|
10
14
|
* ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head12>.json`(TA 가 받는 `self review --json` stdout)이
|
|
11
|
-
* 같은 PR·같은 머리에 대해 `reviewed:true` · `verdict
|
|
15
|
+
* 같은 PR·같은 머리에 대해 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
|
|
16
|
+
* (자동 병합 canAuto · TA-LAND-WARN-MUSTFIX0 과 같은 규칙 — must-fix 를 못 셌으면 거부).
|
|
12
17
|
* ⓐ 이미 조상(TC #26019 실측) — 겹친 최근 착지 커밋이 «전부» 고정 머리의 조상이면 그 겹침은 이미 그 머리의 게이트·리뷰에
|
|
13
18
|
* 반영됐다 ⇒ ①② 없이 ③ 만으로 통과(관측 reason `already-ancestor`). 하나라도 조상이 아니면 ①②③ 전부.
|
|
19
|
+
* ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head12>.json` 만으로 잰다
|
|
20
|
+
* (관측 reason `already-ancestor-auto`). 조상이 아닌 착지가 하나라도 있거나 리뷰 결과가 없으면 거부.
|
|
14
21
|
* 하나라도 못 재거나 어긋나면 거부 — 호출부는 종전 거부를 그대로 낸다(fail-closed).
|
|
15
22
|
* - 관측: `pr.land` · `overlap-evidence` (decision · reason · 잰 값).
|
|
16
23
|
*/
|
|
17
|
-
import { basename, dirname } from 'node:path';
|
|
24
|
+
import { basename, dirname, join } from 'node:path';
|
|
18
25
|
import { debug } from '../debug/log.js';
|
|
19
26
|
import type { CmdRunner } from '../autopilot/pr-manager.js';
|
|
20
27
|
import { parseReviewResultOutput } from '../task-agent/review-result-capture.js';
|
|
@@ -33,6 +40,7 @@ interface OverlapEvidenceFile {
|
|
|
33
40
|
export type OverlapEvidenceVerdict =
|
|
34
41
|
| { ok: true; mode: 'already-ancestor'; reviewResult: string }
|
|
35
42
|
| { ok: true; mode: 'rebased'; baseTip: string; reviewResult: string }
|
|
43
|
+
| { ok: true; mode: 'base-advanced-disjoint'; baseTip: string; gateBase: string; windowCommits: number; reviewResult: string }
|
|
36
44
|
| { ok: false; reason: string };
|
|
37
45
|
|
|
38
46
|
export interface VerifyOverlapEvidenceInput {
|
|
@@ -48,9 +56,14 @@ export interface VerifyOverlapEvidenceInput {
|
|
|
48
56
|
overlappingLandings?: readonly string[];
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
type Checked = { ok: false; reason: string; baseTip?: string
|
|
59
|
+
type Checked = { ok: false; reason: string; baseTip?: string; gateBase?: string; windowCommits?: number }
|
|
60
|
+
| (OverlapEvidenceVerdict & { ok: true; baseTip?: string });
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
/** 리뷰가 받는 판정 — 자동 병합(canAuto)·TA 착지(TA-LAND-WARN-MUSTFIX0)와 같은 집합. */
|
|
63
|
+
const ACCEPTED_REVIEW_VERDICTS: ReadonlySet<string> = new Set(['pass', 'warn']);
|
|
64
|
+
const COMMIT_MARK = '\u0001';
|
|
65
|
+
|
|
66
|
+
function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>, e: Partial<OverlapEvidenceFile>, head: string, baseTip?: string): { ok: false; reason: string; baseTip?: string } | { ok: true; reviewResult: string } {
|
|
54
67
|
if (typeof e.reviewResult !== 'string' || !e.reviewResult) return { ok: false, reason: 'review-missing', baseTip };
|
|
55
68
|
// TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head12>.json` · reviewResultPath)만 — 아무 디렉터리의 같은 이름은 안 받는다.
|
|
56
69
|
if (basename(dirname(e.reviewResult)) !== 'review-results'
|
|
@@ -65,11 +78,67 @@ function checkReview(input: VerifyOverlapEvidenceInput, e: Partial<OverlapEviden
|
|
|
65
78
|
if (!review.head || review.head.toLowerCase() !== head) return { ok: false, reason: 'review-head-mismatch', baseTip };
|
|
66
79
|
if (review.error) return { ok: false, reason: 'review-error', baseTip };
|
|
67
80
|
if (review.reviewed !== true) return { ok: false, reason: 'review-not-reviewed', baseTip };
|
|
68
|
-
if (review.verdict
|
|
81
|
+
if (review.verdict === null || !ACCEPTED_REVIEW_VERDICTS.has(review.verdict)) return { ok: false, reason: `review-verdict-${review.verdict ?? 'null'}`, baseTip };
|
|
69
82
|
if (review.mustFix !== 0) return { ok: false, reason: review.mustFix === null ? 'review-must-fix-unknown' : `review-must-fix-${review.mustFix}`, baseTip };
|
|
70
83
|
return { ok: true, reviewResult: e.reviewResult };
|
|
71
84
|
}
|
|
72
85
|
|
|
86
|
+
type WindowResult = { ok: true; windowCommits: number } | { ok: false; reason: string; windowCommits?: number };
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* ②' — 재게이트 뒤 앞선 base 의 창 `gateBase..baseTip` 이 PR 의 바뀐 파일과 «서로소»인가. 읽기 전용 git 만 쓴다
|
|
90
|
+
* (끝 커밋이 로컬에 없을 때만 ref 를 안 쓰는 fetch). `staleReason` 은 ⓐ(조상 아님)일 때 그대로 내는 종전 사유다.
|
|
91
|
+
*/
|
|
92
|
+
function checkBaseWindow(input: VerifyOverlapEvidenceInput, head: string, baseTip: string, gateBase: string, staleReason: string): WindowResult {
|
|
93
|
+
const git = (args: string[]) => input.run('git', args, { cwd: input.cwd });
|
|
94
|
+
const short = (sha: string) => sha.slice(0, 12);
|
|
95
|
+
if (!git(['merge-base', '--is-ancestor', gateBase, head]).ok) {
|
|
96
|
+
return { ok: false, reason: `${staleReason} (gate base is not an ancestor of head)` };
|
|
97
|
+
}
|
|
98
|
+
if (!git(['cat-file', '-e', `${baseTip}^{commit}`]).ok) {
|
|
99
|
+
// ref 를 안 바꾸는 fetch — FETCH_HEAD 도 원격 추적 ref 도 안 쓴다.
|
|
100
|
+
git(['fetch', '--quiet', '--no-write-fetch-head', '--refmap=', input.remote, `refs/heads/${input.branch}`]);
|
|
101
|
+
if (!git(['cat-file', '-e', `${baseTip}^{commit}`]).ok) {
|
|
102
|
+
return { ok: false, reason: `base-window-unmeasured: ${input.remote}/${input.branch} ${short(baseTip)} not available locally · ${staleReason}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!git(['merge-base', '--is-ancestor', gateBase, baseTip]).ok) {
|
|
106
|
+
return { ok: false, reason: `${staleReason} (gate base is not an ancestor of ${input.remote}/${input.branch})` };
|
|
107
|
+
}
|
|
108
|
+
const mergeBase = git(['merge-base', baseTip, head]);
|
|
109
|
+
const forkPoint = mergeBase.ok ? mergeBase.out.trim() : '';
|
|
110
|
+
if (!SHA40.test(forkPoint)) return { ok: false, reason: `base-window-unmeasured: merge-base ${short(baseTip)} ${short(head)} failed · ${staleReason}` };
|
|
111
|
+
const diff = git(['diff', '--name-only', '--no-renames', forkPoint, head]);
|
|
112
|
+
if (!diff.ok) return { ok: false, reason: `base-window-unmeasured: diff --name-only ${short(forkPoint)} ${short(head)} failed · ${staleReason}` };
|
|
113
|
+
const prFiles = new Set(diff.out.split('\n').map((line) => line.trim()).filter(Boolean));
|
|
114
|
+
if (prFiles.size === 0) return { ok: false, reason: `base-window-unmeasured: PR changed files are empty · ${staleReason}` };
|
|
115
|
+
// 병합 커밋은 첫 부모 대비 diff(-m --first-parent) — base 에 «들어온» 변경만, 빠짐없이.
|
|
116
|
+
const log = git(['log', '--first-parent', '-m', '--no-renames', '--name-only', `--format=${COMMIT_MARK}%H`, `${gateBase}..${baseTip}`]);
|
|
117
|
+
if (!log.ok) return { ok: false, reason: `base-window-unmeasured: log ${short(gateBase)}..${short(baseTip)} failed · ${staleReason}` };
|
|
118
|
+
const touched = new Map<string, string[]>();
|
|
119
|
+
let current = '';
|
|
120
|
+
for (const raw of log.out.split('\n')) {
|
|
121
|
+
const line = raw.trim();
|
|
122
|
+
if (!line) continue;
|
|
123
|
+
if (line.startsWith(COMMIT_MARK)) { current = line.slice(COMMIT_MARK.length); if (!touched.has(current)) touched.set(current, []); continue; }
|
|
124
|
+
if (current && prFiles.has(line)) touched.get(current)!.push(line);
|
|
125
|
+
}
|
|
126
|
+
const windowCommits = touched.size;
|
|
127
|
+
if (windowCommits === 0 || [...touched.keys()].some((sha) => !SHA40.test(sha))) {
|
|
128
|
+
return { ok: false, reason: `base-window-unmeasured: window ${short(gateBase)}..${short(baseTip)} listed no commits · ${staleReason}` };
|
|
129
|
+
}
|
|
130
|
+
const overlapping = [...touched.entries()].filter(([, files]) => files.length > 0);
|
|
131
|
+
if (overlapping.length > 0) {
|
|
132
|
+
const files = [...new Set(overlapping.flatMap(([, f]) => f))];
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
windowCommits,
|
|
136
|
+
reason: `base-stale-overlap: gate ${short(gateBase)} ≠ ${input.remote}/${input.branch} ${short(baseTip)} · ${overlapping.length}/${windowCommits} window commits touch PR files (${overlapping.map(([sha]) => short(sha)).slice(0, 5).join(', ')}): ${files.slice(0, 10).join(', ')}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, windowCommits };
|
|
140
|
+
}
|
|
141
|
+
|
|
73
142
|
function check(input: VerifyOverlapEvidenceInput): Checked {
|
|
74
143
|
const head = input.expectedHead.toLowerCase();
|
|
75
144
|
let raw: unknown;
|
|
@@ -94,15 +163,32 @@ function check(input: VerifyOverlapEvidenceInput): Checked {
|
|
|
94
163
|
const baseTip = remote.ok ? remote.out.trim().split(/\s+/)[0] ?? '' : '';
|
|
95
164
|
if (!SHA40.test(baseTip)) return { ok: false, reason: `rebase-unmeasured: ls-remote ${input.remote} ${input.branch} failed` };
|
|
96
165
|
const ancestor = input.run('git', ['merge-base', '--is-ancestor', baseTip, head], { cwd: input.cwd });
|
|
97
|
-
|
|
166
|
+
const gate = e.gate;
|
|
167
|
+
// ②' 의 입구 — 게이트 기록이 «그 머리»를 통과했고 base 만 지금 끝과 다르다.
|
|
168
|
+
const gateBase = gate && typeof gate === 'object' && gate.passed === true
|
|
169
|
+
&& typeof gate.head === 'string' && gate.head.toLowerCase() === head
|
|
170
|
+
&& typeof gate.baseCommit === 'string' && SHA40.test(gate.baseCommit) && gate.baseCommit.toLowerCase() !== baseTip.toLowerCase()
|
|
171
|
+
? gate.baseCommit.toLowerCase() : undefined;
|
|
172
|
+
const advanced = (staleReason: string): Checked => {
|
|
173
|
+
const window = checkBaseWindow(input, head, baseTip, gateBase!, staleReason);
|
|
174
|
+
if (!window.ok) return { ok: false, reason: window.reason, baseTip, gateBase, windowCommits: window.windowCommits };
|
|
175
|
+
const review = checkReview(input, e, head, baseTip);
|
|
176
|
+
return review.ok
|
|
177
|
+
? { ok: true, mode: 'base-advanced-disjoint', baseTip, gateBase: gateBase!, windowCommits: window.windowCommits, reviewResult: review.reviewResult }
|
|
178
|
+
: { ...review, gateBase, windowCommits: window.windowCommits };
|
|
179
|
+
};
|
|
180
|
+
if (!ancestor.ok) {
|
|
181
|
+
const notRebased = `not-rebased: ${input.remote}/${input.branch} ${baseTip.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}`;
|
|
182
|
+
return gateBase ? advanced(notRebased) : { ok: false, reason: notRebased, baseTip };
|
|
183
|
+
}
|
|
98
184
|
|
|
99
185
|
// ② 재게이트 — 그 머리를, ① 의 그 base 끝 위에서.
|
|
100
|
-
const gate = e.gate;
|
|
101
186
|
if (!gate || typeof gate !== 'object') return { ok: false, reason: 'gate-missing', baseTip };
|
|
102
187
|
if (gate.passed !== true) return { ok: false, reason: 'gate-not-passed', baseTip };
|
|
103
188
|
if (typeof gate.head !== 'string' || gate.head.toLowerCase() !== head) return { ok: false, reason: 'gate-head-mismatch', baseTip };
|
|
104
189
|
if (typeof gate.baseCommit !== 'string' || gate.baseCommit.toLowerCase() !== baseTip.toLowerCase()) {
|
|
105
|
-
|
|
190
|
+
const stale = `gate-base-stale: gate ${String(gate.baseCommit).slice(0, 12)} ≠ ${input.remote}/${input.branch} ${baseTip.slice(0, 12)}`;
|
|
191
|
+
return gateBase ? advanced(stale) : { ok: false, reason: stale, baseTip };
|
|
106
192
|
}
|
|
107
193
|
|
|
108
194
|
// ③ 리뷰 — 그 PR · 그 머리의 pass ⊕ must-fix 0.
|
|
@@ -110,22 +196,84 @@ function check(input: VerifyOverlapEvidenceInput): Checked {
|
|
|
110
196
|
return review.ok ? { ok: true, mode: 'rebased', baseTip, reviewResult: review.reviewResult } : review;
|
|
111
197
|
}
|
|
112
198
|
|
|
199
|
+
export type AncestorAutoVerdict =
|
|
200
|
+
| { ok: true; mode: 'already-ancestor-auto'; reviewResult: string }
|
|
201
|
+
| { ok: false; reason: string };
|
|
202
|
+
|
|
203
|
+
export interface VerifyAncestorAutoInput {
|
|
204
|
+
pr: number;
|
|
205
|
+
expectedHead: string;
|
|
206
|
+
cwd: string;
|
|
207
|
+
run: CmdRunner;
|
|
208
|
+
readFile: (path: string) => string;
|
|
209
|
+
/** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head12>.json` 만 본다. */
|
|
210
|
+
reviewResultsDir: string;
|
|
211
|
+
listDir: (dir: string) => readonly string[];
|
|
212
|
+
/** 겹침 관문이 찾은 «겹친» 최근 착지 커밋들. */
|
|
213
|
+
overlappingLandings: readonly string[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function checkAncestorAuto(input: VerifyAncestorAutoInput): AncestorAutoVerdict {
|
|
217
|
+
const head = input.expectedHead.toLowerCase();
|
|
218
|
+
const landings = input.overlappingLandings;
|
|
219
|
+
if (landings.length === 0) return { ok: false, reason: 'ancestor-auto-no-overlapping-landings' };
|
|
220
|
+
const notAncestor = landings.find((sha) => !(/^[0-9a-f]{7,40}$/i.test(sha)
|
|
221
|
+
&& input.run('git', ['merge-base', '--is-ancestor', sha, head], { cwd: input.cwd }).ok));
|
|
222
|
+
if (notAncestor !== undefined) return { ok: false, reason: `ancestor-auto-not-ancestor: ${notAncestor.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}` };
|
|
223
|
+
let names: readonly string[];
|
|
224
|
+
try { names = input.listDir(input.reviewResultsDir); } catch { return { ok: false, reason: 'review-missing' }; }
|
|
225
|
+
const suffix = new RegExp(`-${input.pr}-${head.slice(0, 12)}\\.json$`, 'i');
|
|
226
|
+
// 같은 <pr>-<head12> 리뷰가 여럿이면 «전부» 통과해야 한다 — 하나라도 거부면 거부(fail-closed · 처음 통과한 하나로 승인하지 않는다).
|
|
227
|
+
let accepted: string | undefined;
|
|
228
|
+
for (const name of [...names].filter((n) => suffix.test(n)).sort()) {
|
|
229
|
+
const review = checkReview(input, { reviewResult: join(input.reviewResultsDir, name) }, head);
|
|
230
|
+
if (!review.ok) return { ok: false, reason: review.reason };
|
|
231
|
+
accepted = review.reviewResult;
|
|
232
|
+
}
|
|
233
|
+
return accepted === undefined ? { ok: false, reason: 'review-missing' } : { ok: true, mode: 'already-ancestor-auto', reviewResult: accepted };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 증거 파일이 없을 때의 already-ancestor 자동 판정 — 겹친 착지가 «전부» 고정 머리의 조상이고 표준 자리의 리뷰 결과가
|
|
238
|
+
* pass|warn ⊕ must-fix 0 이면 통과. 던지지 않는다 — 어떤 실패도 거부(fail-closed)이고 관측 한 줄을 남긴다.
|
|
239
|
+
*/
|
|
240
|
+
export function verifyAncestorAutoPass(input: VerifyAncestorAutoInput): AncestorAutoVerdict {
|
|
241
|
+
let verdict: AncestorAutoVerdict;
|
|
242
|
+
try { verdict = checkAncestorAuto(input); } catch (error) { verdict = { ok: false, reason: `verify-threw: ${String(error).slice(0, 200)}` }; }
|
|
243
|
+
debug.log('pr.land', 'overlap-evidence', {
|
|
244
|
+
decision: verdict.ok ? 'accepted' : 'rejected',
|
|
245
|
+
reason: verdict.ok ? 'already-ancestor-auto' : verdict.reason,
|
|
246
|
+
overlappingLandings: input.overlappingLandings,
|
|
247
|
+
pr: input.pr,
|
|
248
|
+
head: input.expectedHead,
|
|
249
|
+
evidencePath: null,
|
|
250
|
+
reviewResultsDir: input.reviewResultsDir,
|
|
251
|
+
});
|
|
252
|
+
return verdict;
|
|
253
|
+
}
|
|
254
|
+
|
|
113
255
|
/** 던지지 않는다 — 어떤 실패도 거부(fail-closed)이고 관측 한 줄을 남긴다. */
|
|
114
256
|
export function verifyOverlapEvidence(input: VerifyOverlapEvidenceInput): OverlapEvidenceVerdict {
|
|
115
257
|
let verdict: Checked;
|
|
116
258
|
try { verdict = check(input); } catch (error) { verdict = { ok: false, reason: `verify-threw: ${String(error).slice(0, 200)}` }; }
|
|
117
259
|
debug.log('pr.land', 'overlap-evidence', {
|
|
118
260
|
decision: verdict.ok ? 'accepted' : 'rejected',
|
|
119
|
-
reason: verdict.ok
|
|
261
|
+
reason: verdict.ok
|
|
262
|
+
? (verdict.mode === 'already-ancestor' ? 'already-ancestor' : verdict.mode === 'base-advanced-disjoint' ? 'base-advanced-disjoint' : 'rebased+gated+reviewed')
|
|
263
|
+
: verdict.reason,
|
|
120
264
|
overlappingLandings: input.overlappingLandings ?? [],
|
|
121
265
|
pr: input.pr,
|
|
122
266
|
head: input.expectedHead,
|
|
123
267
|
base: `${input.remote}/${input.branch}`,
|
|
124
268
|
baseTip: verdict.baseTip ?? null,
|
|
125
269
|
evidencePath: input.evidencePath,
|
|
270
|
+
gateBase: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.gateBase : undefined) : verdict.gateBase) ?? null,
|
|
271
|
+
windowCommits: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.windowCommits : undefined) : verdict.windowCommits) ?? null,
|
|
126
272
|
});
|
|
127
273
|
if (!verdict.ok) return { ok: false, reason: verdict.reason };
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
274
|
+
if (verdict.mode === 'already-ancestor') return { ok: true, mode: 'already-ancestor', reviewResult: verdict.reviewResult };
|
|
275
|
+
if (verdict.mode === 'base-advanced-disjoint') {
|
|
276
|
+
return { ok: true, mode: 'base-advanced-disjoint', baseTip: verdict.baseTip!, gateBase: verdict.gateBase, windowCommits: verdict.windowCommits, reviewResult: verdict.reviewResult };
|
|
277
|
+
}
|
|
278
|
+
return { ok: true, mode: 'rebased', baseTip: verdict.baseTip!, reviewResult: verdict.reviewResult };
|
|
131
279
|
}
|
package/src/cli/release-cli.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// elanous release tag --version <x.y.z> --source <commit> [--yes]
|
|
6
6
|
// elanous release verify [--version <x.y.z>]
|
|
7
7
|
// elanous release notes --from <ref> [--to <ref>]
|
|
8
|
-
// elanous release run --version <x.y.z> [--prerelease rc] [--main-cut | --cut-commit <sha>] [--publish-at <iso>] [--dry-run] [--json] [--if-ready]
|
|
9
|
-
// elanous release resume --run <runId> --from <node> [--waive <node> --reason <text>] [--accepted-regressions <json>] [--json]
|
|
8
|
+
// elanous release run --version <x.y.z> [--prerelease rc] [--main-cut | --cut-commit <sha>] [--publish-at <iso>] [--gate-executor <pod|host> [--gate-host <ssh-host>]] [--dry-run] [--json] [--if-ready]
|
|
9
|
+
// elanous release resume --run <runId> --from <node> [--waive <node> --reason <text>] [--accepted-regressions <json>] [--gate-executor <pod|host> [--gate-host <ssh-host>]] [--json]
|
|
10
10
|
// elanous release auto-start [--window-minutes <n>] [--apply] [--json]
|
|
11
11
|
// elanous release rehearsal tick [--apply] [--checkout <dir>] [--json]
|
|
12
12
|
// elanous release preflight --version <x.y.z> [--publish-at <iso>] [--json] (컷 30분 전 사전 점검 · 읽기 전용 · ⛔ 있으면 exit 1)
|
|
@@ -989,7 +989,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
989
989
|
.action(async (o: { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; json?: boolean }) => {
|
|
990
990
|
await jsonAction(o.json, async (log) => cutReleaseBranch({ ...o, log }), () => true);
|
|
991
991
|
});
|
|
992
|
-
const runAction = async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
|
|
992
|
+
const runAction = async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; gateExecutor?: string; gateHost?: string; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
|
|
993
993
|
// Only the rc rehearsal channel is operated for now (alpha/beta stay valid version shapes, not run options).
|
|
994
994
|
if (o.prerelease !== undefined && o.prerelease !== 'rc') {
|
|
995
995
|
const message = `--prerelease supports rc only: ${o.prerelease}`;
|
|
@@ -1038,7 +1038,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
1038
1038
|
const outcome = await runReleaseIfReady(o.version, {
|
|
1039
1039
|
ledgerRoot: releaseRunDeps.ledgerRoot,
|
|
1040
1040
|
readiness: (version, options) => releaseReadiness(version, { ledgerRoot: releaseRunDeps.ledgerRoot, checklist: releaseRunDeps.checklist, ...options }),
|
|
1041
|
-
run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt }, releaseRunDeps),
|
|
1041
|
+
run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt, gateExecutor: o.gateExecutor, gateHost: o.gateHost }, releaseRunDeps),
|
|
1042
1042
|
});
|
|
1043
1043
|
if (outcome.skipped) {
|
|
1044
1044
|
if (o.json) await writeStdoutJson(`${JSON.stringify(outcome)}\n`);
|
|
@@ -1066,7 +1066,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
1066
1066
|
}
|
|
1067
1067
|
}
|
|
1068
1068
|
await jsonAction(o.json, async (log) => {
|
|
1069
|
-
const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt }, releaseRunDeps);
|
|
1069
|
+
const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt, gateExecutor: o.gateExecutor, gateHost: o.gateHost }, releaseRunDeps);
|
|
1070
1070
|
log(`${result.dryRun ? '· 드라이런' : '▶ 릴리스 루프'} ${result.input.version} · 입력 ${JSON.stringify(result.input)}`);
|
|
1071
1071
|
return result;
|
|
1072
1072
|
}, (r) => r.dryRun || r.state?.status === 'done' || r.state?.status === 'awaiting-approval');
|
|
@@ -1081,8 +1081,10 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
1081
1081
|
.option('--if-ready', '준비되지 않았으면 사유를 출력하고 성공으로 건너뛴다')
|
|
1082
1082
|
.option('--publish-at <iso>', '승인·선행 노드 통과 뒤 지정 시각까지 발행 대기(오프셋 포함 ISO 시각)')
|
|
1083
1083
|
.option('--force-freeze', '동결 중 게이트·발행 강행(관측 기록) — 가지 컷은 동결이 필요 없다 · 동결은 비상 스위치로 남는다')
|
|
1084
|
+
.option('--gate-executor <pod|host>', 'gate 노드 실행기(기본 pod) — host 는 Pod 대신 SSH 호스트의 깨끗한 클론에서 같은 판정(면제 아님)')
|
|
1085
|
+
.option('--gate-host <ssh-host>', '--gate-executor host 의 SSH 호스트(기본 release.loop.gateRemote)')
|
|
1084
1086
|
.option('--json', '결과 한 줄 JSON(stdout) · 사람 줄은 stderr')
|
|
1085
|
-
.action(async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; json?: boolean }) => { await runAction(o); });
|
|
1087
|
+
.action(async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; gateExecutor?: string; gateHost?: string; json?: boolean }) => { await runAction(o); });
|
|
1086
1088
|
release.command('preflight')
|
|
1087
1089
|
.description('컷 30분 전 사전 점검(읽기 전용) — 체크리스트 게이트 모의 · 발행 뒤 칸 · run --dry-run 입력 · 동결 · 일정/게이트 실측. ⛔ 있으면 exit 1')
|
|
1088
1090
|
.requiredOption('--version <v>', '점검할 판(x.y.z)')
|
|
@@ -1128,8 +1130,10 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
1128
1130
|
.option('--waive <node>', '저장된 실패 노드 한 개를 면제(--from 은 그 노드 그대로)')
|
|
1129
1131
|
.option('--reason <text>', '면제 사유 — 공개 알려진 문제에 들어갈 사용자 영향 설명')
|
|
1130
1132
|
.option('--accepted-regressions <json>', '알려진 문제 추가 입력 JSON 배열 [{"id":"…","note":"…"}]')
|
|
1133
|
+
.option('--gate-executor <pod|host>', '다시 도는 gate 의 실행기 전환(--from gate 이전만) — 런 입력에 기록 · 면제 아님')
|
|
1134
|
+
.option('--gate-host <ssh-host>', '--gate-executor host 의 SSH 호스트(기본 저장된 런 입력 gateRemote)')
|
|
1131
1135
|
.option('--json', '결과 한 줄 JSON(stdout)')
|
|
1132
|
-
.action(async (o: { run: string; from: string; partial?: boolean; waive?: string; reason?: string; acceptedRegressions?: string; json?: boolean }) => {
|
|
1136
|
+
.action(async (o: { run: string; from: string; partial?: boolean; waive?: string; reason?: string; acceptedRegressions?: string; gateExecutor?: string; gateHost?: string; json?: boolean }) => {
|
|
1133
1137
|
await jsonAction(o.json, async (log) => {
|
|
1134
1138
|
let acceptedRegressions: AcceptedRegression[] | undefined;
|
|
1135
1139
|
if (o.acceptedRegressions !== undefined) {
|
|
@@ -1143,12 +1147,17 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
1143
1147
|
const result = await resumeReleaseRun({ runId: o.run, from: o.from, partial: o.partial,
|
|
1144
1148
|
...(o.waive === undefined ? {} : { waive: o.waive }),
|
|
1145
1149
|
...(o.reason === undefined ? {} : { reason: o.reason }),
|
|
1146
|
-
...(o.acceptedRegressions === undefined ? {} : { acceptedRegressions })
|
|
1150
|
+
...(o.acceptedRegressions === undefined ? {} : { acceptedRegressions }),
|
|
1151
|
+
...(o.gateExecutor === undefined ? {} : { gateExecutor: o.gateExecutor }),
|
|
1152
|
+
...(o.gateHost === undefined ? {} : { gateHost: o.gateHost }) });
|
|
1153
|
+
const override = result.state.inputOverrides?.at(-1);
|
|
1154
|
+
if (o.gateExecutor !== undefined && override) log(`⇄ gate 실행기 ${override.set.gateExecutor}${override.set.gateRemote ? ` @ ${override.set.gateRemote}` : ''} (런 입력에 기록 · 면제 아님)`);
|
|
1147
1155
|
if (result.partial.requested) log(result.partial.applied
|
|
1148
1156
|
? `◐ 부분 재검: ${result.partial.files}파일 · 나머지는 앞 게이트(${result.partial.priorCommit.slice(0, 12)}) 결과를 잇는다`
|
|
1149
1157
|
: `● 전체 게이트로: ${result.partial.reason}`);
|
|
1150
1158
|
log(`${result.tip.changed ? `↻ ${result.tip.branch} ${result.tip.from.slice(0, 12)} → ${result.tip.to.slice(0, 12)} (백업 ${result.tip.backup})` : `· ${result.tip.branch} 끝 그대로 ${result.tip.to.slice(0, 12)}`} · ${o.from} 부터 → ${result.state.status}`);
|
|
1151
|
-
return { tip: result.tip, partial: result.partial, status: result.state.status, runId: result.state.runId
|
|
1159
|
+
return { tip: result.tip, partial: result.partial, status: result.state.status, runId: result.state.runId,
|
|
1160
|
+
...(o.gateExecutor !== undefined && result.state.inputOverrides?.length ? { gateExecutor: result.state.inputOverrides.at(-1) } : {}) };
|
|
1152
1161
|
}, (r) => r.status === 'done' || r.status === 'awaiting-approval');
|
|
1153
1162
|
});
|
|
1154
1163
|
release.command('rehearsal').description('판 일정에서 컷 전 가벼운 RC 두 단계').command('tick')
|
package/src/cli/rfc-status.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
6
|
export const RFC_STATUSES = ['unknown', 'proposed', 'adopted', 'completed', 'superseded', 'rejected'] as const;
|
|
@@ -69,6 +69,49 @@ export function adoptedWithoutCard(rows: readonly RfcRow[]): RfcRow[] {
|
|
|
69
69
|
return rows.filter((row) => row.status === 'adopted' && row.card === null);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const RFC_HUB_FILE = 'INDEX-loop-agents-and-harness-rfcs.md';
|
|
73
|
+
|
|
74
|
+
function markdownLinks(markdown: string): string[] {
|
|
75
|
+
return [...markdown.matchAll(/\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g)].map((match) => match[1]!);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasHubConnectionReference(text: string, documentPath: string, root: string): boolean {
|
|
79
|
+
const firstSection = text.search(/^##\s/m);
|
|
80
|
+
const header = firstSection < 0 ? text : text.slice(0, firstSection);
|
|
81
|
+
const hubPath = resolve(root, 'docs', RFC_HUB_FILE);
|
|
82
|
+
return header.split(/\r?\n/).some((line) => {
|
|
83
|
+
if (!line.includes('🔗') || !line.includes('연결 참조')) return false;
|
|
84
|
+
return markdownLinks(line).some((target) =>
|
|
85
|
+
resolve(dirname(resolve(root, documentPath)), target) === hubPath);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 허브를 못 읽으면 null — «0» 과 «못 셌음» 은 다른 값이다. */
|
|
90
|
+
function hubConnectionCounts(root: string, rows: readonly RfcRow[]): { unconnected: number; unlisted: number } | null {
|
|
91
|
+
const hubPath = resolve(root, 'docs', RFC_HUB_FILE);
|
|
92
|
+
let hub: string;
|
|
93
|
+
try { hub = readFileSync(hubPath, 'utf8'); }
|
|
94
|
+
catch { return null; }
|
|
95
|
+
|
|
96
|
+
const hubDirectory = dirname(hubPath);
|
|
97
|
+
const listedPaths = new Set(markdownLinks(hub)
|
|
98
|
+
.filter((target) => /^RFC-.*\.md$/.test(target.split(/[?#]/, 1)[0]!))
|
|
99
|
+
.map((target) => relative(root, resolve(hubDirectory, target)).replaceAll('\\', '/')));
|
|
100
|
+
const rowsByPath = new Map(rows.map((row) => [row.path, row]));
|
|
101
|
+
const unconnected = [...listedPaths].filter((path) => {
|
|
102
|
+
const row = rowsByPath.get(path);
|
|
103
|
+
if (!row) return false;
|
|
104
|
+
return !hasHubConnectionReference(readFileSync(resolve(root, row.path), 'utf8'), row.path, root);
|
|
105
|
+
}).length;
|
|
106
|
+
const unlisted = rows.filter((row) => {
|
|
107
|
+
if (listedPaths.has(row.path)) return false;
|
|
108
|
+
const text = readFileSync(resolve(root, row.path), 'utf8');
|
|
109
|
+
const title = /^#\s+(.+)$/m.exec(text)?.[1] ?? '';
|
|
110
|
+
return /루프|하니스|그래프/.test(title);
|
|
111
|
+
}).length;
|
|
112
|
+
return { unconnected, unlisted };
|
|
113
|
+
}
|
|
114
|
+
|
|
72
115
|
export function runRfcStatus(options: { json?: boolean; missingCards?: boolean }, root = process.cwd(),
|
|
73
116
|
write: (text: string) => void = (text) => process.stdout.write(text)): void {
|
|
74
117
|
const all = scanRfcs(root);
|
|
@@ -77,9 +120,13 @@ export function runRfcStatus(options: { json?: boolean; missingCards?: boolean }
|
|
|
77
120
|
write(`${JSON.stringify({ total: all.length, adoptedWithoutCard: adoptedWithoutCard(all).length, rows }, null, 2)}\n`);
|
|
78
121
|
return;
|
|
79
122
|
}
|
|
123
|
+
const hubCounts = hubConnectionCounts(root, all);
|
|
80
124
|
write(`RFC ${all.length} · 채택·칸 없음 ${adoptedWithoutCard(all).length} · 표시 ${rows.length}\n`);
|
|
81
125
|
write('상태\t담당\t칸\t대체 문서\t핸드북\t옛 상태\t옛 담당\t경로\t메타 오류\n');
|
|
82
126
|
for (const row of rows) write(`${row.status}\t${row.owner ?? '모름'}\t${row.card ?? '모름'}\t${row.supersededBy ?? '모름'}\t${row.handbook ?? '모름'}\t${row.legacyStatus ?? '-'}\t${row.legacyOwner ?? '-'}\t${row.path}\t${row.issue ?? '-'}\n`);
|
|
127
|
+
write(hubCounts
|
|
128
|
+
? `허브 연결: 되돌림 없음 ${hubCounts.unconnected} · 미등재 ${hubCounts.unlisted}\n`
|
|
129
|
+
: `허브 연결: 못 셈 — docs/${RFC_HUB_FILE} 를 못 읽었다\n`);
|
|
83
130
|
}
|
|
84
131
|
|
|
85
132
|
export function registerRfcStatusCommand(docs: Command): void {
|
|
@@ -87,5 +134,8 @@ export function registerRfcStatusCommand(docs: Command): void {
|
|
|
87
134
|
.description('전체 RFC 생애주기 표 (RFC frontmatter만 읽음)')
|
|
88
135
|
.option('--json', '기계가 읽을 JSON')
|
|
89
136
|
.option('--missing-cards', '채택됐으나 칸이 기록되지 않은 RFC만')
|
|
90
|
-
.action((options: { json?: boolean; missingCards?: boolean }) =>
|
|
137
|
+
.action((options: { json?: boolean; missingCards?: boolean }): void => {
|
|
138
|
+
// registerDocsCommands in docs-cli.ts calls registerRfcStatusCommand; this action calls runRfcStatus and its hub check.
|
|
139
|
+
runRfcStatus(options);
|
|
140
|
+
});
|
|
91
141
|
}
|
package/src/cli/self-update.ts
CHANGED
|
@@ -428,6 +428,80 @@ export interface VersionPruneOutcome {
|
|
|
428
428
|
skipped?: string;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
export interface RailSafeProcessListing {
|
|
432
|
+
status: number | null;
|
|
433
|
+
stdout: string;
|
|
434
|
+
stderr: string;
|
|
435
|
+
error?: Error;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface AssessRailSafeInput {
|
|
439
|
+
entries: ReadonlyArray<{ name: string; mtimeMs: number }>;
|
|
440
|
+
current: string;
|
|
441
|
+
daemonSha: string;
|
|
442
|
+
keep: number;
|
|
443
|
+
versionsDir: string;
|
|
444
|
+
listProcesses?: (pattern: string) => RailSafeProcessListing;
|
|
445
|
+
readProcessCommand?: (pid: string) => string | null;
|
|
446
|
+
isReleaseRunActive?: () => boolean;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Shadow probes must never stall the install: a hung pgrep/ps reads as «no process» (status null). */
|
|
450
|
+
const RAIL_SAFE_PROBE_TIMEOUT_MS = 5_000;
|
|
451
|
+
|
|
452
|
+
function pgrepProcesses(pattern: string): RailSafeProcessListing {
|
|
453
|
+
const result = spawnSync('pgrep', ['-f', pattern], { encoding: 'utf8', maxBuffer: SPAWN_MAX_BUFFER, timeout: RAIL_SAFE_PROBE_TIMEOUT_MS });
|
|
454
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.error?.message ?? result.stderr ?? '', ...(result.error ? { error: result.error } : {}) };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function processCommand(pid: string): string | null {
|
|
458
|
+
const result = spawnSync('ps', ['-p', pid, '-o', 'command='], { encoding: 'utf8', maxBuffer: SPAWN_MAX_BUFFER, timeout: RAIL_SAFE_PROBE_TIMEOUT_MS });
|
|
459
|
+
return result.status === 0 ? (result.stdout ?? '').trim() || null : null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function listedProcessCommands(listing: RailSafeProcessListing, readCommand: (pid: string) => string | null): string[] {
|
|
463
|
+
if (listing.status !== 0) return [];
|
|
464
|
+
return listing.stdout.split(/\r?\n/).flatMap((line) => {
|
|
465
|
+
const entry = line.trim();
|
|
466
|
+
if (!entry) return [];
|
|
467
|
+
if (!/^\d+$/.test(entry)) return [entry];
|
|
468
|
+
const command = readCommand(entry);
|
|
469
|
+
return command ? [command] : [];
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function pgrepPathPattern(path: string): string {
|
|
474
|
+
return path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Shadow-only observation: determine whether a prune plan targets an argv-held version or a release run is active. */
|
|
478
|
+
export function assessRailSafe(input: AssessRailSafeInput): {
|
|
479
|
+
wouldBlock: boolean;
|
|
480
|
+
reasons: Array<'held-version' | 'release-run-active'>;
|
|
481
|
+
heldVersions: string[];
|
|
482
|
+
releaseRunActive: boolean;
|
|
483
|
+
} {
|
|
484
|
+
const plan = planVersionPrune(input.entries, input.current, input.daemonSha, input.keep);
|
|
485
|
+
const listProcesses = input.listProcesses ?? pgrepProcesses;
|
|
486
|
+
const readCommand = input.readProcessCommand ?? processCommand;
|
|
487
|
+
const heldVersions = plan.removed.filter((name) => {
|
|
488
|
+
const path = join(input.versionsDir, name);
|
|
489
|
+
try {
|
|
490
|
+
return listedProcessCommands(listProcesses(pgrepPathPattern(path)), readCommand).some((argv) => argv.includes(path));
|
|
491
|
+
} catch { return false; }
|
|
492
|
+
});
|
|
493
|
+
let releaseRunActive = false;
|
|
494
|
+
try {
|
|
495
|
+
releaseRunActive = input.isReleaseRunActive?.() ?? listedProcessCommands(
|
|
496
|
+
listProcesses('release[[:space:]]+(run|resume)'), readCommand,
|
|
497
|
+
).some((argv) => /release\s+(?:run|resume)/.test(argv));
|
|
498
|
+
} catch { releaseRunActive = false; }
|
|
499
|
+
const reasons: Array<'held-version' | 'release-run-active'> = [];
|
|
500
|
+
if (heldVersions.length) reasons.push('held-version');
|
|
501
|
+
if (releaseRunActive) reasons.push('release-run-active');
|
|
502
|
+
return { wouldBlock: reasons.length > 0, reasons, heldVersions, releaseRunActive };
|
|
503
|
+
}
|
|
504
|
+
|
|
431
505
|
/**
|
|
432
506
|
* 야간 자동 설치는 하룻밤에 한 판(약 641MB · 2026-09-24 실측)을 쌓는다. 설치기는 옛 판을 안 지운다.
|
|
433
507
|
* 남기는 것: `current` 가 가리키는 판 · 지금 데몬이 도는 판(판정의 `from` sha) · 최근 `keep` 판(롤백용).
|
|
@@ -664,6 +738,12 @@ export interface ReleaseUpdateDeps {
|
|
|
664
738
|
listPruneProcesses?: ProcessListing;
|
|
665
739
|
/** Per-process cwd/executable probe; when absent the host's lsof is used. */
|
|
666
740
|
scanPruneProcess?: ProcessScan;
|
|
741
|
+
/** Shadow-only rail-safe process listing override. */
|
|
742
|
+
listRailSafeProcesses?: (pattern: string) => RailSafeProcessListing;
|
|
743
|
+
/** Resolves pgrep's PID-only output to a command line; defaults to `ps -p <pid> -o command=`. */
|
|
744
|
+
readRailSafeProcessCommand?: (pid: string) => string | null;
|
|
745
|
+
/** Shadow-only release-run detector override. */
|
|
746
|
+
isReleaseRunActive?: () => boolean;
|
|
667
747
|
/** 릴리스 기준 URL 주입 — 환경변수와 설치 출처보다 우선한다. */
|
|
668
748
|
releaseBase?: string;
|
|
669
749
|
}
|
|
@@ -740,10 +820,14 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
740
820
|
}
|
|
741
821
|
let previous = '';
|
|
742
822
|
let previousBuild = '';
|
|
823
|
+
let previousCommit = '';
|
|
743
824
|
let replacedDev: { from: string; to: string } | null = null;
|
|
744
825
|
try {
|
|
745
826
|
const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
|
|
746
827
|
previousBuild = installedBuild(metadata);
|
|
828
|
+
if (typeof metadata === 'object' && metadata !== null && typeof (metadata as { commit?: unknown }).commit === 'string') {
|
|
829
|
+
previousCommit = (metadata as { commit: string }).commit;
|
|
830
|
+
}
|
|
747
831
|
if (typeof metadata === 'object' && metadata !== null && (metadata as { channel?: unknown }).channel === 'dev') {
|
|
748
832
|
const fromDir = typeof (metadata as { versionDir?: unknown }).versionDir === 'string' ? (metadata as { versionDir: string }).versionDir : '';
|
|
749
833
|
replacedDev = { from: fromDir, to: '' };
|
|
@@ -757,6 +841,26 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
757
841
|
const result = spawnSync(command, args, { cwd, encoding: 'utf8', input, maxBuffer: SPAWN_MAX_BUFFER, env: { ...process.env, ...env, PATH: childPath() } });
|
|
758
842
|
return { status: result.status, stderr: result.error?.message ?? result.stderr ?? '' };
|
|
759
843
|
});
|
|
844
|
+
let railSafeEntries: Array<{ name: string; mtimeMs: number }> = [];
|
|
845
|
+
try {
|
|
846
|
+
railSafeEntries = readdirSync(join(prefix, 'versions'), { withFileTypes: true })
|
|
847
|
+
.filter((entry) => entry.isDirectory())
|
|
848
|
+
.map((entry) => ({ name: entry.name, mtimeMs: statSync(join(prefix, 'versions', entry.name)).mtimeMs }));
|
|
849
|
+
} catch { /* shadow observation cannot change the install decision */ }
|
|
850
|
+
let railSafe: ReturnType<typeof assessRailSafe> = { wouldBlock: false, reasons: [], heldVersions: [], releaseRunActive: false };
|
|
851
|
+
try {
|
|
852
|
+
railSafe = assessRailSafe({
|
|
853
|
+
entries: railSafeEntries,
|
|
854
|
+
current: previous,
|
|
855
|
+
daemonSha: previousCommit,
|
|
856
|
+
keep: options.keep ?? 3,
|
|
857
|
+
versionsDir: join(prefix, 'versions'),
|
|
858
|
+
...(deps.listRailSafeProcesses ? { listProcesses: deps.listRailSafeProcesses } : {}),
|
|
859
|
+
...(deps.readRailSafeProcessCommand ? { readProcessCommand: deps.readRailSafeProcessCommand } : {}),
|
|
860
|
+
...(deps.isReleaseRunActive ? { isReleaseRunActive: deps.isReleaseRunActive } : {}),
|
|
861
|
+
});
|
|
862
|
+
} catch { /* shadow observation cannot change the install decision */ }
|
|
863
|
+
try { debug.log('self-update.rail-safe', 'shadow', railSafe); } catch { /* shadow observation never changes update decisions */ }
|
|
760
864
|
try {
|
|
761
865
|
const install = run('bash', ['-s', '--', '--no-modify-path', '--prefix', prefix], prefix, installer, envLiteral({ ELANOUS_VERSION: version ?? '', ELANOUS_INSTALL_SOURCE: '', ELANOUS_RELEASE_BASE: base }));
|
|
762
866
|
if (install.status !== 0) return fail(1, `설치 실패: ${install.stderr.trim() || install.status}`);
|