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
|
@@ -5,6 +5,9 @@ import { effectiveInstanceRoot, prodInstanceRoot, treeDerivedRootFor } from '../
|
|
|
5
5
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
6
|
import { buildUserConfig, getUserConfig, type UserConfig } from '../user-config.js';
|
|
7
7
|
import { resolveCodexQuotaPolicy } from '../oauth/codex-quota-policy.js';
|
|
8
|
+
import { debug } from '../debug/log.js';
|
|
9
|
+
import type { PoolLeaseRecommendation } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
10
|
+
import type { PodPoolMember, PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
8
11
|
|
|
9
12
|
/** Key-order-insensitive JSON so equal per-account caps written in another order do not warn. */
|
|
10
13
|
function stableJson(value: unknown): string {
|
|
@@ -79,22 +82,154 @@ export interface ResolvedHarnessSubstrate {
|
|
|
79
82
|
source: 'flag' | 'config' | 'default';
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
/**
|
|
85
|
+
/** Conservative local headroom for one standard harness goal (the Pod lease's default per-goal size). */
|
|
86
|
+
export const AUTO_LOCAL_MIN_AVAILABLE_MEMORY_BYTES = 16 * 1024 ** 3;
|
|
87
|
+
|
|
88
|
+
export interface AutoSubstrateSignals {
|
|
89
|
+
/** A blocked memory admission has recommended=0 and limitedBy=memory; unknown is not blocked. */
|
|
90
|
+
podAdmission: Pick<PoolLeaseRecommendation, 'recommended' | 'limitedBy'> | null;
|
|
91
|
+
load1: number | null;
|
|
92
|
+
cores: number | null;
|
|
93
|
+
availableMemoryBytes: number | null;
|
|
94
|
+
localLaneOpen: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface AutoSubstrateDecision {
|
|
98
|
+
substrate: HarnessSubstrate;
|
|
99
|
+
reason: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface NextLocalLaneCapInput {
|
|
103
|
+
current: number;
|
|
104
|
+
wanted: number;
|
|
105
|
+
lastRaisedAtMs: number | null;
|
|
106
|
+
nowMs: number;
|
|
107
|
+
load1: number | null;
|
|
108
|
+
cores: number | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface NextLocalLaneCapDecision {
|
|
112
|
+
cap: number;
|
|
113
|
+
action: 'raise' | 'hold' | 'lower';
|
|
114
|
+
reason: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Decide one conservative local-lane cap adjustment from the current observations. */
|
|
118
|
+
export function nextLocalLaneCap({ current, wanted, lastRaisedAtMs, nowMs, load1, cores }: NextLocalLaneCapInput): NextLocalLaneCapDecision {
|
|
119
|
+
let decision: NextLocalLaneCapDecision;
|
|
120
|
+
if (load1 === null || cores === null || !Number.isFinite(load1) || !Number.isFinite(cores) || load1 < 0 || cores <= 0) {
|
|
121
|
+
decision = { cap: current, action: 'hold', reason: 'load-unreadable' };
|
|
122
|
+
} else if (load1 >= cores) {
|
|
123
|
+
decision = { cap: Math.max(0, current - 5), action: 'lower', reason: 'local-load-overloaded' };
|
|
124
|
+
} else if (!Number.isFinite(current) || !Number.isFinite(wanted) || !Number.isFinite(nowMs)) {
|
|
125
|
+
decision = { cap: current, action: 'hold', reason: 'cap-or-time-unreadable' };
|
|
126
|
+
} else if (wanted <= current) {
|
|
127
|
+
decision = { cap: current, action: 'hold', reason: 'no-increase-requested' };
|
|
128
|
+
} else if (load1 >= cores / 2) {
|
|
129
|
+
decision = { cap: current, action: 'hold', reason: 'local-load-not-spare' };
|
|
130
|
+
} else if (lastRaisedAtMs !== null && (!Number.isFinite(lastRaisedAtMs) || nowMs - lastRaisedAtMs < 15 * 60 * 1000)) {
|
|
131
|
+
decision = { cap: current, action: 'hold', reason: 'raise-cooldown' };
|
|
132
|
+
} else {
|
|
133
|
+
decision = { cap: Math.min(wanted, current + 5), action: 'raise', reason: 'local-load-spare' };
|
|
134
|
+
}
|
|
135
|
+
debug.log('harness.guardian', 'lane-step', { current, wanted, lastRaisedAtMs, nowMs, load1, cores, ...decision });
|
|
136
|
+
return decision;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Pure routing judgment. Missing/invalid observations are not evidence that the host has room. */
|
|
140
|
+
export function routeAutoSubstrate(signals: AutoSubstrateSignals): AutoSubstrateDecision {
|
|
141
|
+
const { podAdmission, load1, cores, availableMemoryBytes, localLaneOpen } = signals;
|
|
142
|
+
if (podAdmission === null || podAdmission.recommended === null || podAdmission.limitedBy === null)
|
|
143
|
+
return { substrate: 'pod', reason: 'pod-admission-unobserved' };
|
|
144
|
+
if (podAdmission.recommended !== 0 || podAdmission.limitedBy !== 'memory')
|
|
145
|
+
return { substrate: 'pod', reason: 'pod-admission-not-memory-blocked' };
|
|
146
|
+
if (!localLaneOpen) return { substrate: 'pod', reason: 'local-lane-closed' };
|
|
147
|
+
if (cores === null || !Number.isFinite(cores) || cores <= 0 || load1 === null || !Number.isFinite(load1) || load1 < 0 || load1 >= cores / 2)
|
|
148
|
+
return { substrate: 'pod', reason: 'local-load-not-spare' };
|
|
149
|
+
if (availableMemoryBytes === null || !Number.isFinite(availableMemoryBytes) || availableMemoryBytes < AUTO_LOCAL_MIN_AVAILABLE_MEMORY_BYTES)
|
|
150
|
+
return { substrate: 'pod', reason: 'local-memory-not-spare' };
|
|
151
|
+
return { substrate: 'local', reason: 'pod-memory-blocked-local-spare' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* POD-POOL-UNCAPPED-SPEC — the pre-launch pool check of an «implicit» Pod pool (the current kube context),
|
|
156
|
+
* reported in context names. `unmeasured` = kubectl itself could not run, so nothing was measured:
|
|
157
|
+
* «unknown» is not «unfit» (same rule as LAUNCH-STALL's unreadable member memory).
|
|
158
|
+
*/
|
|
159
|
+
export interface HarnessPoolCheck {
|
|
160
|
+
ok: boolean;
|
|
161
|
+
ready: string[];
|
|
162
|
+
dropped: { context: string; reason: string }[];
|
|
163
|
+
unmeasured?: string | null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `checkPodPool` over one pool spec — a kube context that cannot answer before launch is refused, not silently kept. */
|
|
167
|
+
export function checkHarnessPodPool(pool: string, run?: PoolKubectl): HarnessPoolCheck {
|
|
168
|
+
// Lazy require: the Pod surface pulls `bun:ffi`, and only a Pod launch without an explicit pool needs it.
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
170
|
+
const surface = require('../task-orchestrator/surfaces/pod-pool.js') as typeof import('../task-orchestrator/surfaces/pod-pool.js');
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
172
|
+
const leaseKubectl = run ?? (require('../task-orchestrator/surfaces/pod-lease.js') as typeof import('../task-orchestrator/surfaces/pod-lease.js')).leaseKubectl;
|
|
173
|
+
let members: PodPoolMember[];
|
|
174
|
+
try { members = surface.parsePodPool(pool); }
|
|
175
|
+
catch (error) {
|
|
176
|
+
return { ok: false, ready: [], dropped: [{ context: pool, reason: error instanceof Error ? error.message : String(error) }] };
|
|
177
|
+
}
|
|
178
|
+
let calls = 0;
|
|
179
|
+
let unrunnable = 0;
|
|
180
|
+
const kubectl: PoolKubectl = (args, input) => {
|
|
181
|
+
const r = leaseKubectl(args, input);
|
|
182
|
+
calls++;
|
|
183
|
+
// Only «kubectl is not installed» (spawn ENOENT) is no verdict about the cluster. A kubectl that started but
|
|
184
|
+
// timed out (status null · ETIMEDOUT) is a cluster that did not answer — that stays a measured refusal.
|
|
185
|
+
if (r.status === null && /\bENOENT\b/.test(r.stderr)) unrunnable++;
|
|
186
|
+
return r;
|
|
187
|
+
};
|
|
188
|
+
const checked = surface.checkPodPool(members, kubectl);
|
|
189
|
+
if (!checked.ok && calls > 0 && unrunnable === calls) return { ok: true, ready: [], dropped: checked.dropped, unmeasured: 'kubectl 을 실행할 수 없다' };
|
|
190
|
+
return { ok: checked.ok, ready: checked.ready.map((m) => m.context), dropped: checked.dropped };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Only inspect the current context when no explicit pool is available. Never turn an explicit Pod selection into local. */
|
|
83
194
|
export function resolveHarnessSubstrate({ flag, config, env, currentContext = () => {
|
|
84
195
|
const result = spawnSync('kubectl', ['config', 'current-context'], { encoding: 'utf8', timeout: 5000 });
|
|
85
196
|
return result.status === 0 ? result.stdout?.trim() : undefined;
|
|
86
|
-
} }: {
|
|
87
|
-
flag?: { substrate?: HarnessSubstrate; podPool?: string };
|
|
197
|
+
}, autoSignals, poolCheck = checkHarnessPodPool }: {
|
|
198
|
+
flag?: { substrate?: HarnessSubstrate | 'auto'; podPool?: string };
|
|
88
199
|
config?: Pick<UserConfig, 'harness' | 'pod'>;
|
|
89
200
|
env?: NodeJS.ProcessEnv;
|
|
90
201
|
currentContext?: () => string | undefined;
|
|
202
|
+
/** Signals must come from the launch host and Pod admission, not a worktree's cached config. */
|
|
203
|
+
autoSignals?: AutoSubstrateSignals;
|
|
204
|
+
/** POD-POOL-UNCAPPED-SPEC — pre-launch check of the implicit kube context; tests inject it instead of shelling out. */
|
|
205
|
+
poolCheck?: (pool: string) => HarnessPoolCheck;
|
|
91
206
|
}): ResolvedHarnessSubstrate {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
207
|
+
const requested = flag?.substrate ?? config?.harness?.substrate ?? 'local';
|
|
208
|
+
if (requested !== 'local' && requested !== 'pod' && requested !== 'auto') throw new Error('harness.substrate: local, pod 또는 auto 만 허용한다');
|
|
94
209
|
const source = flag?.substrate ? 'flag' : config?.harness?.substrate ? 'config' : 'default';
|
|
210
|
+
// Missing signals mean admission was not observed, not that the Pod has room.
|
|
211
|
+
const decision = requested === 'auto' ? routeAutoSubstrate(autoSignals ?? {
|
|
212
|
+
podAdmission: null, load1: null, cores: null, availableMemoryBytes: null, localLaneOpen: false,
|
|
213
|
+
}) : null;
|
|
214
|
+
const substrate: HarnessSubstrate = requested === 'auto' ? decision!.substrate : requested;
|
|
215
|
+
if (decision) debug.log('harness.substrate', 'route', { substrate, reason: decision.reason, source });
|
|
95
216
|
if (substrate === 'local') return { substrate, pool: null, source };
|
|
96
|
-
const
|
|
97
|
-
|
|
217
|
+
const explicit = flag?.podPool?.trim() || env?.ELANOUS_POD_POOL?.trim() || config?.harness?.podPool?.trim() || config?.pod?.pool?.trim();
|
|
218
|
+
const pool = explicit || currentContext()?.trim();
|
|
219
|
+
if (!pool) throw new Error(`harness.substrate=${decision ? `auto→pod(${decision.reason})` : 'pod'}: Pod 풀 또는 현재 컨텍스트에 닿지 못했다 — 풀을 설정하거나 \`--substrate local\` 로 명시하라`);
|
|
220
|
+
// POD-POOL-UNCAPPED-SPEC — an explicit pool is the operator's own statement and gets `checkPodPool` at the launch
|
|
221
|
+
// site (`src/index.ts` self orchestrate · `src/loops/orchestrator/tick.ts` · `pod-command-job.ts`). The implicit
|
|
222
|
+
// kube context had no such gate: a context whose cluster is gone, or whose `elanous-test` namespace is missing,
|
|
223
|
+
// sailed through here and then sat on `recommended=0` for 35 minutes (10-10 probe, 6 runs). Check before launch.
|
|
224
|
+
if (!explicit) {
|
|
225
|
+
const checked = poolCheck(pool);
|
|
226
|
+
debug.log('harness.substrate', 'pool-check', { pool, origin: 'current-context', ok: checked.ok, ready: checked.ready, dropped: checked.dropped, unmeasured: checked.unmeasured ?? null });
|
|
227
|
+
if (!checked.ok) {
|
|
228
|
+
const why = checked.dropped.map((d) => `${d.context}: ${d.reason}`).join(' · ') || '노드가 하나도 준비되지 않았다';
|
|
229
|
+
throw new Error(`harness.substrate=pod: 현재 컨텍스트 「${pool}」 이 발사 전 풀 점검을 넘지 못했다 — ${why} · 풀을 설정하거나 \`--substrate local\` 로 명시하라`);
|
|
230
|
+
}
|
|
231
|
+
if (checked.unmeasured) debug.log('harness.substrate', 'pool-check-unmeasured', { pool, reason: checked.unmeasured }, { level: 'warn' });
|
|
232
|
+
}
|
|
98
233
|
return { substrate, pool, source };
|
|
99
234
|
}
|
|
100
235
|
|
|
@@ -19,6 +19,10 @@ export interface LaunchBinding {
|
|
|
19
19
|
readonly goalPath?: string;
|
|
20
20
|
/** The `self-dev-runs` directory that holds this run's `pid.json` — tells a stop which universe launched it. */
|
|
21
21
|
readonly runsDir?: string;
|
|
22
|
+
/** `ELANOUS_POD_NAME` at launch — a Pod child's run (TA backfill skips it like the launch-time hook does). */
|
|
23
|
+
readonly podName?: string;
|
|
24
|
+
/** `ELANOUS_TASK_CARD_ID` at launch — a handed run that already owns a card (TA backfill skips it). */
|
|
25
|
+
readonly taskCardId?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
@@ -72,6 +76,8 @@ export function readLaunchBinding(runId: string, dir = launchRegistryDir()): Lau
|
|
|
72
76
|
...(typeof value.argv0 === 'string' ? { argv0: value.argv0 } : {}),
|
|
73
77
|
...(typeof value.goalPath === 'string' && isAbsolute(value.goalPath) ? { goalPath: value.goalPath } : {}),
|
|
74
78
|
...(typeof value.runsDir === 'string' && isAbsolute(value.runsDir) ? { runsDir: value.runsDir } : {}),
|
|
79
|
+
...(typeof value.podName === 'string' && value.podName ? { podName: value.podName } : {}),
|
|
80
|
+
...(typeof value.taskCardId === 'string' && value.taskCardId ? { taskCardId: value.taskCardId } : {}),
|
|
75
81
|
};
|
|
76
82
|
} catch { return null; }
|
|
77
83
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { branchGoalId } from '../cli/pr-lineage.js';
|
|
2
3
|
|
|
3
4
|
/** Prefix for disposable worktree branches created by self-implement and inspected by harness clean.
|
|
4
5
|
* A successfully merged run cleans up its own worktree.
|
|
@@ -31,3 +32,11 @@ export function plannedSelfImplBranch(feature: string, goalId?: string, runId?:
|
|
|
31
32
|
const digest = createHash('sha256').update(id).digest('hex').slice(0, 8);
|
|
32
33
|
return `${WORKTREE_BRANCH_PREFIX}goalid-${id}-${readable}-${digest}${runSuffix}`;
|
|
33
34
|
}
|
|
35
|
+
|
|
36
|
+
/** 같은 골의 재실행이 공유하는 «안정한» 계보 키. `plannedSelfImplBranch` 가 싣는 `goalid-<id>-` 구간만 읽는다 —
|
|
37
|
+
* feature 문구·다이제스트·런 접미(`-r<runId>`)는 재실행마다 달라지므로 키에 넣지 않는다.
|
|
38
|
+
* ⛔ 골 식별자가 없는 옛 브랜치는 `null` — 읽기용 슬러그는 서로 다른 골이 같은 값을 낼 수 있어(`grounded-N-…`) 키가 될 수 없다. */
|
|
39
|
+
export function branchLineageKey(branch: string): string | null {
|
|
40
|
+
const id = branchGoalId(branch);
|
|
41
|
+
return id ? `goalid:${id.toLowerCase()}` : null;
|
|
42
|
+
}
|
package/src/hq/fence.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, readlinkSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { cronMatches } from '../domains/cron-match.js';
|
|
4
|
+
import { resolveTimeZone } from '../time/format.js';
|
|
5
|
+
|
|
6
|
+
export interface CliProcess { pid: number; ppid: number; ageSeconds: number; cpuSeconds: number; command: string }
|
|
7
|
+
export interface OrphanCandidate { process: CliProcess; tree: CliProcess[]; periodMinutes: number }
|
|
8
|
+
export interface FenceGcDeps {
|
|
9
|
+
snapshot?: () => CliProcess[];
|
|
10
|
+
run?: (binary: string, args: string[]) => string;
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
kill?: (pid: number) => void;
|
|
13
|
+
timeZone?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function cliFingerprint(command: string): string {
|
|
17
|
+
return command.replace(/^\s*(?:\S*\/)?bun\s+(?:\S*\/)?elanous\.mjs\s+/, 'elanous ')
|
|
18
|
+
.replace(/^\s*(?:\S*\/)?(?:elanous\.mjs|eln)\s+/, 'elanous ')
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function seconds(text: string): number {
|
|
23
|
+
const parts = text.split(':').map(Number);
|
|
24
|
+
return parts.reduce((total, part) => total * 60 + part, 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** ps gives command after the four numeric fields; never split command on whitespace. */
|
|
28
|
+
export function processSnapshot(): CliProcess[] {
|
|
29
|
+
const result = spawnSync('ps', ['-axo', 'pid=,ppid=,etime=,time=,command='], { encoding: 'utf8', timeout: 10_000, maxBuffer: 4 * 1024 * 1024 });
|
|
30
|
+
if (result.status !== 0) throw new Error(`ps failed: ${result.stderr}`);
|
|
31
|
+
return result.stdout.split('\n').flatMap(line => {
|
|
32
|
+
const fields = /^\s*(\d+)\s+(\d+)\s+([\d:-]+)\s+([\d:.]+)\s+(.+)$/.exec(line);
|
|
33
|
+
if (!fields) return [];
|
|
34
|
+
const elapsed = fields[3]!.split('-');
|
|
35
|
+
const ageSeconds = seconds(elapsed.at(-1)!) + (elapsed.length === 2 ? Number(elapsed[0]) * 86_400 : 0);
|
|
36
|
+
const cpuSeconds = seconds(fields[4]!);
|
|
37
|
+
if (!Number.isFinite(ageSeconds) || !Number.isFinite(cpuSeconds)) return [];
|
|
38
|
+
return [{ pid: Number(fields[1]), ppid: Number(fields[2]), ageSeconds, cpuSeconds, command: fields[5]! }];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Only installed elanous one-shot CLI launches, not shells, daemons, or an incidental flag inside a quoted payload. */
|
|
43
|
+
export function oneShotCommand(command: string): boolean {
|
|
44
|
+
const invocation = /^(?:(?:\S*\/)?bun\s+(?:\S*\/)?elanous\.mjs|(?:\S*\/)?(?:elanous|elanous\.mjs|eln))\s+/.exec(command);
|
|
45
|
+
if (!invocation) return false;
|
|
46
|
+
const args = command.slice(invocation[0].length).split(/\s+--\s+|\s+-c\s+/)[0] ?? '';
|
|
47
|
+
const payload = /\s+--\s+(.+)$/.exec(command)?.[1] ?? '';
|
|
48
|
+
const shellBody = /\s+--\s+(?:\S*\/)?(?:sh|bash)\s+-c\s+(.+)$/.exec(command)?.[1];
|
|
49
|
+
if (shellBody !== undefined) {
|
|
50
|
+
if (!/^['"]?(?:\S*\/)?bun\s+(?:\S*\/)?elanous\.mjs\s+/.test(shellBody)
|
|
51
|
+
|| /[;|`&]/.test(shellBody) || /\bnexus\s+run\b/.test(shellBody)
|
|
52
|
+
|| !/(?:^|\s)(?:--once|tick|--refresh|--notify)(?:\s|['"]|$)/.test(shellBody)) return false;
|
|
53
|
+
}
|
|
54
|
+
// Test-universe and help invocations are never collected — on the fence itself, in its payload, or in a wrapped shell body.
|
|
55
|
+
for (const part of [args, payload, shellBody ?? '']) {
|
|
56
|
+
if (/(?:^|\s|['"])--test(?:\s|=|['"]|$)/.test(part) || /(?:^|\s)(?:--help|-h)(?:\s|['"]|$)/.test(part)) return false;
|
|
57
|
+
}
|
|
58
|
+
// The daemon is never a one-shot, whatever flag rides along (`nexus run --once`), directly or as a fence payload.
|
|
59
|
+
if (/(?:^|\s)nexus\s+run\b/.test(args) || /(?:^|\s)nexus\s+run\b/.test(payload) || /&&|\|\||[;|`&]/.test(payload)) return false;
|
|
60
|
+
if (/^hq\s+fence\s+--role\s+(?:cron|seat-loop|conatus|release-run)\b/.test(args) && payload) {
|
|
61
|
+
return /(?:^|\s)(?:--once|tick|--refresh|--notify)(?:\s|['"]|$)/.test(payload);
|
|
62
|
+
}
|
|
63
|
+
return /(?:^|\s)hq\s+fence\s+--role\s+(?:cron|seat-loop|conatus|release-run)\b/.test(args)
|
|
64
|
+
|| /(?:^|\s)--once(?:\s|$)/.test(args)
|
|
65
|
+
|| /(?:^|\s)(?:tick|--refresh|--notify)(?:\s|$)/.test(args);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Unknown/mismatched schedules are not assigned a made-up period. Match the launched action (including fence payload). */
|
|
69
|
+
export function scheduledPeriodMinutes(command: string, schedules: readonly { command: string; cron: string }[], now: Date, timeZone = resolveTimeZone().timeZone): number | null {
|
|
70
|
+
const current = cliFingerprint(command);
|
|
71
|
+
const matches = schedules.filter(job => {
|
|
72
|
+
const action = job.command.trim().replace(/^cd\s+\S+\s*&&\s*/, '').replace(/\s+>>?\s+\S+(?:\s+2>&1)?\s*$/, '');
|
|
73
|
+
if (action.length < 16) return false;
|
|
74
|
+
if (current === cliFingerprint(action)) return true;
|
|
75
|
+
// The installed hq-fence shell wrapper execs the CLI with an explicit role and -- payload.
|
|
76
|
+
// Accept only a single, unambiguous quoted command; never infer a period from a substring.
|
|
77
|
+
const wrapper = /^(?:\S*\/)?hq-fence\s+(cron|seat-loop|conatus|release-run)\s+'([^']+)'$/.exec(action);
|
|
78
|
+
if (!wrapper) return false;
|
|
79
|
+
return current === `elanous hq fence --role ${wrapper[1]} -- ${wrapper[2]}`;
|
|
80
|
+
});
|
|
81
|
+
if (matches.length !== 1) return null;
|
|
82
|
+
const cron = matches[0]!.cron;
|
|
83
|
+
if (cron.trim().split(/\s+/).length !== 5) return null;
|
|
84
|
+
const start = Math.floor(now.getTime() / 60_000) * 60_000;
|
|
85
|
+
// A 7-day minute scan per process is costly with many hung trees; one tick asks the same (cron, zone, minute) often.
|
|
86
|
+
const memoKey = `${cron}\0${timeZone}\0${start}`;
|
|
87
|
+
if (periodMemo.has(memoKey)) return periodMemo.get(memoKey)!;
|
|
88
|
+
const period = cronPeriodMinutes(cron, start, timeZone);
|
|
89
|
+
if (periodMemo.size > 256) periodMemo.clear();
|
|
90
|
+
periodMemo.set(memoKey, period);
|
|
91
|
+
return period;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const periodMemo = new Map<string, number | null>();
|
|
95
|
+
|
|
96
|
+
function cronPeriodMinutes(cron: string, start: number, timeZone: string): number | null {
|
|
97
|
+
const fires: number[] = [];
|
|
98
|
+
try {
|
|
99
|
+
// Every fire in the next 7 days, not just the first few: a weekday-hours cron (`0 9-17 * * 1-5`) is hourly
|
|
100
|
+
// for three gaps and then pauses for a night or a weekend — that is no single period.
|
|
101
|
+
for (let i = 0; i < 7 * 24 * 60; i++) {
|
|
102
|
+
const at = start + i * 60_000;
|
|
103
|
+
if (cronMatches(cron, new Date(at), { timeZone })) fires.push(at);
|
|
104
|
+
}
|
|
105
|
+
} catch { return null; }
|
|
106
|
+
// An irregular schedule (any unequal gap within the window, e.g. `0 9,18 * * *`, weekday hours) has no single period
|
|
107
|
+
// to multiply — unknown, never collected. Fewer than three fires in 7 days (weekly) is unknown too (safe side).
|
|
108
|
+
if (fires.length < 3) return null;
|
|
109
|
+
const gap = fires[1]! - fires[0]!;
|
|
110
|
+
for (let i = 2; i < fires.length; i++) if (fires[i]! - fires[i - 1]! !== gap) return null;
|
|
111
|
+
return gap / 60_000;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function orphanCandidates(first: readonly CliProcess[], second: readonly CliProcess[], schedules: readonly { command: string; cron: string }[], now: Date, timeZone?: string): OrphanCandidate[] {
|
|
115
|
+
const byPid = new Map(second.map(row => [row.pid, row]));
|
|
116
|
+
const initial = new Map(first.map(row => [row.pid, row]));
|
|
117
|
+
const candidates: OrphanCandidate[] = [];
|
|
118
|
+
// The collector itself and every ancestor of it (the guardian tick's cron/fence parents) are never signalled.
|
|
119
|
+
const self = new Set<number>([process.pid]);
|
|
120
|
+
for (let pid: number | undefined = byPid.get(process.pid)?.ppid; pid !== undefined && pid > 1 && !self.has(pid); pid = byPid.get(pid)?.ppid) self.add(pid);
|
|
121
|
+
for (const root of first) {
|
|
122
|
+
if (root.pid === process.pid || !oneShotCommand(root.command)) continue;
|
|
123
|
+
const current = byPid.get(root.pid);
|
|
124
|
+
if (!current || current.command !== root.command || current.ppid !== root.ppid || current.cpuSeconds !== root.cpuSeconds) continue;
|
|
125
|
+
// Identity: a reused PID restarts its age; the same process can only have grown older between snapshots.
|
|
126
|
+
if (current.ageSeconds < root.ageSeconds) continue;
|
|
127
|
+
const periodMinutes = scheduledPeriodMinutes(root.command, schedules, now, timeZone);
|
|
128
|
+
if (!periodMinutes || root.ageSeconds <= periodMinutes * 3 * 60) continue;
|
|
129
|
+
// An old ancestor with an actively executing payload is not an orphan.
|
|
130
|
+
if (second.some(row => row.ppid === root.pid && !initial.has(row.pid))) continue;
|
|
131
|
+
const tree = [current];
|
|
132
|
+
for (let i = 0; i < tree.length; i++) {
|
|
133
|
+
for (const child of second) if (child.ppid === tree[i]!.pid && !tree.some(row => row.pid === child.pid)) tree.push(child);
|
|
134
|
+
}
|
|
135
|
+
if (tree.some(row => {
|
|
136
|
+
const before = initial.get(row.pid);
|
|
137
|
+
return !before || before.command !== row.command || before.cpuSeconds !== row.cpuSeconds || before.ppid !== row.ppid
|
|
138
|
+
|| Math.abs((row.ageSeconds - before.ageSeconds) - (current.ageSeconds - root.ageSeconds)) > 2;
|
|
139
|
+
})) continue;
|
|
140
|
+
if (tree.some(row => self.has(row.pid))) continue;
|
|
141
|
+
candidates.push({ process: current, tree, periodMinutes });
|
|
142
|
+
}
|
|
143
|
+
return candidates.filter(candidate => !candidates.some(parent => parent !== candidate && parent.tree.some(row => row.pid === candidate.process.pid)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function gcTools(deps: FenceGcDeps = {}) {
|
|
147
|
+
return {
|
|
148
|
+
snapshot: deps.snapshot ?? processSnapshot,
|
|
149
|
+
sleep: deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))),
|
|
150
|
+
run: deps.run ?? ((binary: string, args: string[]) => {
|
|
151
|
+
// Linux has no macOS sample. Over one second, sample each thread's wait channel and state (readable by the
|
|
152
|
+
// process owner); the kernel stack is root-only on most hosts, so it is added only when readable.
|
|
153
|
+
if (binary === 'sample' && process.platform === 'linux') {
|
|
154
|
+
const pid = args[0];
|
|
155
|
+
const samples: string[] = [];
|
|
156
|
+
const deadline = Date.now() + 1_000;
|
|
157
|
+
do {
|
|
158
|
+
for (const tid of readdirSync(`/proc/${pid}/task`)) {
|
|
159
|
+
const base = `/proc/${pid}/task/${tid}`;
|
|
160
|
+
const state = /^State:\s*(.+)$/m.exec(readFileSync(`${base}/status`, 'utf8'))?.[1] ?? '?';
|
|
161
|
+
let stack = '';
|
|
162
|
+
// Root-only on most kernels: carried when readable, otherwise the evidence says so (not a collection gate).
|
|
163
|
+
try { stack = `\n${readFileSync(`${base}/stack`, 'utf8')}`; } catch (error) { stack = ` kernel-stack=unreadable(${(error as NodeJS.ErrnoException).code ?? 'error'})`; }
|
|
164
|
+
samples.push(`tid=${tid} state=${state} wchan=${readFileSync(`${base}/wchan`, 'utf8') || '0'}${stack}`);
|
|
165
|
+
}
|
|
166
|
+
if (Date.now() < deadline) Bun.sleepSync(Math.min(100, deadline - Date.now()));
|
|
167
|
+
} while (Date.now() < deadline);
|
|
168
|
+
if (samples.length === 0) throw new Error(`sample: no threads for PID ${pid}`);
|
|
169
|
+
return samples.join('\n');
|
|
170
|
+
}
|
|
171
|
+
// macOS `sample` by absolute path: a PATH shim (e.g. pyenv's `sample`) must not stand in for the profiler.
|
|
172
|
+
const resolved = binary === 'sample' && process.platform === 'darwin' ? '/usr/bin/sample' : binary;
|
|
173
|
+
// sample symbolicates a large Bun binary after its 1s window — allow time and room, or it would never succeed.
|
|
174
|
+
const result = spawnSync(resolved, args, { encoding: 'utf8', timeout: binary === 'sample' ? 30_000 : 10_000, maxBuffer: 8 * 1024 * 1024 });
|
|
175
|
+
if (result.status !== 0) throw new Error(`${binary}: ${result.error ?? result.stderr ?? 'failed'}`);
|
|
176
|
+
return result.stdout;
|
|
177
|
+
}),
|
|
178
|
+
kill: deps.kill ?? ((pid: number) => process.kill(pid, 'SIGTERM')),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The target's own Bun binary — read from the target PID, so a PID that died (or was reused) fails closed. */
|
|
183
|
+
export function bunVersion(pid: number, run: (binary: string, args: string[]) => string): string {
|
|
184
|
+
const bin = process.platform === 'linux' ? readlinkSync(`/proc/${pid}/exe`) : run('ps', ['-o', 'comm=', '-p', String(pid)]).trim();
|
|
185
|
+
if (!/(?:^|\/)bun(?:$|\s)/.test(bin)) throw new Error(`PID ${pid} is not a Bun process`);
|
|
186
|
+
return run(bin, ['--version']).trim();
|
|
187
|
+
}
|
package/src/hq/seats.ts
CHANGED
|
@@ -47,6 +47,12 @@ export function resolveHqLaunchSeat(role: string, home: string, options: { root?
|
|
|
47
47
|
if (head.status !== 1) throw new Error(`git symbolic-ref --quiet HEAD: ${head.stderr.trim() || `exit ${head.status}`}`);
|
|
48
48
|
return path;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/** Installed-CLI argv: absolute runtime + absolute CLI entry, so a launch never resolves `bin/elanous.mjs` against its cwd. */
|
|
52
|
+
export function installedCliArgv(args: readonly string[] = []): string[] {
|
|
53
|
+
return [process.execPath, resolve(import.meta.dir, '../../bin/elanous.mjs'), ...args];
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
function readLedger(root: string): HouseLedger {
|
|
51
57
|
const ledger = JSON.parse(readFileSync(ledgerPath(root), 'utf8')) as HouseLedger;
|
|
52
58
|
if (!ledger.home?.path || !ledger.sandboxes || typeof ledger.sandboxes !== 'object') throw new Error('invalid house.json; refusing to guess houses');
|
|
@@ -157,7 +163,7 @@ export function proveMergedPr(repo: string, branch: string, tip: string, gh: (ar
|
|
|
157
163
|
: { status: parsed.length >= MERGED_PR_QUERY_LIMIT ? 'unknown' : 'not-merged' };
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
//
|
|
166
|
+
// Attached tips use their branch; a detached HEAD is checked separately from the registered work tip.
|
|
161
167
|
function landedTip(repo: string, branch: string, tip: string, gh: (args: string[]) => GhCliResult): 'ancestor' | 'pr-merged' | 'tree-equal' | null {
|
|
162
168
|
if (mergedIntoMain(repo, tip)) return 'ancestor';
|
|
163
169
|
if (proveMergedPr(repo, branch, tip, gh, 'main').status === 'pr-merged') return 'pr-merged';
|
|
@@ -170,8 +176,8 @@ function judgeLandedTip(repo: string, branch: string, tip: string, gh: (args: st
|
|
|
170
176
|
return rule !== null;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
// Squash changes
|
|
174
|
-
//
|
|
179
|
+
// Squash changes commit identity. Check the changed paths together against a fetched main
|
|
180
|
+
// snapshot, not merely the latest tree: a later main commit may have edited those paths again.
|
|
175
181
|
function treeEqualToMain(repo: string, tip: string): boolean {
|
|
176
182
|
const main = 'refs/remotes/origin/main';
|
|
177
183
|
const base = git(repo, 'merge-base', tip, main);
|
|
@@ -179,9 +185,15 @@ function treeEqualToMain(repo: string, tip: string): boolean {
|
|
|
179
185
|
if (changed.status !== 0) throw new Error(`cannot verify changed files: ${changed.stderr.trim() || `exit ${changed.status}`}`);
|
|
180
186
|
const files = changed.stdout.split('\0').filter(Boolean);
|
|
181
187
|
if (files.length === 0) return false;
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
const equalAt = (commit: string): boolean => {
|
|
189
|
+
const equal = runGitCommand(repo, ['diff', '--quiet', tip, commit, '--', ...files], { encoding: 'utf8' });
|
|
190
|
+
if (equal.status !== 0 && equal.status !== 1) throw new Error(`cannot verify tree equality: ${equal.stderr.trim() || `exit ${equal.status}`}`);
|
|
191
|
+
return equal.status === 0;
|
|
192
|
+
};
|
|
193
|
+
if (equalAt(main)) return true;
|
|
194
|
+
// Only first-parent main snapshots can establish a previous landing; a side branch alone cannot.
|
|
195
|
+
const history = git(repo, 'log', '--first-parent', '--format=%H', `${base}..${main}`, '--', ...files);
|
|
196
|
+
return history.split('\n').filter(Boolean).some(equalAt);
|
|
185
197
|
}
|
|
186
198
|
// Git lock files are created exclusively; ours carry our pid. A lock left by a killed `work done` is never
|
|
187
199
|
// removed automatically (between our check and a delete, a live git could have re-taken it): the error names it.
|
|
@@ -398,49 +410,53 @@ export function hqWork(action: 'new' | 'done', value: string, nameValue: string,
|
|
|
398
410
|
// Resume: an interrupted done left the work detached at its recorded merged tip, with the branch either
|
|
399
411
|
// still at that tip (stopped before the delete) or already deleted (stopped before the removal).
|
|
400
412
|
const branchPresent = branchTip.status === 0;
|
|
401
|
-
const
|
|
402
|
-
|
|
413
|
+
const head = git(path, 'rev-parse', 'HEAD');
|
|
414
|
+
const resuming = attached.status === 1 && entry.reclaiming !== undefined
|
|
415
|
+
&& head === (entry.checkoutTip ?? entry.reclaiming)
|
|
403
416
|
&& (!branchPresent || branchTip.stdout.trim() === entry.reclaiming);
|
|
404
|
-
if (!resuming && (attached.status !== 0 || !branchPresent)) throw new Error(`work branch mismatch: ${path}`);
|
|
417
|
+
if (!resuming && (attached.status !== 0 && attached.status !== 1 || !branchPresent)) throw new Error(`work branch mismatch: ${path}`);
|
|
405
418
|
const ownTip = resuming ? entry.reclaiming! : branchTip.stdout.trim();
|
|
406
|
-
const
|
|
419
|
+
const detached = !resuming && attached.status === 1;
|
|
420
|
+
const detachedCheckout = detached || (resuming && entry.checkoutBranch === branch && entry.checkoutTip !== undefined && entry.checkoutTip !== ownTip);
|
|
421
|
+
const checkoutBranch = resuming ? entry.checkoutBranch ?? branch : detached ? branch : attached.stdout.trim();
|
|
407
422
|
const checkoutRef = `refs/heads/${checkoutBranch}`;
|
|
408
423
|
const differentBranch = checkoutBranch !== branch;
|
|
409
|
-
const tip = differentBranch
|
|
410
|
-
?
|
|
411
|
-
|
|
412
|
-
if (!resuming && differentBranch && git(path, 'rev-parse', 'HEAD') !== tip) throw new Error(`work branch moved before the merge check: ${checkoutBranch}`);
|
|
424
|
+
const tip = detached ? head : resuming && entry.checkoutTip ? entry.checkoutTip : differentBranch
|
|
425
|
+
? git(repo, 'rev-parse', '--verify', checkoutRef) : ownTip;
|
|
426
|
+
if (!resuming && !detached && differentBranch && head !== tip) throw new Error(`work branch moved before the merge check: ${checkoutBranch}`);
|
|
413
427
|
// The ancestor check is against origin/main, not bare HEAD (which may still point to master).
|
|
414
428
|
if (!judgeLandedTip(repo, checkoutBranch, tip, options.gh ?? runGhCliWithResult)) throw new Error(`work is not merged into origin/main: ${checkoutBranch}`);
|
|
415
|
-
if (differentBranch && !judgeLandedTip(repo, branch, ownTip, options.gh ?? runGhCliWithResult)) throw new Error(`work is not merged into origin/main: ${branch}`);
|
|
429
|
+
if ((differentBranch || detachedCheckout) && tip !== ownTip && !judgeLandedTip(repo, branch, ownTip, options.gh ?? runGhCliWithResult)) throw new Error(`work is not merged into origin/main: ${branch}`);
|
|
416
430
|
if (differentBranch && !resuming) {
|
|
417
431
|
const current = runGitCommand(repo, ['rev-parse', '--verify', checkoutRef], { encoding: 'utf8' });
|
|
418
432
|
if (current.status !== 0 || current.stdout.trim() !== tip || git(path, 'rev-parse', 'HEAD') !== tip) throw new Error(`work branch moved before removal: ${checkoutBranch}`);
|
|
419
433
|
}
|
|
420
434
|
// Removal order keeps every interruption resumable: record the proven checkout tip and original branch tip
|
|
421
435
|
// → detach → hold the work's git locks → compare-and-delete the original branch → remove the worktree
|
|
422
|
-
// → drop the registration last. A different attached branch is proven but its ref is never deleted
|
|
436
|
+
// → drop the registration last. A different attached branch is proven but its ref is never deleted;
|
|
437
|
+
// a pre-existing detached HEAD is checked independently and never reattached on a failed removal.
|
|
423
438
|
// Holding this worktree's HEAD.lock and index.lock makes git refuse every commit and `add` in it until the
|
|
424
439
|
// removal ends (`worktree remove` itself does not need them), so no commit can land between the last
|
|
425
440
|
// check and the removal. The locks live in the worktree's admin dir, which the removal deletes.
|
|
426
441
|
updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = { ...entry, reclaiming: ownTip,
|
|
427
|
-
...(differentBranch ? { checkoutTip: tip, checkoutBranch } : {}) }; });
|
|
442
|
+
...(differentBranch || (detachedCheckout && tip !== ownTip) ? { checkoutTip: tip, checkoutBranch } : {}) }; });
|
|
428
443
|
const admin = git(path, 'rev-parse', '--absolute-git-dir');
|
|
429
444
|
const held: string[] = [];
|
|
430
445
|
const release = (): void => { for (const lock of held.splice(0)) rmSync(lock, { force: true }); };
|
|
431
446
|
const restore = (): string => {
|
|
432
447
|
release();
|
|
433
448
|
const head = runGitCommand(path, ['rev-parse', '--verify', 'HEAD'], { encoding: 'utf8' });
|
|
434
|
-
runGitCommand(repo, ['update-ref', ref, differentBranch ? ownTip : head.status === 0 ? head.stdout.trim() : tip, ''], { encoding: 'utf8' });
|
|
435
|
-
const reattached =
|
|
436
|
-
|
|
449
|
+
runGitCommand(repo, ['update-ref', ref, differentBranch || detachedCheckout ? ownTip : head.status === 0 ? head.stdout.trim() : tip, ''], { encoding: 'utf8' });
|
|
450
|
+
const reattached = detachedCheckout ? head.status === 0 && head.stdout.trim() === tip
|
|
451
|
+
: runGitCommand(path, ['symbolic-ref', 'HEAD', checkoutRef], { encoding: 'utf8' }).status === 0;
|
|
452
|
+
// If HEAD could not be restored (e.g. a stale HEAD.lock), keep `reclaiming` so the next done resumes.
|
|
437
453
|
const restored = reattached ? { kind: entry.kind, path: entry.path, owner: entry.owner } : { ...entry, reclaiming: ownTip,
|
|
438
|
-
...(differentBranch ? { checkoutTip: tip, checkoutBranch } : {}) };
|
|
454
|
+
...(differentBranch || (detachedCheckout && tip !== ownTip) ? { checkoutTip: tip, checkoutBranch } : {}) };
|
|
439
455
|
try { updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = restored; }); return reattached ? 'registration restored' : 'left resumable'; }
|
|
440
456
|
catch (error) { return `REGISTRATION NOT RESTORED (fix ${path} in house.json by hand): ${error instanceof Error ? error.message : String(error)}`; }
|
|
441
457
|
};
|
|
442
458
|
const fail = (message: string): never => { throw new Error(`${message}; nothing removed: ${path} · ${restore()}`); };
|
|
443
|
-
if (!resuming) git(path, 'checkout', '--quiet', '--detach');
|
|
459
|
+
if (!resuming && !detached) git(path, 'checkout', '--quiet', '--detach');
|
|
444
460
|
for (const name of ['HEAD.lock', 'index.lock']) {
|
|
445
461
|
const lock = join(admin, name);
|
|
446
462
|
try { takeLock(lock); held.push(lock); }
|