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
|
@@ -155,11 +155,16 @@ export function registerGraphCommands(program: Command, runsDeps: { root?: strin
|
|
|
155
155
|
.description('Advance one graph run, resuming a pending decision or starting only when requested')
|
|
156
156
|
.option('--start', 'Start a new run when the graph is idle')
|
|
157
157
|
.option('--schedule', 'Subscribe graph YAML triggers.schedule to the workflow scheduler and tick on each fire')
|
|
158
|
+
.option('--webhook', 'Subscribe graph YAML triggers.webhook to the workflow webhook router and tick on each request')
|
|
159
|
+
.option('--port <n>', 'Port for --webhook to listen on (default: any free port)')
|
|
158
160
|
.option('--input <json>', 'JSON object passed to a newly started run')
|
|
159
161
|
.option('--json', 'Print the tick result as JSON')
|
|
160
|
-
.action(async (file: string, opts: { start?: boolean; schedule?: boolean; input?: string; json?: boolean }) => {
|
|
162
|
+
.action(async (file: string, opts: { start?: boolean; schedule?: boolean; webhook?: boolean; port?: string; input?: string; json?: boolean }) => {
|
|
161
163
|
try {
|
|
162
164
|
if (opts.schedule && (opts.start || opts.input !== undefined || opts.json)) throw new Error('--schedule cannot be combined with --start, --input or --json');
|
|
165
|
+
if (opts.webhook && (opts.schedule || opts.start || opts.input !== undefined || opts.json)) throw new Error('--webhook cannot be combined with --schedule, --start, --input or --json');
|
|
166
|
+
if (opts.port !== undefined && !opts.webhook) throw new Error('--port requires --webhook');
|
|
167
|
+
if (opts.port !== undefined && (!/^\d+$/.test(opts.port) || Number(opts.port) > 65535)) throw new Error('--port must be an integer from 0 to 65535');
|
|
163
168
|
if (opts.input !== undefined && !opts.start) throw new Error('--input requires --start');
|
|
164
169
|
let input: Record<string, unknown> | undefined;
|
|
165
170
|
if (opts.input !== undefined) {
|
|
@@ -171,6 +176,42 @@ export function registerGraphCommands(program: Command, runsDeps: { root?: strin
|
|
|
171
176
|
}
|
|
172
177
|
await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
|
|
173
178
|
const { graphTick } = await import('./graph-tick.js');
|
|
179
|
+
if (opts.webhook) {
|
|
180
|
+
const { createWebhookSource } = await import('../workflow-runtime/triggers/webhook-source.js');
|
|
181
|
+
const graphFile = resolve(file);
|
|
182
|
+
const source = createWebhookSource();
|
|
183
|
+
// The CLI tick callback is the graph counterpart of the workflow webhook router's onEmit.
|
|
184
|
+
source.subscribeGraph(graphFile, async () => {
|
|
185
|
+
try {
|
|
186
|
+
const { notifyGraphEvents } = await import('./graph-notify.js');
|
|
187
|
+
const result = await graphTick(graphFile, { startIfIdle: true, deps: { notify: async () => { await notifyGraphEvents(); } } });
|
|
188
|
+
if (result.status === 'refused') return { ok: false, error: `refused: ${result.reason}` };
|
|
189
|
+
if (result.status === 'failed') return { ok: false, runId: result.runId, error: 'failed' };
|
|
190
|
+
return result.runId ? { ok: true, runId: result.runId } : { ok: false, error: `no run (${result.action})` };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
debug.log('graph.runner', 'webhook-tick-failed', { file: graphFile, error: error instanceof Error ? error.message : String(error) });
|
|
193
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (!source.subscriptions().length) throw new Error('graph triggers.webhook is missing');
|
|
197
|
+
await source.start();
|
|
198
|
+
const server = Bun.serve({
|
|
199
|
+
hostname: '127.0.0.1',
|
|
200
|
+
port: opts.port === undefined ? 0 : Number(opts.port),
|
|
201
|
+
fetch: async (request) => {
|
|
202
|
+
const response = await source.dispatch({
|
|
203
|
+
method: request.method, path: new URL(request.url).pathname,
|
|
204
|
+
headers: Object.fromEntries(request.headers), body: await request.text(),
|
|
205
|
+
});
|
|
206
|
+
return response ? new Response(response.body, { status: response.status, headers: response.headers }) : new Response('webhook source not started', { status: 503 });
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
console.log(`graph webhook: ${source.subscriptions().length} active on http://127.0.0.1:${server.port}`);
|
|
210
|
+
const shutdown = () => { void server.stop(true).then(() => source.stop()).then(() => { process.exitCode = 0; }); };
|
|
211
|
+
process.once('SIGINT', shutdown);
|
|
212
|
+
process.once('SIGTERM', shutdown);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
174
215
|
if (opts.schedule) {
|
|
175
216
|
const { createScheduleSource } = await import('../workflow-runtime/triggers/schedule-source.js');
|
|
176
217
|
const graphFile = resolve(file);
|
|
@@ -2,8 +2,9 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
6
|
-
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
7
|
+
import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
7
8
|
import { debug } from '../debug/log.js';
|
|
8
9
|
import { observeModelInputTokens } from '../harness/model-input-observation.js';
|
|
9
10
|
import { publishInsideEvent } from '../nexus/api/inside-events.js';
|
|
@@ -22,7 +23,7 @@ import { psProcessStartMs, START_TOLERANCE_MS } from '../harness/harness-stop.js
|
|
|
22
23
|
import { executeBashNode } from '../workflow-runtime/nodes/bash.js';
|
|
23
24
|
import { interpolate } from '../workflow-runtime/variables.js';
|
|
24
25
|
import type { BashNode, NodeExecContext, WorkflowDeps } from '../workflow-runtime/types.js';
|
|
25
|
-
import { releaseResumeAcceptedRegressions, releaseResumeWaiver, type AcceptedRegression } from '../../scripts/release-loop/resume.js';
|
|
26
|
+
import { releaseResumeAcceptedRegressions, releaseResumeGateExecutor, releaseResumeWaiver, type AcceptedRegression, type GateExecutorOverride } from '../../scripts/release-loop/resume.js';
|
|
26
27
|
|
|
27
28
|
type BashRun = WorkflowDeps['runBash'];
|
|
28
29
|
|
|
@@ -51,10 +52,14 @@ export interface GraphRunState {
|
|
|
51
52
|
graphSnapshot?: { graphSha: string; recipesSha: string };
|
|
52
53
|
graphPath?: string;
|
|
53
54
|
variant?: { overlayId: string; goal: string; plan: GraphVariantPlan };
|
|
55
|
+
parentRunId?: string;
|
|
56
|
+
unitCalls?: Array<{ node: string; unit: string; version: number; hash: string; childRunId: string; exit: string; steps: number }>;
|
|
54
57
|
growth?: GraphGrowth[];
|
|
55
58
|
growthRejections?: Array<{ from: string; outcome: string; reason: string }>;
|
|
56
59
|
growthPark?: { from: string; outcome: string; reason: string; decisionId: string; decisionRef: string; growth: GraphGrowth; command?: GrowthCommand; approval?: string };
|
|
57
60
|
resume?: { from: string; at: string; previousStatus: GraphRunState['status']; graph?: 'snapshot' | 'current' };
|
|
61
|
+
/** GATE-HOST-EXECUTOR — every explicit input change a resume made (what was set and what it replaced), oldest first. */
|
|
62
|
+
inputOverrides?: GateExecutorOverride[];
|
|
58
63
|
executed: number;
|
|
59
64
|
dryRun: boolean;
|
|
60
65
|
statePath: string;
|
|
@@ -74,7 +79,11 @@ export interface GraphRunOptions {
|
|
|
74
79
|
releaseWaiver?: { nodeId: string; reason: string; requestedRegressions: AcceptedRegression[]; acceptedRegressions: AcceptedRegression[] };
|
|
75
80
|
/** Release-only: add accepted issues independently of a waiver at a restart boundary. */
|
|
76
81
|
releaseAcceptedRegressions?: { requested: AcceptedRegression[]; acceptedRegressions: AcceptedRegression[] };
|
|
82
|
+
/** Release-only (GATE-HOST-EXECUTOR): run the restarted gate on this executor — recorded in `inputOverrides`, never a waiver. */
|
|
83
|
+
releaseGateExecutor?: { executor: string; host?: string };
|
|
77
84
|
useCurrentGraph?: boolean;
|
|
85
|
+
/** Internal unit invocation context, propagated only by runGraph. */
|
|
86
|
+
unitContext?: { parentRunId: string; ancestors: readonly string[]; remainingSteps: number };
|
|
78
87
|
/**
|
|
79
88
|
* RELEASE-LEDGER-UNIVERSE — stamp every cmd: child with this run's ledger root (`ELANOUS_STATE_DIR`).
|
|
80
89
|
* Without it a child launched from a non-leader tree (`bun scripts/...` with cwd = that tree) re-resolves
|
|
@@ -389,6 +398,55 @@ function nextNode(edges: readonly GraphEdgeSpec[], nodeId: string, exitOutcome:
|
|
|
389
398
|
return edge.map?.[exitOutcome] ?? edge.fallback?.[0]?.node;
|
|
390
399
|
}
|
|
391
400
|
|
|
401
|
+
type ResolvedUnit = { path: string; template: GraphTemplateSpec; hash: string };
|
|
402
|
+
|
|
403
|
+
function resolveUnit(call: string, callerPath: string): ResolvedUnit {
|
|
404
|
+
const [id, requested] = call.split('@');
|
|
405
|
+
const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
406
|
+
const directories = [join(dirname(callerPath), 'units'), dirname(callerPath), join(repo, 'graphs', 'units'), join(getElanousConfigDir(), 'graphs', 'units')];
|
|
407
|
+
for (const dir of directories) {
|
|
408
|
+
if (!existsSync(dir)) continue;
|
|
409
|
+
const matches = readdirSync(dir).filter(file => /\.ya?ml$/.test(file) && file !== 'recipes.yaml' && file !== 'recipes.yml').flatMap(file => {
|
|
410
|
+
const path = join(dir, file);
|
|
411
|
+
const source = readFileSync(path, 'utf8');
|
|
412
|
+
const header: unknown = parseYaml(source);
|
|
413
|
+
if (!header || typeof header !== 'object' || Array.isArray(header) || (header as Record<string, unknown>).graph_id !== id) return [];
|
|
414
|
+
const parsed = parseGraphTemplateYaml(source, path);
|
|
415
|
+
if (parsed.errors.length) throw new Error(parsed.errors.map(e => `${e.path}: ${e.message}`).join('\n'));
|
|
416
|
+
const template = parsed.template!;
|
|
417
|
+
return template.unit && (requested === String(template.version) || requested === `^${template.version}`)
|
|
418
|
+
? [{ path, template, hash: createHash('sha256').update(source).digest('hex') }] : [];
|
|
419
|
+
});
|
|
420
|
+
if (matches.length > 1) throw new Error(`ambiguous unit: ${call}`);
|
|
421
|
+
if (matches.length) return matches[0]!;
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`unit not found: ${call}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateUnitCalls(graph: GraphTemplateSpec, path: string, ancestors: readonly string[] = []): Map<string, ResolvedUnit> {
|
|
427
|
+
if (ancestors.includes(graph.graphId)) throw new Error(`unit cycle: ${[...ancestors, graph.graphId].join(' -> ')}`);
|
|
428
|
+
if (ancestors.length > 3) throw new Error('unit nesting exceeds depth 3');
|
|
429
|
+
const result = new Map<string, ResolvedUnit>();
|
|
430
|
+
for (const node of graph.nodes) {
|
|
431
|
+
if (node.kind !== 'subgraph' || node.call === undefined) continue;
|
|
432
|
+
const unit = resolveUnit(node.call!, path);
|
|
433
|
+
const contract = node.contract!;
|
|
434
|
+
const edge = graph.edges.find(e => e.from === node.nodeId);
|
|
435
|
+
if (!unit.template.unit || contract.inputs.length !== unit.template.unit.inputs.length ||
|
|
436
|
+
contract.inputs.some(key => !unit.template.unit!.inputs.includes(key)) ||
|
|
437
|
+
contract.outputs.length !== unit.template.unit.outputs.length ||
|
|
438
|
+
contract.outputs.some(key => !unit.template.unit!.outputs.includes(key)) || edge?.on !== 'outcome' ||
|
|
439
|
+
!edge.map || Object.keys(edge.map).length !== unit.template.unit.exits.length ||
|
|
440
|
+
unit.template.unit.exits.some(exit => !Object.hasOwn(edge.map!, exit))) {
|
|
441
|
+
debug.log('graph.unit', 'contract-mismatch', { graphId: graph.graphId, nodeId: node.nodeId, call: node.call });
|
|
442
|
+
throw new Error(`unit contract mismatch: ${node.nodeId} -> ${node.call}`);
|
|
443
|
+
}
|
|
444
|
+
validateUnitCalls(unit.template, unit.path, [...ancestors, graph.graphId]);
|
|
445
|
+
result.set(node.nodeId, unit);
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
392
450
|
export async function runGraph(path: string, options: GraphRunOptions = {}): Promise<GraphRunState> {
|
|
393
451
|
if (options.resumeRunId && options.runId) throw new Error('runId and resumeRunId cannot be combined');
|
|
394
452
|
if (options.fromNodeId && !options.resumeRunId) throw new Error('--from requires --resume');
|
|
@@ -396,6 +454,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
396
454
|
if (options.variant && options.resumeRunId) throw new Error('variant cannot be combined with --resume');
|
|
397
455
|
if (options.releaseWaiver && (!options.resumeRunId || options.fromNodeId !== options.releaseWaiver.nodeId)) throw new Error('release waiver requires --resume --from <waived node>');
|
|
398
456
|
if (options.releaseAcceptedRegressions && (!options.resumeRunId || !options.fromNodeId || options.releaseWaiver)) throw new Error('accepted regressions require a release resume boundary');
|
|
457
|
+
if (options.releaseGateExecutor && (!options.resumeRunId || !options.fromNodeId)) throw new Error('gate executor override requires a release resume boundary');
|
|
399
458
|
const root = options.deps?.root ?? effectiveInstanceRoot();
|
|
400
459
|
const matchingRuns = options.resumeRunId && !options.resumeGraphId
|
|
401
460
|
? listGraphRuns(root).runs.filter((run) => run.runId === options.resumeRunId && run.graphPath === resolve(path)) : [];
|
|
@@ -428,6 +487,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
428
487
|
const variant = options.variant ? createGraphVariant({ template: graph, ...options.variant }) : undefined;
|
|
429
488
|
if (variant && !variant.ok) throw new Error(`graph variant rejected: ${JSON.stringify(variant.rejections)}`);
|
|
430
489
|
if (variant?.ok) graph = variant.template;
|
|
490
|
+
const units = validateUnitCalls(graph, path, options.unitContext?.ancestors);
|
|
431
491
|
const variantSource = variant?.ok ? stringifyYaml({ ...document,
|
|
432
492
|
nodes: graph.nodes.map((node, index) => ({
|
|
433
493
|
...((document.nodes as Record<string, unknown>[])[index] ?? {}), node_id: node.nodeId, kind: node.kind,
|
|
@@ -506,6 +566,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
506
566
|
if (options.resumeRunId && state.growth?.length && graphMode === 'current' && state.sourceHash !== approvalSourceHash) {
|
|
507
567
|
throw new Error('graph or recipes changed since grown run');
|
|
508
568
|
}
|
|
569
|
+
if (options.unitContext) {
|
|
570
|
+
if (state.parentRunId !== undefined && state.parentRunId !== options.unitContext.parentRunId) throw new Error('unit parent run changed');
|
|
571
|
+
state.parentRunId = options.unitContext.parentRunId;
|
|
572
|
+
}
|
|
509
573
|
if (state.growth?.length) graph = withGrowth(graph, state.growth);
|
|
510
574
|
let waivedRecord: GraphRunState['nodes'][number] | undefined;
|
|
511
575
|
if (options.fromNodeId) {
|
|
@@ -577,6 +641,22 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
577
641
|
state.input = { ...(state.input as Record<string, unknown>),
|
|
578
642
|
acceptedRegressions: releaseResumeAcceptedRegressions(state, [], options.fromNodeId) };
|
|
579
643
|
}
|
|
644
|
+
if (options.releaseGateExecutor) {
|
|
645
|
+
if (graphId !== 'release-loop') throw new Error('gate executor override requires the release-loop graph');
|
|
646
|
+
const override = releaseResumeGateExecutor(state, { from: options.fromNodeId, executor: options.releaseGateExecutor.executor, host: options.releaseGateExecutor.host });
|
|
647
|
+
const next: Record<string, unknown> = { ...(state.input as Record<string, unknown>), ...override.set };
|
|
648
|
+
for (const key of override.unset ?? []) delete next[key];
|
|
649
|
+
state.input = next;
|
|
650
|
+
state.inputOverrides = [...(state.inputOverrides ?? []), override];
|
|
651
|
+
debug.log('graph.runs', 'input-override', { graphId, runId, ...override });
|
|
652
|
+
}
|
|
653
|
+
if (state.unitCalls) {
|
|
654
|
+
const calls = state.unitCalls;
|
|
655
|
+
state.unitCalls = calls.filter(call => {
|
|
656
|
+
const keep = state.path.slice(0, from).filter(nodeId => nodeId === call.node).length;
|
|
657
|
+
return calls.filter(item => item.node === call.node).indexOf(call) < keep;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
580
660
|
state.path = state.path.slice(0, from);
|
|
581
661
|
state.nodes = state.nodes.slice(0, from);
|
|
582
662
|
state.executed = state.nodes.filter((node) => node.executed).length;
|
|
@@ -659,7 +739,11 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
659
739
|
if (state.status === 'done' || state.status === 'failed' || state.status === 'budget-exceeded') state.finishedAt ??= new Date().toISOString();
|
|
660
740
|
persistOwnedRun(state, runnerOwner);
|
|
661
741
|
};
|
|
662
|
-
const
|
|
742
|
+
const baseLog = options.deps?.log ?? ((event: string, data: Record<string, unknown>) => debug.log('graph.runner', event, data));
|
|
743
|
+
// RFC §U: every event of a unit (child) run carries its parentRunId.
|
|
744
|
+
const log = options.unitContext
|
|
745
|
+
? (event: string, data: Record<string, unknown>) => baseLog(event, { ...data, parentRunId: options.unitContext!.parentRunId })
|
|
746
|
+
: baseLog;
|
|
663
747
|
const visits = new Map<string, number>();
|
|
664
748
|
for (const nodeId of state.path) visits.set(nodeId, (visits.get(nodeId) ?? 0) + 1);
|
|
665
749
|
let current: string | undefined = options.fromNodeId ?? state.pending?.nodeId ?? state.path.at(-1) ?? graph.entryNode;
|
|
@@ -685,7 +769,9 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
685
769
|
const completed = state.nodes.at(-1);
|
|
686
770
|
const resumingCompleted = options.resumeRunId !== undefined && !options.fromNodeId && completed?.nodeId === current &&
|
|
687
771
|
state.path.at(-1) === current && !resumingPending;
|
|
688
|
-
|
|
772
|
+
const usedSteps = state.path.length + (state.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
|
|
773
|
+
if (!resumingPending && !resumingCompleted && ((visits.get(current) ?? 0) >= node.maxVisits ||
|
|
774
|
+
(options.unitContext && usedSteps >= options.unitContext.remainingSteps))) {
|
|
689
775
|
state.status = 'budget-exceeded';
|
|
690
776
|
log('budget-exceeded', { graphId, runId, nodeId: current });
|
|
691
777
|
persist();
|
|
@@ -795,6 +881,81 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
795
881
|
error = result.error ? redactCredentials(result.error) : undefined;
|
|
796
882
|
state.executed++;
|
|
797
883
|
}
|
|
884
|
+
let unitExecuted = false;
|
|
885
|
+
if (units.has(current) && !waiving && state.dryRun && !resumingCompleted) {
|
|
886
|
+
// Dry run plans the walk only: no child run, no unit event, no unitCalls entry; the first declared exit stands in.
|
|
887
|
+
output = JSON.stringify({ outcome: units.get(current)!.template.unit!.exits[0], dryRun: true });
|
|
888
|
+
} else if (units.has(current) && !waiving && !resumingCompleted) {
|
|
889
|
+
const unit = units.get(current)!;
|
|
890
|
+
const contract = node.contract!;
|
|
891
|
+
const previous: Record<string, unknown> = Object.create(null);
|
|
892
|
+
for (const record of state.nodes) previous[record.nodeId] = lastJsonObject(record.output) ?? record.output;
|
|
893
|
+
const supplied = state.input && typeof state.input === 'object' && !Array.isArray(state.input)
|
|
894
|
+
? state.input as Record<string, unknown> : {};
|
|
895
|
+
const inputs: Record<string, unknown> = Object.create(null);
|
|
896
|
+
for (const key of contract.inputs) {
|
|
897
|
+
const value = Object.hasOwn(supplied, key) ? supplied[key] : previous[key];
|
|
898
|
+
if (value !== undefined) inputs[key] = value;
|
|
899
|
+
}
|
|
900
|
+
const alreadySpent = state.path.length + (state.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
|
|
901
|
+
const remaining = options.unitContext ? options.unitContext.remainingSteps - alreadySpent : Infinity;
|
|
902
|
+
const childRunId = randomUUID();
|
|
903
|
+
const info = { graphId, runId, nodeId: current, unit: unit.template.graphId, childRunId, parentRunId: runId };
|
|
904
|
+
let entered = false;
|
|
905
|
+
try {
|
|
906
|
+
for (const key of contract.inputs) if (!Object.hasOwn(inputs, key)) throw new Error(`unit input missing: ${current}.${key}`);
|
|
907
|
+
if (createHash('sha256').update(readFileSync(unit.path, 'utf8')).digest('hex') !== unit.hash)
|
|
908
|
+
throw new Error(`unit source changed before call: ${current}`);
|
|
909
|
+
debug.log('graph.unit', 'enter', info);
|
|
910
|
+
log('graph.unit', { ...info, phase: 'enter' });
|
|
911
|
+
entered = true;
|
|
912
|
+
const child = await runGraph(unit.path, { runId: childRunId, input: inputs, dryRun: false,
|
|
913
|
+
pinChildUniverse: options.pinChildUniverse,
|
|
914
|
+
unitContext: { parentRunId: runId, ancestors: [...(options.unitContext?.ancestors ?? []), graphId],
|
|
915
|
+
remainingSteps: Math.min(node.budget!.innerSteps, remaining) }, deps: options.deps });
|
|
916
|
+
const steps = child.path.length + (child.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
|
|
917
|
+
const childExit = child.status === 'done' || child.status === 'failed' ? child.path.at(-1) : undefined;
|
|
918
|
+
// The last successful record carrying every contracted output is the source;
|
|
919
|
+
// intervening nodes (including terminal gates) need not reproduce those fields.
|
|
920
|
+
const values = child.nodes.reduce<Record<string, unknown> | undefined>((found, record) => {
|
|
921
|
+
if (!record.ok || !record.executed) return found;
|
|
922
|
+
const value = lastJsonObject(record.output) ??
|
|
923
|
+
(record.output && typeof record.output === 'object' && !Array.isArray(record.output)
|
|
924
|
+
? record.output as Record<string, unknown> : undefined);
|
|
925
|
+
return value && contract.outputs.every(key => Object.hasOwn(value, key)) ? value : found;
|
|
926
|
+
}, undefined) ?? {};
|
|
927
|
+
state.unitCalls ??= [];
|
|
928
|
+
state.unitCalls.push({ node: current, unit: unit.template.graphId, version: unit.template.version,
|
|
929
|
+
hash: unit.hash, childRunId, exit: childExit ?? child.status, steps });
|
|
930
|
+
if (child.status === 'awaiting-approval') throw new Error(`unit cannot pause for approval: ${current}`);
|
|
931
|
+
if (child.status === 'budget-exceeded' || steps > node.budget!.innerSteps || steps > remaining) {
|
|
932
|
+
debug.log('graph.unit', 'budget-exhausted', { ...info, steps, innerSteps: node.budget!.innerSteps });
|
|
933
|
+
log('graph.unit', { ...info, phase: 'budget-exhausted', steps });
|
|
934
|
+
throw new Error(`unit inner_steps exceeded: ${current}`);
|
|
935
|
+
}
|
|
936
|
+
if (!childExit || !unit.template.unit!.exits.includes(childExit)) throw new Error(`unit exit missing: ${current}`);
|
|
937
|
+
if (child.status === 'done' && childExit === 'failed') throw new Error(`unit exit mismatch: ${current}`);
|
|
938
|
+
const result: Record<string, unknown> = { outcome: childExit };
|
|
939
|
+
for (const key of contract.outputs) {
|
|
940
|
+
if (!Object.hasOwn(values, key)) throw new Error(`unit output missing: ${current}.${key}`);
|
|
941
|
+
result[key] = values[key];
|
|
942
|
+
}
|
|
943
|
+
output = JSON.stringify(result);
|
|
944
|
+
ok = child.status === 'done';
|
|
945
|
+
debug.log('graph.unit', 'exit', { ...info, exit: childExit, steps });
|
|
946
|
+
log('graph.unit', { ...info, phase: 'exit', exit: childExit, steps });
|
|
947
|
+
} catch (cause) {
|
|
948
|
+
ok = false;
|
|
949
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
950
|
+
if (entered) {
|
|
951
|
+
debug.log('graph.unit', 'exit', { ...info, error });
|
|
952
|
+
log('graph.unit', { ...info, phase: 'exit', error });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// A rejected invocation has no child ledger and did not execute a unit.
|
|
956
|
+
unitExecuted = existsSync(graphRunPath(unit.template.graphId, childRunId, root));
|
|
957
|
+
if (unitExecuted) state.executed++;
|
|
958
|
+
}
|
|
798
959
|
if (prompt && !waiving && !state.dryRun && !resumingCompleted) {
|
|
799
960
|
const outputs: Record<string, { ok: boolean; output: unknown; durationMs: number }> = Object.create(null);
|
|
800
961
|
for (const previous of state.nodes) outputs[previous.nodeId] = { ok: previous.ok, output: lastJsonObject(previous.output) ?? previous.output ?? null, durationMs: 0 };
|
|
@@ -833,7 +994,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
833
994
|
if (approval && !state.dryRun && !command) {
|
|
834
995
|
output = JSON.stringify({ outcome: ok ? 'approved' : 'rejected', ...(state.pending?.decidedBy === undefined ? {} : { decidedBy: state.pending.decidedBy }), decidedAt: state.pending?.decidedAt ?? null });
|
|
835
996
|
}
|
|
836
|
-
state.nodes.push({ nodeId: current, ok, exit, executed: !waiving && ((!!command && (!state.dryRun || !!command.dry_run_command)) || (!!prompt && !state.dryRun)), ...((waiving || ((command || prompt || approval) && (!state.dryRun || !!command?.dry_run_command))) && output !== undefined ? { output } : {}), ...(error ? { error } : {}),
|
|
997
|
+
state.nodes.push({ nodeId: current, ok, exit, executed: !waiving && ((!!command && (!state.dryRun || !!command.dry_run_command)) || (!!prompt && !state.dryRun) || unitExecuted), ...((waiving || units.has(current) || ((command || prompt || approval) && (!state.dryRun || !!command?.dry_run_command))) && output !== undefined ? { output } : {}), ...(error ? { error } : {}),
|
|
837
998
|
...(waiving ? { waiver: { reason: options.releaseWaiver!.reason, at: nodeEndedAt, original: { exit: waivedRecord!.exit, ...(waivedRecord!.output === undefined ? {} : { output: waivedRecord!.output }), ...(waivedRecord!.error === undefined ? {} : { error: waivedRecord!.error }) } } } : {}),
|
|
838
999
|
...(approval && state.pending ? { ...(state.pending.decidedBy === undefined ? {} : { decidedBy: state.pending.decidedBy }), decidedAt: state.pending.decidedAt } : {}),
|
|
839
1000
|
startedAt: nodeStartedAt, endedAt: nodeEndedAt, seconds });
|
|
@@ -842,7 +1003,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
842
1003
|
delete state.pending;
|
|
843
1004
|
delete state.approvalSourceHash;
|
|
844
1005
|
}
|
|
845
|
-
const reported = waiving || (prompt && !state.dryRun) || (command && (!state.dryRun || command.dry_run_command)) ? lastJsonObject(resumingCompleted ? completed?.output : output)?.outcome : undefined;
|
|
1006
|
+
const reported = waiving || units.has(current) || (prompt && !state.dryRun) || (command && (!state.dryRun || command.dry_run_command)) ? lastJsonObject(resumingCompleted ? completed?.output : output)?.outcome : undefined;
|
|
846
1007
|
const namedOutcome = typeof reported === 'string' ? reported : undefined;
|
|
847
1008
|
console.error(`[graph] ${current} ${ok ? 'ok' : 'fail'} (${seconds.toFixed(2)}s)`);
|
|
848
1009
|
debug.log('graph.run', 'node', { graphId, runId, nodeId: current, phase: ok ? 'ok' : 'fail', seconds, exit });
|
package/src/grok/usage.ts
CHANGED
|
@@ -182,13 +182,34 @@ export async function fetchGrokUsage(
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
185
|
+
/** 주간 사용률·리셋이 없거나 유효하지 않은 응답에서 구독 축이 제공되지 않음을 나타낸다. */
|
|
186
186
|
export const GROK_SUBSCRIPTION_AXIS = {
|
|
187
187
|
status: 'unavailable',
|
|
188
188
|
reason: 'query-does-not-supply',
|
|
189
189
|
} as const;
|
|
190
190
|
|
|
191
|
-
/**
|
|
191
|
+
/** billing 쿼리가 주간 창과 사용률을 함께 돌려준 경우에만 실제 구독 잔량을 구성한다. */
|
|
192
|
+
export function grokUsageToSubscriptionAxis(usage: GrokUsage):
|
|
193
|
+
| { readonly status: 'available'; readonly remainingPercent: number; readonly resetsAt: number; readonly windowKind: 'weekly' }
|
|
194
|
+
| typeof GROK_SUBSCRIPTION_AXIS {
|
|
195
|
+
const end = usage.periodEnd === null ? Number.NaN : Date.parse(usage.periodEnd);
|
|
196
|
+
if (
|
|
197
|
+
usage.periodType !== 'USAGE_PERIOD_TYPE_WEEKLY'
|
|
198
|
+
|| usage.usedPercent === null
|
|
199
|
+
|| !Number.isFinite(usage.usedPercent)
|
|
200
|
+
|| usage.usedPercent < 0
|
|
201
|
+
|| usage.usedPercent > 100
|
|
202
|
+
|| !Number.isFinite(end)
|
|
203
|
+
) return GROK_SUBSCRIPTION_AXIS;
|
|
204
|
+
return {
|
|
205
|
+
status: 'available',
|
|
206
|
+
remainingPercent: 100 - usage.usedPercent,
|
|
207
|
+
resetsAt: end,
|
|
208
|
+
windowKind: 'weekly',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 원본 크레딧 사용률·창·금액류 → 크레딧 축. ⛔ 크레딧 숫자만으로 구독 잔량을 만들지 않는다. */
|
|
192
213
|
export function grokUsageToCreditAxis(usage: GrokUsage): {
|
|
193
214
|
readonly status: 'ok';
|
|
194
215
|
readonly usedPercent: number | null;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Reference card builder — L0 card + L1 listing for one local-repo / local-docs source.
|
|
2
|
+
//
|
|
3
|
+
// Pure value out: nothing is written. File access goes through the injected
|
|
4
|
+
// `RefIndexFs`, and every read is capped (file count, bytes per file), so a
|
|
5
|
+
// full file body is never loaded. Persistence (`grounding index` → jsonl) and
|
|
6
|
+
// BM25 are deliberately not here — they are the next slice.
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { closeSync, openSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { basename, join } from 'node:path';
|
|
11
|
+
import { GroundingSourceError, type GroundingSource } from './sources.js';
|
|
12
|
+
|
|
13
|
+
export interface RefIndexDirEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
isDirectory: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RefIndexFs {
|
|
19
|
+
/** Names directly under `dir`. Unreadable dir → empty list. */
|
|
20
|
+
readDir(dir: string): RefIndexDirEntry[];
|
|
21
|
+
/** At most `maxBytes` from the start of the file. `truncated` when the file is longer. */
|
|
22
|
+
readHead(file: string, maxBytes: number): { text: string; truncated: boolean } | undefined;
|
|
23
|
+
/** Modified time of a path, ms since epoch. */
|
|
24
|
+
mtimeMs(path: string): number | undefined;
|
|
25
|
+
/** Last commit time (ISO) of a git repo, when it has one. */
|
|
26
|
+
lastCommitAt(repoDir: string): string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RefIndexLimits {
|
|
30
|
+
/** Max markdown/readme/package files opened for one source. */
|
|
31
|
+
maxFiles: number;
|
|
32
|
+
/** Max bytes read from the head of any one file. */
|
|
33
|
+
maxBytesPerFile: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_REF_INDEX_LIMITS: RefIndexLimits = { maxFiles: 200, maxBytesPerFile: 16 * 1024 };
|
|
37
|
+
|
|
38
|
+
const ONE_LINE_MAX = 200;
|
|
39
|
+
const SKIP_DIRS = new Set(['node_modules']);
|
|
40
|
+
|
|
41
|
+
export interface ReferenceTitle {
|
|
42
|
+
/** Path relative to the source root. */
|
|
43
|
+
file: string;
|
|
44
|
+
title: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ReferenceCard {
|
|
48
|
+
/** L0 — `name` is the repository's own name (package.json `name`, else its directory name), not the registry `id`. */
|
|
49
|
+
id: string;
|
|
50
|
+
kind: GroundingSource['kind'];
|
|
51
|
+
name: string;
|
|
52
|
+
oneLine: string;
|
|
53
|
+
directories: string[];
|
|
54
|
+
receivedAt?: string;
|
|
55
|
+
/** L1 */
|
|
56
|
+
titles: ReferenceTitle[];
|
|
57
|
+
commands: string[];
|
|
58
|
+
/** A file-count or byte limit cut the read short — the lists may be incomplete. */
|
|
59
|
+
truncated: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildReferenceCard(
|
|
63
|
+
source: GroundingSource,
|
|
64
|
+
fs: RefIndexFs,
|
|
65
|
+
limits: Partial<RefIndexLimits> = {},
|
|
66
|
+
): ReferenceCard {
|
|
67
|
+
if ((source.kind !== 'local-repo' && source.kind !== 'local-docs') || !source.path) {
|
|
68
|
+
throw new GroundingSourceError(`reference card needs a local-repo/local-docs path: ${source.id}`);
|
|
69
|
+
}
|
|
70
|
+
const root = source.path;
|
|
71
|
+
// A missing root would otherwise yield an empty, untruncated card indistinguishable from an empty repo.
|
|
72
|
+
if (fs.mtimeMs(root) === undefined) {
|
|
73
|
+
throw new GroundingSourceError(`reference card source path is not readable: ${source.id} (${root})`);
|
|
74
|
+
}
|
|
75
|
+
const { maxFiles, maxBytesPerFile } = { ...DEFAULT_REF_INDEX_LIMITS, ...limits };
|
|
76
|
+
let opened = 0;
|
|
77
|
+
let truncated = false;
|
|
78
|
+
|
|
79
|
+
const open = (file: string): string | undefined => {
|
|
80
|
+
if (opened >= maxFiles) {
|
|
81
|
+
truncated = true;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
opened += 1;
|
|
85
|
+
const head = fs.readHead(file, maxBytesPerFile);
|
|
86
|
+
if (!head) return undefined;
|
|
87
|
+
if (head.truncated) truncated = true;
|
|
88
|
+
|
|
89
|
+
return head.text;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const top = fs.readDir(root).sort((a, b) => a.name.localeCompare(b.name));
|
|
93
|
+
const directories = top
|
|
94
|
+
.filter((e) => e.isDirectory && !e.name.startsWith('.') && !SKIP_DIRS.has(e.name))
|
|
95
|
+
.map((e) => e.name);
|
|
96
|
+
|
|
97
|
+
const titles: ReferenceTitle[] = [];
|
|
98
|
+
const readme = top.find((e) => !e.isDirectory && /^readme(\.[a-z]+)?$/i.test(e.name));
|
|
99
|
+
let oneLine = '';
|
|
100
|
+
if (readme) {
|
|
101
|
+
const text = open(join(root, readme.name));
|
|
102
|
+
if (text !== undefined) {
|
|
103
|
+
oneLine = firstParagraphLine(text);
|
|
104
|
+
for (const title of headingTitles(text)) titles.push({ file: readme.name, title });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const docRoots = source.kind === 'local-docs' ? [''] : directories.includes('docs') ? ['docs'] : [];
|
|
109
|
+
for (const rel of docRoots) {
|
|
110
|
+
for (const file of markdownFiles(fs, root, rel, maxFiles + 2)) {
|
|
111
|
+
if (file === readme?.name) continue;
|
|
112
|
+
const text = open(join(root, file));
|
|
113
|
+
if (text === undefined) continue;
|
|
114
|
+
for (const title of headingTitles(text)) titles.push({ file, title });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const commands: string[] = [];
|
|
119
|
+
let packageName: string | undefined;
|
|
120
|
+
if (top.some((e) => !e.isDirectory && e.name === 'package.json')) {
|
|
121
|
+
const raw = open(join(root, 'package.json'));
|
|
122
|
+
if (raw !== undefined) {
|
|
123
|
+
const pkg = parsePackage(raw);
|
|
124
|
+
packageName = pkg.name;
|
|
125
|
+
commands.push(...pkg.commands);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const receivedAt = receivedAtOf(source, fs, root);
|
|
130
|
+
return {
|
|
131
|
+
id: source.id,
|
|
132
|
+
kind: source.kind,
|
|
133
|
+
name: packageName ?? basename(root),
|
|
134
|
+
oneLine,
|
|
135
|
+
directories,
|
|
136
|
+
...(receivedAt ? { receivedAt } : {}),
|
|
137
|
+
titles,
|
|
138
|
+
commands,
|
|
139
|
+
truncated,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function receivedAtOf(source: GroundingSource, fs: RefIndexFs, root: string): string | undefined {
|
|
144
|
+
if (source.kind === 'local-repo') {
|
|
145
|
+
const commit = fs.lastCommitAt(root);
|
|
146
|
+
if (commit) return commit;
|
|
147
|
+
}
|
|
148
|
+
const mtime = fs.mtimeMs(root);
|
|
149
|
+
return mtime === undefined ? undefined : new Date(mtime).toISOString();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Markdown paths (relative to `root`) under `rel`, sorted. Directory listing only — no bodies.
|
|
154
|
+
* The walk stops once `cap` paths are found: past the file budget the caller only needs to know
|
|
155
|
+
* «there was more» (→ truncated), not the whole tree.
|
|
156
|
+
*/
|
|
157
|
+
function markdownFiles(fs: RefIndexFs, root: string, rel: string, cap: number): string[] {
|
|
158
|
+
const found: string[] = [];
|
|
159
|
+
const walk = (dirRel: string): void => {
|
|
160
|
+
if (found.length >= cap) return;
|
|
161
|
+
const entries = fs.readDir(dirRel ? join(root, dirRel) : root).sort((a, b) => a.name.localeCompare(b.name));
|
|
162
|
+
for (const e of entries) {
|
|
163
|
+
const childRel = dirRel ? join(dirRel, e.name) : e.name;
|
|
164
|
+
if (found.length >= cap) return;
|
|
165
|
+
if (e.isDirectory) {
|
|
166
|
+
if (!e.name.startsWith('.') && !SKIP_DIRS.has(e.name)) walk(childRel);
|
|
167
|
+
} else if (/\.md$/i.test(e.name)) {
|
|
168
|
+
found.push(childRel);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
walk(rel);
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function firstParagraphLine(text: string): string {
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
179
|
+
const line = raw.trim();
|
|
180
|
+
if (!line) {
|
|
181
|
+
if (lines.length > 0) break;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (line.startsWith('#') || line.startsWith('![') || line.startsWith('[![') || line.startsWith('<') || line.startsWith('```')) {
|
|
185
|
+
if (lines.length > 0) break;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
lines.push(line);
|
|
189
|
+
}
|
|
190
|
+
const joined = lines.join(' ');
|
|
191
|
+
return joined.length > ONE_LINE_MAX ? `${joined.slice(0, ONE_LINE_MAX - 1)}…` : joined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function headingTitles(text: string): string[] {
|
|
195
|
+
const out: string[] = [];
|
|
196
|
+
let fenced = false;
|
|
197
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
198
|
+
if (/^\s*(```|~~~)/.test(raw)) {
|
|
199
|
+
fenced = !fenced;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (fenced) continue;
|
|
203
|
+
const m = /^#{1,6}\s+(.+?)(?:\s+#+)?\s*$/.exec(raw);
|
|
204
|
+
if (m?.[1]) out.push(m[1]);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parsePackage(packageJson: string): { name?: string; commands: string[] } {
|
|
210
|
+
let pkg: unknown;
|
|
211
|
+
try { pkg = JSON.parse(packageJson); } catch { return { commands: [] }; }
|
|
212
|
+
if (!pkg || typeof pkg !== 'object') return { commands: [] };
|
|
213
|
+
const { bin, name } = pkg as { bin?: unknown; name?: unknown };
|
|
214
|
+
const pkgName = typeof name === 'string' && name.trim() ? name.trim() : undefined;
|
|
215
|
+
const base = pkgName ? { name: pkgName } : {};
|
|
216
|
+
if (typeof bin === 'string') return { ...base, commands: pkgName ? [pkgName.replace(/^@[^/]+\//, '')] : [] };
|
|
217
|
+
if (bin && typeof bin === 'object' && !Array.isArray(bin)) return { ...base, commands: Object.keys(bin).sort() };
|
|
218
|
+
return { ...base, commands: [] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Real filesystem + local git. `git log` reads local refs only. */
|
|
222
|
+
export const nodeRefIndexFs: RefIndexFs = {
|
|
223
|
+
readDir(dir) {
|
|
224
|
+
try {
|
|
225
|
+
return readdirSync(dir, { withFileTypes: true }).map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
|
|
226
|
+
} catch {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
readHead(file, maxBytes) {
|
|
231
|
+
let fd: number | undefined;
|
|
232
|
+
try {
|
|
233
|
+
const size = statSync(file).size;
|
|
234
|
+
const length = Math.min(size, maxBytes);
|
|
235
|
+
const buf = Buffer.alloc(length);
|
|
236
|
+
fd = openSync(file, 'r');
|
|
237
|
+
readSync(fd, buf, 0, length, 0);
|
|
238
|
+
return { text: buf.toString('utf8'), truncated: size > maxBytes };
|
|
239
|
+
} catch {
|
|
240
|
+
return undefined;
|
|
241
|
+
} finally {
|
|
242
|
+
if (fd !== undefined) closeSync(fd);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
mtimeMs(path) {
|
|
246
|
+
try { return statSync(path).mtimeMs; } catch { return undefined; }
|
|
247
|
+
},
|
|
248
|
+
lastCommitAt(repoDir) {
|
|
249
|
+
const result = spawnSync('git', ['-C', repoDir, 'log', '-1', '--format=%cI'], { encoding: 'utf8', timeout: 10_000 });
|
|
250
|
+
const out = result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
251
|
+
return out || undefined;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
@@ -36,7 +36,9 @@ const constants = { BUILD_STOP, BUILD_SLOW, QUEUE_STOP, QUEUE_SLOW, UNKNOWN_CAP
|
|
|
36
36
|
const valid = (value: unknown): BackpressureSignal =>
|
|
37
37
|
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 'unknown';
|
|
38
38
|
|
|
39
|
-
/** Fixed, reviewable thresholds; observations are normalized before any arithmetic.
|
|
39
|
+
/** Fixed, reviewable thresholds; observations are normalized before any arithmetic.
|
|
40
|
+
* Existing caller: the inline `.action(...)` callback registered by `queue.command('backpressure')` in src/harness/harness-cli-command.ts calls `decideBackpressure(signals, { maxPerCycle: opts.max === undefined ? 6 : Number(opts.max) })`.
|
|
41
|
+
*/
|
|
40
42
|
export function decideBackpressure(input: BackpressureSignals, { maxPerCycle = 6 }: { maxPerCycle?: number } = {}) {
|
|
41
43
|
const max = Number.isSafeInteger(maxPerCycle) && maxPerCycle >= 0 ? maxPerCycle : 6;
|
|
42
44
|
const signals: BackpressureSignals = {
|
|
@@ -81,7 +83,13 @@ export function decideBackpressure(input: BackpressureSignals, { maxPerCycle = 6
|
|
|
81
83
|
budget = Math.min(budget, signals.queueDepth >= QUEUE_STOP ? 0 : Math.floor(max / 2));
|
|
82
84
|
reasons.push('queue-depth');
|
|
83
85
|
}
|
|
84
|
-
if (signals.staleParents !== 'unknown' && signals.staleParents > 0)
|
|
86
|
+
if (signals.staleParents !== 'unknown' && signals.staleParents > 0) {
|
|
87
|
+
reasons.push(`outdated-version-parents:${signals.staleParents} (legacy observation alias: stale-parents:${signals.staleParents}; 살아 있는 런 · 예산 안 깎음 · 낡은 부모 후보는 eln tasks parents)`);
|
|
88
|
+
// Co-occurrence only: staleParents is local ps, admitWaiting includes remote hosts — not proof that this parent is the one waiting.
|
|
89
|
+
if (signals.admitWaiting !== 'unknown' && signals.admitWaiting > 0) {
|
|
90
|
+
reasons.push('restart-stale-parent:approval-wait');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
85
93
|
return { launchBudget: Math.max(0, Math.min(max, Math.floor(budget))), reasons, signals, constants };
|
|
86
94
|
}
|
|
87
95
|
|