elanous 0.2.23 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
- package/apps/pwa/out/_next/static/css/72becfd42807b50d.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
- package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
- package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
- package/catalog/providers/openrouter.yaml +2 -0
- package/graphs/implement-loop.yaml +31 -0
- package/graphs/ops/harness-gc.yaml +17 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/overlays/README.md +1 -1
- package/graphs/overlays/stop-heal-ladder.yaml +105 -0
- package/package.json +1 -1
- package/scripts/add-api-key.sh +1 -1
- package/scripts/ci-typecheck-changed.ts +71 -14
- package/scripts/fleet/bedrock-pod-secret.ts +122 -0
- package/scripts/fleet/node-setup-linux.sh +97 -0
- package/scripts/fleet/node-setup.sh +11 -0
- package/scripts/fleet/zai-pod-secret.ts +86 -0
- package/scripts/land-global-checks.ts +3 -0
- package/scripts/measure-log-diet-2.ts +213 -0
- package/scripts/measure-tsc-affected-roots.ts +289 -0
- package/scripts/release-loop/gate-executor.ts +28 -0
- package/scripts/release-loop/gate-node.ts +262 -26
- package/scripts/release-loop/resume-release.ts +9 -3
- package/scripts/release-loop/resume.ts +41 -0
- package/scripts/release-loop/unattended-release.ts +22 -7
- package/scripts/tsc-baseline-cache.ts +147 -0
- package/scripts/usage-rollup.ts +58 -6
- package/src/agent/run-context.ts +10 -0
- package/src/agent-mission/driver.ts +18 -3
- package/src/agent-mission/resource-ladder.ts +8 -3
- package/src/agent-mission/review-loop.ts +8 -2
- package/src/agent-substrate/acp-implementer.ts +139 -0
- package/src/agent-substrate/pr-reviewer.ts +31 -4
- package/src/agent-substrate/self-review-cli.ts +48 -9
- package/src/autopilot/mission-critique.ts +18 -5
- package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
- package/src/autopilot/mission-trace.ts +12 -1
- package/src/autopilot/system-lookback-run.ts +16 -10
- package/src/bench/bench.ts +183 -0
- package/src/budget/fetchers/grok.ts +15 -5
- package/src/budget/llm-cost.ts +37 -5
- package/src/budget/types.ts +8 -2
- package/src/budget/unified-usage.ts +88 -6
- package/src/cli/bench-cli.ts +34 -0
- package/src/cli/guardian-cli.ts +26 -1
- package/src/cli/image-cli.ts +101 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +62 -5
- package/src/cli/pr-land-overlap-evidence.ts +160 -12
- package/src/cli/release-cli.ts +18 -9
- package/src/cli/rfc-status.ts +52 -2
- package/src/cli/self-update.ts +104 -0
- package/src/cli/tasks-cli.ts +109 -4
- package/src/cli/usage-cli.ts +91 -8
- package/src/core-turn/run-goal-loop.ts +5 -4
- package/src/dashboard/ad-run-setup-bindings.ts +279 -0
- package/src/dashboard/index.ts +18 -276
- package/src/dashboard/stale-install-check.ts +11 -0
- package/src/debug/log-diet.ts +38 -7
- package/src/debug/log.ts +19 -1
- package/src/domains/release-tool.ts +39 -0
- package/src/error-report/report.ts +21 -4
- package/src/github/pr-labels.ts +1 -0
- package/src/graph-runner/graph-cli.ts +42 -1
- package/src/graph-runner/runner.ts +167 -6
- package/src/grok/usage.ts +23 -2
- package/src/grounding/ref-index.ts +253 -0
- package/src/harness/dispatch-backpressure.ts +10 -2
- package/src/harness/harness-clean.ts +199 -78
- package/src/harness/harness-cli-command.ts +122 -7
- package/src/harness/harness-gc.ts +331 -0
- package/src/harness/harness-stop.ts +4 -2
- package/src/harness/harness-substrate-default.ts +142 -7
- package/src/harness/launch-registry.ts +6 -0
- package/src/harness/worktree-branch-prefix.ts +9 -0
- package/src/hq/fence.ts +187 -0
- package/src/hq/seats.ts +38 -22
- package/src/images/image-ledger.ts +179 -0
- package/src/index.ts +93 -11
- package/src/instance/child-scope.ts +32 -1
- package/src/instance/dev-project.ts +133 -0
- package/src/instance/resolve.ts +24 -0
- package/src/instance/spawn-elanous.ts +45 -0
- package/src/intake-plane/ref-prefetch.ts +110 -0
- package/src/intelligence-map/model-catalog.ts +72 -0
- package/src/llm/bedrock-eventstream.ts +215 -0
- package/src/llm/bedrock-provider.ts +114 -41
- package/src/llm/bedrock.ts +174 -28
- package/src/llm/openrouter-byok-cost.ts +96 -0
- package/src/llm/openrouter-deepseek.ts +151 -0
- package/src/llm/openrouter-upstream-pins.config.ts +25 -0
- package/src/llm/zai-coding-plan.ts +700 -0
- package/src/llm.ts +87 -16
- package/src/loops/checker.ts +148 -9
- package/src/loops/orchestrator/resync-shadow.ts +34 -7
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/model-tier/llm-tier-map.ts +9 -5
- package/src/models/prompts.ts +2 -1
- package/src/mss/logging/log-store.ts +58 -11
- package/src/nexus/api/missions.ts +73 -2
- package/src/nexus/api/ops-api.ts +18 -0
- package/src/nexus/index.ts +7 -75
- package/src/nexus/tools-kind.ts +78 -0
- package/src/oauth/codex-account-rotation.ts +39 -2
- package/src/oauth/codex-account-store.ts +323 -24
- package/src/oauth/fallback-chain.ts +53 -15
- package/src/onboarding/first-run.ts +37 -18
- package/src/onboarding/web-first.ts +20 -2
- package/src/ops/machine-heartbeat.ts +185 -0
- package/src/ops/overview-cache.ts +175 -0
- package/src/ops/overview.ts +8 -0
- package/src/prompt-cache/openai.ts +3 -0
- package/src/prompt-cache/types.ts +5 -0
- package/src/prompt-enhance/enhance.ts +23 -0
- package/src/release-loop/placement.ts +16 -1
- package/src/requirements/requirement-ledger.ts +536 -0
- package/src/retro/daily.ts +5 -5
- package/src/seat-loop/seat-loop.ts +32 -10
- package/src/self-dev/dev-cli.ts +44 -10
- package/src/self-dev/dev-pipeline.ts +7 -2
- package/src/self-dev/draft-residue.ts +14 -2
- package/src/self-dev/draft-sweep.ts +103 -0
- package/src/self-dev/graph-journey-route.ts +6 -0
- package/src/self-dev/orchestrate-cli.ts +25 -3
- package/src/self-dev/orchestrate.ts +41 -6
- package/src/self-dev/run-supervisor.ts +188 -15
- package/src/self-dev/run-types.ts +13 -0
- package/src/self-dev/supervisor-verdict-edges.ts +1 -0
- package/src/self-implement/budget-gate.ts +53 -13
- package/src/self-implement/child-context-composition.ts +139 -0
- package/src/self-implement/child-llm-preference.ts +13 -5
- package/src/self-implement/claude-acp-implement.ts +67 -0
- package/src/self-implement/flex-signals.ts +59 -0
- package/src/self-implement/frozen-merges.ts +8 -3
- package/src/self-implement/goal-author-decompose.ts +10 -2
- package/src/self-implement/goal-author.ts +24 -9
- package/src/self-implement/graph-overlay-condition.ts +28 -0
- package/src/self-implement/graph-overlay-yaml.ts +41 -7
- package/src/self-implement/graph-templates.ts +33 -1
- package/src/self-implement/graph-yaml.ts +56 -0
- package/src/self-implement/heal-intake.ts +5 -1
- package/src/self-implement/orchestrator.ts +144 -15
- package/src/self-implement/running-runs.ts +75 -5
- package/src/self-implement/seams.ts +47 -1
- package/src/self-implement/self-implement-cli.ts +9 -4
- package/src/self-implement/stop-autoheal.ts +69 -2
- package/src/session-runtime/retry-policy.ts +2 -1
- package/src/steward/launch.ts +5 -4
- package/src/steward/triage-budget.ts +62 -0
- package/src/steward/triage.ts +83 -22
- package/src/task-agent/actions.ts +62 -5
- package/src/task-agent/completion-evidence.ts +5 -0
- package/src/task-agent/cover.ts +101 -3
- package/src/task-agent/judge.ts +16 -0
- package/src/task-agent/land-order-ledger.ts +95 -0
- package/src/task-agent/live-move-history.ts +87 -1
- package/src/task-agent/live-moves.ts +350 -20
- package/src/task-agent/rejudge-on-head.ts +75 -20
- package/src/task-agent/review-prior-fix.ts +146 -0
- package/src/task-agent/run-adopt.ts +136 -0
- package/src/task-agent/shadow.ts +53 -9
- package/src/task-agent/task-hand.ts +54 -7
- package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
- package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
- package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
- package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
- package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
- package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
- package/src/typecheck-ratchet.ts +1 -0
- package/src/user-config.ts +51 -11
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/triggers/registry.ts +59 -0
- package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
- package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
- /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
|
@@ -7,21 +7,29 @@
|
|
|
7
7
|
* 회수한 pass 는 그 틱의 판단부·land 관문 입력(`selfReview`)이 될 뿐 land 판정 로직은 그대로다.
|
|
8
8
|
* - `propose-green`: 카드에 green «제안»(`greenProposal`)만 적는다 — 체크리스트는 손대지 않는다.
|
|
9
9
|
* - `propose-land`: 카드·현재 머리의 self review 통과(pass «또는» warn ⊕ must-fix 0 · TA-LAND-WARN-MUSTFIX0)·non-draft 를 확인하고 기존 `pr land` 를 머리 고정으로 한 번 부른다.
|
|
10
|
+
* - 사다리 R4: `narrow-relaunch` · `wait-retry` · `salvage-relaunch` · `alternative` 도 허용 목록 어휘다 — 실제 발사는 실행부
|
|
11
|
+
* `executeNextAction`(`actions.ts`)이 «목록 ⊕ 같은 수 2회 상한 ⊕ 회당 1건 ⊕ 부하 관문»을 모두 넘을 때만 한다.
|
|
12
|
+
* - `decision-card`: 막힌 과제를 «근거(서명 · 화면 · must-fix)» 붙은 결정 카드로 올린다 — 근거가 하나도 없으면 카드를 올리지 않는다.
|
|
13
|
+
* 카드는 종전 실행부와 같은 `decisions raise` 모양이라 OP 거르기(`seat-loop/op-decide.ts`)가 그대로 본다 · 같은 근거는 카드 history 로 한 번만.
|
|
10
14
|
* - 설정 `taskAgent.liveMoves`(config > env `ELANOUS_TASK_AGENT_LIVE_MOVES` > 없음). 모르는 항목은 경고하고 버린다.
|
|
11
15
|
* - 관측: 실행한 수마다 `debug.log('task-agent', 'live-move', {kind, card, ok, detail, live:true})`.
|
|
12
16
|
*/
|
|
13
17
|
import { spawn, spawnSync } from 'node:child_process';
|
|
14
|
-
import {
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { closeSync, mkdirSync, openSync, readFileSync, statSync } from 'node:fs';
|
|
15
20
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
16
21
|
import { debug } from '../debug/log.js';
|
|
17
22
|
import { findGitDir } from '../git-fs/locate.js';
|
|
18
|
-
import { getUserConfig } from '../user-config.js';
|
|
23
|
+
import { getUserConfig, userConfigPath } from '../user-config.js';
|
|
19
24
|
import type { DetachedSpawn } from '../cli/tasks-cli.js';
|
|
20
|
-
import { taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
25
|
+
import { LIVE_MOVE_HISTORY_EVENT, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
26
|
+
import { LIVE_RETRY_MOVES } from './judge.js';
|
|
27
|
+
import { decisionCardGrounds, decisionRaiseArgs, type DecisionCardEvidence, type NextAction } from './actions.js';
|
|
21
28
|
import { reviewResultPath } from './review-result-capture.js';
|
|
29
|
+
import { buildPriorFixContext, PRIOR_FIX_SCOPE_LINE, priorFixBlock, priorFixObservation, type PriorDiffReader, type PriorFixContext } from './review-prior-fix.js';
|
|
22
30
|
import { createLandWorktree, localHead, observeLandWorktree, type LandWorktree, type LandWorktreeFailure, type LandWorktreeInput, type LandWorktreeRemoval } from './land-worktree.js';
|
|
23
31
|
|
|
24
|
-
export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green', 'propose-land'] as const;
|
|
32
|
+
export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green', 'propose-land', 'decision-card', ...LIVE_RETRY_MOVES] as const;
|
|
25
33
|
export type TaskAgentLiveMove = typeof TASK_AGENT_LIVE_MOVES[number];
|
|
26
34
|
export const TASK_AGENT_LIVE_MOVES_ENV = 'ELANOUS_TASK_AGENT_LIVE_MOVES';
|
|
27
35
|
|
|
@@ -70,11 +78,15 @@ export interface LiveMoveDeps {
|
|
|
70
78
|
* TA-REVIEW-RESULT-CAPTURE — `capture.resultPath` 가 오면 자식 `--json` stdout 을 그 파일로 받는다(회수 = `review-result-capture.ts`).
|
|
71
79
|
*/
|
|
72
80
|
requestReview?: (pr: number, intent: string, cwd?: string, capture?: { resultPath: string }) => Promise<void>;
|
|
81
|
+
/** REVIEW-CONVERGE-PRIOR-FIX — 같은 PR 재리뷰의 직전 머리 → 지금 머리 diff(시험 주입 · 기본 = `git diff --numstat`). */
|
|
82
|
+
priorDiff?: PriorDiffReader;
|
|
73
83
|
/** Fresh PR head and draft flag for landing; separate from the unchanged review lookup. */
|
|
74
84
|
landPrHead?: (pr: number, cwd: string) => Promise<(PrHeadView & { isDraft: boolean }) | null>;
|
|
75
85
|
/** One existing pr land operation; inject in tests instead of invoking the CLI. */
|
|
76
86
|
/** TA-REJUDGE-ON-HEAD — `opts.overlapEvidence` 가 있으면 `pr land --overlap-evidence <file>` 로 넘긴다(없으면 종전 인자). */
|
|
77
87
|
land?: (pr: number, head: string, cwd: string, opts?: { overlapEvidence?: string }) => Promise<{ status: number; stdout: string; stderr?: string }>;
|
|
88
|
+
/** `decision-card` — `decisions raise` 한 번(시험 주입 · 기본 = 같은 우주의 CLI 자식). */
|
|
89
|
+
raiseDecision?: (args: string[]) => Promise<{ status: number; stdout: string; stderr?: string }>;
|
|
78
90
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
79
91
|
/** TA-LAND-WORKTREE — cwd HEAD 읽기 · PR 머리 임시 워크트리 만들기(시험 주입 · 기본 = git). */
|
|
80
92
|
landWorktree?: { localHead?: (cwd: string) => string | null; create?: (input: LandWorktreeInput) => Promise<LandWorktree | LandWorktreeFailure> };
|
|
@@ -87,6 +99,8 @@ export interface LiveMoveResult {
|
|
|
87
99
|
reviewRepo?: { resolved: boolean; source?: ReviewRepoCandidate['source']; cwd?: string; proof?: 'head-sha' | 'branch'; reason: string };
|
|
88
100
|
/** TA-REVIEW-RESULT-CAPTURE — 띄운 review 의 결과 파일(자식 stdout) — 실행한 review 에만 실린다. */
|
|
89
101
|
resultPath?: string;
|
|
102
|
+
/** TA-LAND-RETRY-ON-INFRA — 실행한 land 의 그 머리 몇 번째 시도인가 · 실패면 갈래(infra|judgement|base-stale)·사유·재시도 가능 여부. */
|
|
103
|
+
landAttempt?: { attempt: number; class?: LandFailureClass; reason?: string; retryAllowed?: boolean };
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
export interface PrHeadView { head: string; state: string; branch?: string; url?: string }
|
|
@@ -242,11 +256,13 @@ export async function defaultLand(pr: number, head: string, cwd: string, opts: {
|
|
|
242
256
|
child.once('error', reject);
|
|
243
257
|
child.once('close', (code) => {
|
|
244
258
|
if (code !== 0) { resolveResult({ status: code ?? 1, stdout, stderr }); return; }
|
|
259
|
+
// TA-LAND-RETRY-ON-INFRA — 왜 미확인인지 «마지막 줄»로 가른다(꼬리 300자에서도 남게): 못 읽음 = 인프라 · 읽었는데 MERGED 아님 = 판정.
|
|
260
|
+
const unconfirmed = (line: string, before = stderr || stdout || 'landing completed without confirmed merge') => ({ status: 1, stdout, stderr: `${before.slice(-(299 - line.length))}\n${line}` });
|
|
245
261
|
void defaultLandPrHead(pr, cwd).then((view) => {
|
|
246
262
|
resolveResult(view?.state === 'MERGED'
|
|
247
263
|
? { status: 0, stdout, stderr }
|
|
248
|
-
:
|
|
249
|
-
}).catch((error) => resolveResult(
|
|
264
|
+
: unconfirmed(view ? `✗ ta-land: pr land exited 0 but PR is ${view.state}` : '✗ ta-land: merge state unreadable after pr land exit 0'));
|
|
265
|
+
}).catch((error) => resolveResult(unconfirmed('✗ ta-land: merge state unreadable after pr land exit 0', String(error))));
|
|
250
266
|
});
|
|
251
267
|
});
|
|
252
268
|
}
|
|
@@ -269,13 +285,17 @@ export async function defaultLandPrHead(pr: number, cwd: string): Promise<(PrHea
|
|
|
269
285
|
} catch { return null; }
|
|
270
286
|
}
|
|
271
287
|
|
|
272
|
-
|
|
288
|
+
/**
|
|
289
|
+
* REVIEW-CONVERGE-PRIOR-FIX — `prior` 는 같은 PR 의 두 번째 이후 리뷰에만 온다: «직전 반영분»에 직전 머리 범위·diff·must-fix 를,
|
|
290
|
+
* 범위 경계에 «바뀌지 않은 줄의 새 must-fix 는 근거 줄» 한 줄을 붙인다. 없으면(첫 리뷰) 종전 문면 바이트 그대로.
|
|
291
|
+
*/
|
|
292
|
+
export function liveReviewIntent(card: TaskCard, runId: string | null, stopReason: string, prior?: PriorFixContext | null): string {
|
|
273
293
|
const goal = card.text.split('\n')[0]!.trim().slice(0, 300);
|
|
274
294
|
return [
|
|
275
295
|
`목표: ${goal}`,
|
|
276
296
|
`수용기준: 과제 카드 ${card.id} 원문 —\n${card.text.trim().slice(0, 4000)}`,
|
|
277
|
-
`직전 반영분: 런 ${runId ?? '-'} 이 ${stopReason} 로 멈춤 — 수확
|
|
278
|
-
|
|
297
|
+
`직전 반영분: 런 ${runId ?? '-'} 이 ${stopReason} 로 멈춤 — 수확 가능${prior ? `\n${priorFixBlock(prior)}` : ''}`,
|
|
298
|
+
`의도적 범위 경계: task-agent live review 요청(TA-JUDGE-LIVE-SAFE) — 리뷰만 한다 · 착지·재발사는 이 요청의 결정이 아니다${prior ? `\n${PRIOR_FIX_SCOPE_LINE}` : ''}`,
|
|
279
299
|
].join('\n');
|
|
280
300
|
}
|
|
281
301
|
|
|
@@ -321,18 +341,25 @@ export async function executeLiveReview(
|
|
|
321
341
|
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
322
342
|
// TA-REVIEW-RESULT-CAPTURE — 머리마다 한 번이라 경로도 머리마다 하나다. 요청 칸에 먼저 적는다(회수가 이 칸을 본다).
|
|
323
343
|
const resultPath = reviewResultPath(path, card.id, pr, head.head);
|
|
324
|
-
const claim = { claimed: false, missing: false };
|
|
344
|
+
const claim: { claimed: boolean; missing: boolean; before?: TaskCard } = { claimed: false, missing: false };
|
|
325
345
|
updateTaskCard(path, card.id, (current) => {
|
|
326
346
|
if (!current) { claim.missing = true; return undefined; }
|
|
327
347
|
// 머리 «이력»으로 본다 — A→B→A 로 돌아와도 A 는 다시 요청하지 않는다.
|
|
328
348
|
if ((current.reviewRequests ?? []).some((entry) => entry.pr === pr && entry.head === head!.head)) return undefined;
|
|
329
349
|
claim.claimed = true;
|
|
350
|
+
claim.before = current;
|
|
330
351
|
return { ...current, reviewRequests: [...(current.reviewRequests ?? []), { pr, head: head!.head, at, resultPath }] };
|
|
331
352
|
});
|
|
332
353
|
if (claim.missing) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' }));
|
|
333
354
|
if (!claim.claimed) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: true, detail: `already requested for #${pr} head ${head.head.slice(0, 12)}` }));
|
|
334
355
|
try {
|
|
335
|
-
|
|
356
|
+
// REVIEW-CONVERGE-PRIOR-FIX — 같은 PR 의 직전 요청(잠금 안에서 본 이력)이 있으면 그 라운드를 intent 에 싣는다 · 맥락을 못 만들면 종전 문면.
|
|
357
|
+
let prior: PriorFixContext | null = null;
|
|
358
|
+
try { prior = claim.before ? buildPriorFixContext(claim.before, pr, head.head, { ...(cwd ? { cwd } : {}), ...(deps.priorDiff ? { priorDiff: deps.priorDiff } : {}) }) : null; } catch { prior = null; }
|
|
359
|
+
if (prior) {
|
|
360
|
+
try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'review-prior-fix-filled', priorFixObservation(card.id, pr, prior)); } catch { /* fail-soft */ }
|
|
361
|
+
}
|
|
362
|
+
const intent = liveReviewIntent(card, ctx.runId, ctx.stopReason, prior);
|
|
336
363
|
await (deps.requestReview ? deps.requestReview(pr, intent, cwd, { resultPath }) : defaultRequestReview(pr, intent, cwd, { resultPath }));
|
|
337
364
|
} catch (error) {
|
|
338
365
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -344,13 +371,155 @@ export async function executeLiveReview(
|
|
|
344
371
|
return observe(deps, withRepo({ kind, executed: true, card: card.id, ok: true, detail: `review requested for #${pr} head ${head.head.slice(0, 12)}`, resultPath }));
|
|
345
372
|
}
|
|
346
373
|
|
|
374
|
+
/**
|
|
375
|
+
* TA-LAND-RETRY-ON-INFRA — land 실패를 둘로 가른다.
|
|
376
|
+
*
|
|
377
|
+
* - 왜: «같은 머리에 land 한 번»이 TA 쪽 인프라 결손(호스트에 PR 머리 트리가 없어 `✗ expected-head: … matching local HEAD and clean
|
|
378
|
+
* worktree required`)에 그 한 번을 써 버렸고, 결손을 고친(#26037) 뒤에도 같은 머리는 `already attempted` 로 막혀 사람이 머리를
|
|
379
|
+
* 흔들어야 했다(2026-10-11 #26030 · 카드 ta-20261010-c6b53b).
|
|
380
|
+
* - `infra` — 아래 «명시적 허용 목록»에 있는 사유만. 같은 머리에 한 번 더(머리당 총 `LAND_ATTEMPTS_PER_HEAD_ON_INFRA`).
|
|
381
|
+
* - `judgement` — 그 밖 전부(must-fix · 겹침 관문 · 머리 바뀜 · 동결 · 게이트 · 충돌 · 목록에 없는 ✗ 줄 · 기록 없는 옛 시도) — 종전대로 한 번 끝(fail-closed).
|
|
382
|
+
* - 판독 근거: `pr land` 의 실제 출력 줄(`src/cli/pr-cli.ts` 의 `✗ <관문>:` 문면 그대로)과 `executeLiveLand` 의 사유.
|
|
383
|
+
* - 임시 워크트리 «생성 실패»는 여기 오지 않는다 — #26037 이 land 를 부르지 않고 시도 기록을 «되돌린다»(시도를 안 쓴다).
|
|
384
|
+
* - `base-stale` — REJUDGE base-stale(OVERLAP-EVIDENCE-BASE-WINDOW 짝 · #26060): 겹침 증거의 게이트 base 뒤에 main 이 «PR 파일을 건드리며»
|
|
385
|
+
* 움직였다(`✗ overlap-evidence: base-stale-overlap: …` ⊕ 딸린 `✗ expected-head: overlap merge approval unavailable …` 줄 «만»).
|
|
386
|
+
* 같은 머리 «즉시» 재시도는 없다(`retryAllowed:false`) — 재판정 틱이 새 base 위 재게이트를 청하고, 그 재게이트가 «통과한 뒤»
|
|
387
|
+
* (`executeLiveLand` ctx `gateAt` = 그 재게이트 요청 시각)에만 이 시도는 «소비됨»으로 빠진다. judgement·infra 와 섞이지 않는다.
|
|
388
|
+
*/
|
|
389
|
+
export const LAND_ATTEMPTS_PER_HEAD_ON_INFRA = 2;
|
|
390
|
+
export type LandFailureClass = 'infra' | 'judgement' | 'base-stale';
|
|
391
|
+
|
|
392
|
+
/** `pr land` 출력 줄 → 인프라 사유. ⛔ 줄 «전체»가 맞아야 한다 — 비슷한 문면을 넓게 받지 않는다. */
|
|
393
|
+
export const LAND_INFRA_OUTPUT_REASONS: ReadonlyArray<{ reason: string; line: RegExp }> = [
|
|
394
|
+
// 머리 고정 사전 조건 — gh 로 본 PR(OPEN·non-draft·머리)은 executeLiveLand 가 «직전에» 확인했다 · 남는 원인은 로컬 HEAD·더러운 트리·gh 조회 실패.
|
|
395
|
+
{ reason: 'precondition-local-head-clean-tree', line: /^✗ expected-head: PR OPEN non-draft, matching local HEAD and clean worktree required$/ },
|
|
396
|
+
{ reason: 'precondition-branch', line: /^✗ branch: (?:PR head branch와 현재 브랜치가 다릅니다\.|현재 브랜치를 해석하지 못했습니다\.)$/ },
|
|
397
|
+
{ reason: 'base-unresolved', line: /^✗ base: 비교 base를 선택하지 못했습니다\.$/ },
|
|
398
|
+
{ reason: 'git-read-failed', line: /^✗ granularity: (?:비교 base를 해석하지 못했습니다\.|로컬 git log 를 읽지 못했습니다\.)$/ },
|
|
399
|
+
{ reason: 'gh-lookup-failed', line: /^✗ find: / },
|
|
400
|
+
{ reason: 'gh-merge-state-unknown', line: /^✗ merge: squash merge (?:결과를 모름 \(상태 미확정\)|상태 되읽기 실패)$/ },
|
|
401
|
+
// defaultLand — `pr land` 가 0 으로 끝났는데 병합 상태를 gh 로 다시 못 읽었다(병합됐으면 다음 시도는 MERGED 로 관문 앞에서 멈춘다).
|
|
402
|
+
{ reason: 'gh-merge-state-unreadable', line: /^✗ ta-land: merge state unreadable after pr land exit 0$/ },
|
|
403
|
+
];
|
|
404
|
+
/** 인프라 사유에 «딸려 나오는» 줄 — 그 사유가 같이 있을 때만 판정 거절로 세지 않는다. */
|
|
405
|
+
const LAND_INFRA_CONSEQUENCE_LINES: ReadonlyArray<{ of: string; line: RegExp }> = [
|
|
406
|
+
{ of: 'gh-lookup-failed', line: /^✗ upsert-ready: gh: pinned PR is not the branch open PR$/ },
|
|
407
|
+
];
|
|
408
|
+
/** REJUDGE base-stale — 겹침 증거의 게이트 base 가 낡았고 그 사이 착지가 PR 파일을 건드렸다(`pr land` · #26060 문면). */
|
|
409
|
+
const LAND_BASE_STALE_LINE = /^✗ overlap-evidence: (base-stale-overlap: .*)$/;
|
|
410
|
+
/** base-stale 에 «딸려 나오는» 줄 — 증거가 거절되면 `pr land` 가 이어서 낸다(그 줄만 있으면 판정 거절로 세지 않는다). */
|
|
411
|
+
const LAND_BASE_STALE_CONSEQUENCE_LINES: readonly RegExp[] = [/^✗ expected-head: overlap merge approval unavailable for the reviewed head$/];
|
|
412
|
+
/** 실패가 아닌 ✗ 줄 — `pr land` 가 «계속합니다»로 넘어간다(판정에 쓰지 않는다). */
|
|
413
|
+
const LAND_NON_FATAL_LINES: readonly RegExp[] = [/^✗ active-run-files: .*계속합니다\.?$/];
|
|
414
|
+
/** executeLiveLand 쪽 인프라 사유(출력 줄이 아니라 실행 사실). */
|
|
415
|
+
export const LAND_INFRA_EXECUTOR_REASONS = ['land-process-error', 'land-no-result-line'] as const;
|
|
416
|
+
const [LAND_PROCESS_ERROR, LAND_NO_RESULT_LINE] = LAND_INFRA_EXECUTOR_REASONS;
|
|
417
|
+
|
|
418
|
+
const RESULT_MARK = /^(?:\[dry-run\] )?[✓✗⚠⏳]/u;
|
|
419
|
+
|
|
420
|
+
export interface LandFailureClassification { class: LandFailureClass; reason: string }
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 실패한 land 의 출력(또는 던진 오류)을 가른다 — 목록에 없는 ✗ 줄이 «하나라도» 있으면 판정 거절이다.
|
|
424
|
+
* `thrown` = land 를 띄우거나 기다리다 던졌다(프로세스 오류).
|
|
425
|
+
*/
|
|
426
|
+
export function classifyLandFailure(outcome: { status: number; stdout?: string; stderr?: string } | { thrown: string }): LandFailureClassification {
|
|
427
|
+
if ('thrown' in outcome) return { class: 'infra', reason: LAND_PROCESS_ERROR };
|
|
428
|
+
const lines = `${outcome.stdout ?? ''}\n${outcome.stderr ?? ''}`.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
429
|
+
const marked = lines.filter((line) => RESULT_MARK.test(line));
|
|
430
|
+
// 결과 줄이 «하나도» 없이 0 아닌 코드로 끝났다 — 관문까지 못 간 채 죽었다(크래시 · 신호).
|
|
431
|
+
if (!marked.length) return outcome.status !== 0 ? { class: 'infra', reason: LAND_NO_RESULT_LINE } : { class: 'judgement', reason: 'no-failure-line' };
|
|
432
|
+
const failures = marked.map((line) => line.replace(/^\[dry-run\] /, '')).filter((line) => line.startsWith('✗') && !LAND_NON_FATAL_LINES.some((re) => re.test(line)));
|
|
433
|
+
const infra = new Set<string>();
|
|
434
|
+
const unlisted: string[] = [];
|
|
435
|
+
for (const line of failures) {
|
|
436
|
+
const hit = LAND_INFRA_OUTPUT_REASONS.find((entry) => entry.line.test(line));
|
|
437
|
+
if (hit) infra.add(hit.reason); else unlisted.push(line);
|
|
438
|
+
}
|
|
439
|
+
const judged = unlisted.filter((line) => !LAND_INFRA_CONSEQUENCE_LINES.some((entry) => infra.has(entry.of) && entry.line.test(line)));
|
|
440
|
+
// REJUDGE base-stale — 거절 줄이 base-stale-overlap(⊕ 딸린 줄) «뿐»이고 인프라 사유가 없을 때만. 하나라도 다른 ✗ 줄이 섞이면 judgement(fail-closed).
|
|
441
|
+
const stale = judged.map((line) => LAND_BASE_STALE_LINE.exec(line)?.[1]).find((reason): reason is string => !!reason);
|
|
442
|
+
if (stale && !infra.size && judged.every((line) => LAND_BASE_STALE_LINE.test(line) || LAND_BASE_STALE_CONSEQUENCE_LINES.some((re) => re.test(line)))) {
|
|
443
|
+
return { class: 'base-stale', reason: stale.slice(0, 160) };
|
|
444
|
+
}
|
|
445
|
+
if (judged.length) return { class: 'judgement', reason: judged[0]!.slice(0, 120) };
|
|
446
|
+
if (infra.size) return { class: 'infra', reason: [...infra].join('+') };
|
|
447
|
+
return { class: 'judgement', reason: 'no-failure-line' };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
type LandAttempt = NonNullable<TaskCard['landAttempts']>[number];
|
|
451
|
+
type LandWorktreeCreateFailure = { pr: number; head: string; at: string; reason: string };
|
|
452
|
+
declare module './task-hand.js' {
|
|
453
|
+
interface TaskCard { landWorktreeCreateFailures?: LandWorktreeCreateFailure[] }
|
|
454
|
+
}
|
|
455
|
+
const sameHeadOf = (pr: number, head: string) => (entry: LandAttempt) => entry.pr === pr && entry.head.toLowerCase() === head.toLowerCase();
|
|
456
|
+
|
|
457
|
+
function landWorktreeCreateCap(): number {
|
|
458
|
+
try {
|
|
459
|
+
const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { tasks?: { landWorktreeCreateCap?: unknown } };
|
|
460
|
+
const configured = config.tasks?.landWorktreeCreateCap;
|
|
461
|
+
return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0 ? configured : 3;
|
|
462
|
+
} catch { return 3; }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function consecutiveLandWorktreeCreateFailures(card: TaskCard, pr: number, head: string): LandWorktreeCreateFailure[] {
|
|
466
|
+
const entries = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr === pr);
|
|
467
|
+
const last = entries.at(-1);
|
|
468
|
+
return last?.head.toLowerCase() === head.toLowerCase()
|
|
469
|
+
? entries.filter((entry) => entry.head.toLowerCase() === head.toLowerCase())
|
|
470
|
+
: [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function recordLandWorktreeCreateFailure(card: TaskCard, failure: LandWorktreeCreateFailure): TaskCard {
|
|
474
|
+
const samePr = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr !== failure.pr);
|
|
475
|
+
const samePrHead = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr === failure.pr && entry.head.toLowerCase() === failure.head.toLowerCase());
|
|
476
|
+
return { ...card, landWorktreeCreateFailures: [...samePr, ...samePrHead, failure] };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function clearLandWorktreeCreateFailures(card: TaskCard, pr: number): TaskCard {
|
|
480
|
+
const remaining = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr !== pr);
|
|
481
|
+
const { landWorktreeCreateFailures: _removed, ...rest } = card;
|
|
482
|
+
return { ...rest, ...(remaining.length ? { landWorktreeCreateFailures: remaining } : {}) };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function cappedLandWorktreeReason(failures: readonly LandWorktreeCreateFailure[], cap: number): string {
|
|
486
|
+
return `land worktree create capped (${failures.length}/${cap}): ${failures.at(-1)?.reason ?? 'unknown'}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function observeLandWorktreeCreateCapped(deps: LiveMoveDeps, data: { card: string; pr: number; head: string; failures: number; cap: number; reason: string }): void {
|
|
490
|
+
try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'land-worktree-create-capped', data); } catch { /* fail-soft */ }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** 같은 머리에 한 번 더 시도해도 되나 — 앞 시도가 «전부» 끝났고(ok:false) «전부» infra 이고 상한 밑일 때만. */
|
|
494
|
+
function landRetryAllowed(prior: readonly LandAttempt[]): boolean {
|
|
495
|
+
return prior.length > 0 && prior.length < LAND_ATTEMPTS_PER_HEAD_ON_INFRA && prior.every((entry) => entry.ok === false && entry.class === 'infra');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* REJUDGE base-stale — `gateAt`(새 base 위 재게이트를 «요청한» 시각) 앞의 base-stale 시도는 그 재게이트가 통과해 소비됐다 —
|
|
500
|
+
* 같은 머리 land 판단에서 뺀다. 나머지(infra·judgement·그 뒤의 시도)는 종전 규칙 그대로.
|
|
501
|
+
*/
|
|
502
|
+
export function openLandAttempts<T extends Pick<LandAttempt, 'at' | 'class'>>(prior: readonly T[], gateAt?: string): T[] {
|
|
503
|
+
if (!gateAt) return [...prior];
|
|
504
|
+
const since = Date.parse(gateAt);
|
|
505
|
+
return prior.filter((entry) => !(entry.class === 'base-stale' && Number.isFinite(since) && Date.parse(entry.at) < since));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** 카드의 이 PR·머리에 인프라 재시도가 남았나(재판정 틱이 «머리 그대로»여도 land 단계까지 가게). */
|
|
509
|
+
export function landRetryPending(card: Pick<TaskCard, 'landAttempts'>, pr: number, head: string): boolean {
|
|
510
|
+
return landRetryAllowed((card.landAttempts ?? []).filter(sameHeadOf(pr, head)));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const LAND_RETRY_ON_INFRA_EVENT = 'land-retry-on-infra';
|
|
514
|
+
|
|
347
515
|
const landedCycles = new Set<string>();
|
|
348
516
|
|
|
349
517
|
/** `propose-land` — only a passing self review (pass or warn, with must-fix 0) of this exact OPEN, non-draft head may invoke pr land. */
|
|
350
518
|
export async function executeLiveLand(
|
|
351
519
|
card: TaskCard | undefined, pr: number | undefined,
|
|
352
520
|
ctx: { runId?: string | null; cycleId?: string; cwd?: string; produced?: { headCommit?: string; branch?: string; prUrl?: string }; review?: { verdict: 'pass' | 'warn' | 'fail'; head: string; /** TA-LAND-WARN-MUSTFIX0 — `false` 면 리뷰가 실제로 안 돌았다 → land 거부. 슈퍼바이저 입력은 reviewed 리뷰로만 만들어져 칸이 없다. */ reviewed?: boolean };
|
|
353
|
-
/** TA-REJUDGE-ON-HEAD — 겹침 증거 파일(있으면 land 에 넘길 뿐 · 판정은 그대로). */ overlapEvidence?: string
|
|
521
|
+
/** TA-REJUDGE-ON-HEAD — 겹침 증거 파일(있으면 land 에 넘길 뿐 · 판정은 그대로). */ overlapEvidence?: string;
|
|
522
|
+
/** REJUDGE base-stale — 증거의 재게이트를 요청한 시각 · 그 앞의 base-stale 시도는 소비됨(`openLandAttempts`). */ gateAt?: string },
|
|
354
523
|
deps: LiveMoveDeps = {},
|
|
355
524
|
): Promise<LiveMoveResult> {
|
|
356
525
|
const kind = 'land' as const;
|
|
@@ -390,12 +559,26 @@ export async function executeLiveLand(
|
|
|
390
559
|
if (cycle && landedCycles.has(cycle)) return reject(card.id, `cycle ${cycle} already landed — stays shadow`, reviewRepo);
|
|
391
560
|
const path = deps.statePath ?? taskAgentStatePath();
|
|
392
561
|
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
562
|
+
const local = (deps.landWorktree?.localHead ?? localHead)(cwd);
|
|
563
|
+
const needsLandWorktree = !!local && local.toLowerCase() !== view.head.toLowerCase();
|
|
564
|
+
const createCap = landWorktreeCreateCap();
|
|
393
565
|
let claimed = false;
|
|
394
566
|
let missing = false;
|
|
567
|
+
let cappedReason: string | undefined;
|
|
568
|
+
// TA-LAND-RETRY-ON-INFRA — 같은 머리의 앞 시도들(잠금 안에서 읽은 «지금» 카드) · 재시도면 그 근거.
|
|
569
|
+
let prior: LandAttempt[] = [];
|
|
395
570
|
try {
|
|
396
571
|
updateTaskCard(path, card.id, (current) => {
|
|
397
572
|
if (!current) { missing = true; return undefined; }
|
|
398
|
-
if (
|
|
573
|
+
if (needsLandWorktree) {
|
|
574
|
+
const failures = consecutiveLandWorktreeCreateFailures(current, pr, view!.head);
|
|
575
|
+
if (failures.length >= createCap) {
|
|
576
|
+
cappedReason = cappedLandWorktreeReason(failures, createCap);
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
prior = openLandAttempts((current.landAttempts ?? []).filter(sameHeadOf(pr, view!.head)), ctx.gateAt);
|
|
581
|
+
if (prior.length && !landRetryAllowed(prior)) return undefined;
|
|
399
582
|
claimed = true;
|
|
400
583
|
return { ...current, landAttempts: [...(current.landAttempts ?? []), { pr, head: view!.head, at }] };
|
|
401
584
|
});
|
|
@@ -403,14 +586,23 @@ export async function executeLiveLand(
|
|
|
403
586
|
return reject(card.id, `land attempt could not be recorded: ${String(error).slice(-250)}`, reviewRepo);
|
|
404
587
|
}
|
|
405
588
|
if (missing) return reject(card.id, 'task card not in state file', reviewRepo);
|
|
406
|
-
if (
|
|
589
|
+
if (cappedReason) return reject(card.id, cappedReason, reviewRepo);
|
|
590
|
+
if (!claimed) {
|
|
591
|
+
const last = prior.at(-1);
|
|
592
|
+
const why = last ? ` (attempts ${prior.length} · last ${last.ok === false ? `class ${last.class ?? 'unclassified'}${last.reason ? ` ${last.reason}` : ''}` : last.ok === true ? 'ok' : 'unfinished'})` : '';
|
|
593
|
+
return observe(deps, withRepo({ kind, card: card.id, ok: true, executed: false, detail: `already attempted land for #${pr} head ${view.head.slice(0, 12)}${why}` }));
|
|
594
|
+
}
|
|
595
|
+
const attempt = prior.length + 1;
|
|
596
|
+
if (attempt > 1) {
|
|
597
|
+
const last = prior.at(-1)!;
|
|
598
|
+
try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', LAND_RETRY_ON_INFRA_EVENT, { card: card.id, pr, head: view.head, attempt, reason: last.reason ?? 'unknown', class: 'infra', decision: 'retrying' }); } catch { /* fail-soft */ }
|
|
599
|
+
}
|
|
407
600
|
if (cycle) landedCycles.add(cycle);
|
|
408
601
|
// TA-LAND-WORKTREE — `pr land --expected-head` 는 «로컬 HEAD = 고정 머리»를 요구한다. cwd 가 다른 머리면(Pod 런 PR · 호스트 checkout)
|
|
409
602
|
// PR 머리의 임시 워크트리에서 부르고 끝나면 걷는다. 같은 머리(로컬 런 작업 트리)거나 HEAD 를 못 읽으면 종전 그대로 cwd 에서.
|
|
410
603
|
let landCwd = cwd;
|
|
411
604
|
let worktree: LandWorktree | undefined;
|
|
412
|
-
|
|
413
|
-
if (local && local.toLowerCase() !== view.head.toLowerCase()) {
|
|
605
|
+
if (needsLandWorktree && local) {
|
|
414
606
|
let made: LandWorktree | LandWorktreeFailure;
|
|
415
607
|
try {
|
|
416
608
|
made = await (deps.landWorktree?.create ?? createLandWorktree)({ card: card.id, pr, head: view.head, branch: view.branch, repoCwd: cwd, statePath: path });
|
|
@@ -418,6 +610,15 @@ export async function executeLiveLand(
|
|
|
418
610
|
if ('reason' in made) {
|
|
419
611
|
const reason = made.reason;
|
|
420
612
|
observeLandWorktree({ card: card.id, pr, head: view.head, created: made.created, removed: made.removed, reason }, deps.log);
|
|
613
|
+
let failures: LandWorktreeCreateFailure[] = [];
|
|
614
|
+
let recordFailureError: unknown;
|
|
615
|
+
try {
|
|
616
|
+
updateTaskCard(path, card.id, (current) => {
|
|
617
|
+
if (!current) return undefined;
|
|
618
|
+
failures = [...consecutiveLandWorktreeCreateFailures(current, pr, view!.head), { pr, head: view!.head, at, reason }];
|
|
619
|
+
return recordLandWorktreeCreateFailure(current, { pr, head: view!.head, at, reason });
|
|
620
|
+
});
|
|
621
|
+
} catch (error) { recordFailureError = error; }
|
|
421
622
|
// land 를 부르지 않았다 — 이 머리의 시도 기록과 주기 표시를 되돌려 다음 틱이 다시 시도할 수 있게 한다.
|
|
422
623
|
if (cycle) landedCycles.delete(cycle);
|
|
423
624
|
try {
|
|
@@ -429,16 +630,30 @@ export async function executeLiveLand(
|
|
|
429
630
|
observeLandWorktree({ card: card.id, pr, head: view.head, created: made.created, removed: made.removed, reason: `land attempt release failed: ${String(error).slice(-200)}` }, deps.log);
|
|
430
631
|
return reject(card.id, `PR #${pr} land worktree unavailable: ${reason} · attempt release failed (head stays attempted)`, reviewRepo);
|
|
431
632
|
}
|
|
633
|
+
if (recordFailureError) return reject(card.id, `PR #${pr} land worktree unavailable: ${reason} · failure record failed: ${String(recordFailureError).slice(-200)}`, reviewRepo);
|
|
634
|
+
if (failures.length >= createCap) {
|
|
635
|
+
const capped = cappedLandWorktreeReason(failures, createCap);
|
|
636
|
+
if (failures.length === createCap) observeLandWorktreeCreateCapped(deps, { card: card.id, pr, head: view.head, failures: failures.length, cap: createCap, reason });
|
|
637
|
+
return reject(card.id, capped, reviewRepo);
|
|
638
|
+
}
|
|
432
639
|
return reject(card.id, `PR #${pr} land worktree unavailable: ${reason}`, reviewRepo);
|
|
433
640
|
}
|
|
434
641
|
worktree = made;
|
|
435
642
|
landCwd = made.cwd;
|
|
643
|
+
// 실패 장부 정리는 fail-soft — 정리 쓰기 실패로 정상 착지를 막지 않는다(막으면 잡아 둔 landAttempts 가 남아 그 head 가 영영 «already attempted»).
|
|
644
|
+
try {
|
|
645
|
+
updateTaskCard(path, card.id, (current) => current?.landWorktreeCreateFailures?.some((entry) => entry.pr === pr)
|
|
646
|
+
? clearLandWorktreeCreateFailures(current, pr) : undefined);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
try { (deps.log ?? ((c, e, d) => debug.log(c, e, d, { level: 'warn' })))('task-agent', 'land-worktree-create-failures-clear-failed', { card: card.id, pr, head: view.head, error: String(error).slice(-200) }); } catch { /* fail-soft */ }
|
|
649
|
+
}
|
|
436
650
|
observeLandWorktree({ card: card.id, pr, head: view.head, created: true, removed: false, reason: `cwd HEAD ${local.slice(0, 12)} ≠ PR head ${view.head.slice(0, 12)}` }, deps.log);
|
|
437
651
|
}
|
|
438
652
|
// 증거 경로는 «원래» cwd 기준으로 고정한다 — 임시 트리로 cwd 를 바꿔도 상대경로가 다른 파일을 가리키지 않게(같은 cwd 면 그대로).
|
|
439
653
|
const overlapEvidence = ctx.overlapEvidence && worktree ? resolve(cwd, ctx.overlapEvidence) : ctx.overlapEvidence;
|
|
440
654
|
let ok = false;
|
|
441
655
|
let detail: string;
|
|
656
|
+
let failure: LandFailureClassification | undefined;
|
|
442
657
|
try {
|
|
443
658
|
// The CLI rechecks the pinned PR/head immediately before merge, after its own gates and freeze admission.
|
|
444
659
|
const outcome = overlapEvidence
|
|
@@ -446,7 +661,14 @@ export async function executeLiveLand(
|
|
|
446
661
|
: await (deps.land ?? defaultLand)(pr, view.head, landCwd);
|
|
447
662
|
ok = outcome.status === 0;
|
|
448
663
|
detail = (ok ? outcome.stdout || `land merged #${pr} head ${view.head.slice(0, 12)}` : outcome.stderr || outcome.stdout || `exit ${outcome.status}`).slice(-300);
|
|
449
|
-
|
|
664
|
+
if (!ok) failure = classifyLandFailure(outcome);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
detail = (error instanceof Error ? error.message : String(error)).slice(-300);
|
|
667
|
+
failure = classifyLandFailure({ thrown: detail });
|
|
668
|
+
}
|
|
669
|
+
const retryAllowed = failure?.class === 'infra' && attempt < LAND_ATTEMPTS_PER_HEAD_ON_INFRA;
|
|
670
|
+
// 인프라 실패로 재시도가 남으면 주기 표시도 푼다 — 같은 주기 id(재판정 틱의 `rejudge:<card>:<head>`)가 그 한 번을 막지 않게.
|
|
671
|
+
if (retryAllowed && cycle) landedCycles.delete(cycle);
|
|
450
672
|
if (worktree) {
|
|
451
673
|
// 성공이든 실패든 걷는다.
|
|
452
674
|
let removal: LandWorktreeRemoval;
|
|
@@ -454,15 +676,38 @@ export async function executeLiveLand(
|
|
|
454
676
|
observeLandWorktree({ card: card.id, pr, head: view.head, created: true, removed: removal.removed, ...(removal.branchRemoved !== undefined ? { branchRemoved: removal.branchRemoved } : {}),
|
|
455
677
|
reason: removal.reason ?? `land ${ok ? 'ok' : 'failed'} — removed` }, deps.log);
|
|
456
678
|
}
|
|
679
|
+
const landAttempt: LiveMoveResult['landAttempt'] = failure
|
|
680
|
+
? { attempt, class: failure.class, reason: failure.reason, retryAllowed }
|
|
681
|
+
: { attempt };
|
|
682
|
+
if (failure) {
|
|
683
|
+
try {
|
|
684
|
+
(deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', LAND_RETRY_ON_INFRA_EVENT, {
|
|
685
|
+
card: card.id, pr, head: view.head, attempt, reason: failure.reason, class: failure.class,
|
|
686
|
+
decision: retryAllowed ? 'retry-allowed' : failure.class === 'infra' ? 'retry-exhausted' : failure.class === 'base-stale' ? 'base-stale-awaiting-regate' : 'no-retry',
|
|
687
|
+
});
|
|
688
|
+
} catch { /* fail-soft */ }
|
|
689
|
+
}
|
|
457
690
|
try {
|
|
458
691
|
updateTaskCard(path, card.id, (current) => current?.landAttempts?.some(entry => entry.pr === pr && entry.head === view!.head && entry.at === at)
|
|
459
|
-
? {
|
|
692
|
+
? {
|
|
693
|
+
...current,
|
|
694
|
+
landAttempts: current.landAttempts.map(entry => entry.pr === pr && entry.head === view!.head && entry.at === at
|
|
695
|
+
? { ...entry, ok, detail, ...(failure ? { class: failure.class, reason: failure.reason } : {}) } : entry),
|
|
696
|
+
// TA-LAND-RETRY-ON-INFRA — 실패한 시도마다 카드 history 한 줄(logs 창이 지나도 «왜 다시 했나/안 했나»가 남게).
|
|
697
|
+
...(failure ? { history: [...(current.history ?? []), {
|
|
698
|
+
at, event: LAND_RETRY_ON_INFRA_EVENT, kind: failure.class, pr, head: view!.head,
|
|
699
|
+
effect: retryAllowed ? 'retry-allowed' : failure.class === 'infra' ? 'retry-exhausted' : failure.class === 'base-stale' ? 'awaiting-regate' : 'no-retry',
|
|
700
|
+
detail: failure.class === 'base-stale'
|
|
701
|
+
? `attempt ${attempt} · base-stale → 재게이트 대기 (land again only after a re-gate on a new base passes) · ${failure.reason}`
|
|
702
|
+
: `attempt ${attempt}/${failure.class === 'infra' ? LAND_ATTEMPTS_PER_HEAD_ON_INFRA : attempt} · ${failure.reason}`,
|
|
703
|
+
}] } : {}),
|
|
704
|
+
}
|
|
460
705
|
: undefined);
|
|
461
706
|
} catch (error) {
|
|
462
|
-
return observe(deps, withRepo({ kind, card: card.id, ok: false, executed: true,
|
|
707
|
+
return observe(deps, withRepo({ kind, card: card.id, ok: false, executed: true, landAttempt,
|
|
463
708
|
detail: `land ${ok ? 'succeeded' : 'failed'} but card update failed: ${String(error).slice(-230)}` }));
|
|
464
709
|
}
|
|
465
|
-
return observe(deps, withRepo({ kind, card: card.id, ok, executed: true, detail }));
|
|
710
|
+
return observe(deps, withRepo({ kind, card: card.id, ok, executed: true, detail, landAttempt }));
|
|
466
711
|
}
|
|
467
712
|
|
|
468
713
|
/** `propose-green` — 비지 않은 근거가 있을 때만 카드에 제안을 적는다(한 번). 체크리스트는 쓰지 않는다. */
|
|
@@ -484,3 +729,88 @@ export function executeLiveGreenProposal(card: TaskCard | undefined, evidence: R
|
|
|
484
729
|
if (!wrote.proposed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: 'already proposed' });
|
|
485
730
|
return observe(deps, { kind, executed: true, card: card.id, ok: true, detail: `green proposed (${Object.values(refs).join(', ')}) · checklist untouched` });
|
|
486
731
|
}
|
|
732
|
+
|
|
733
|
+
export interface LiveDecisionCardContext {
|
|
734
|
+
runId: string | null; stopReason: string; reason: string; pr?: number;
|
|
735
|
+
risk?: NextAction['risk'];
|
|
736
|
+
/** 서명 · 화면 · must-fix — 하나도 비지 않아야 카드가 올라간다. */
|
|
737
|
+
evidence: DecisionCardEvidence;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const DECISION_SIGNATURE_KEEP = 300;
|
|
741
|
+
const DECISION_SCREEN_KEEP = 1500;
|
|
742
|
+
|
|
743
|
+
/** 같은 런 · 사유 · 근거면 같은 지문 — 카드 history 의 «이미 올렸다» 판정 키. */
|
|
744
|
+
export function decisionCardFingerprint(runId: string | null, reason: string, evidence: { signature?: string; screen?: string; mustFix: readonly string[] }): string {
|
|
745
|
+
return createHash('sha1').update(JSON.stringify([runId, reason, evidence.signature ?? null, evidence.screen ?? null, evidence.mustFix])).digest('hex').slice(0, 12);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** `decisions raise` 를 같은 우주의 CLI 자식으로 한 번 부른다 — 카드는 결정 원장에 들어가 OP 거르기가 읽는다. */
|
|
749
|
+
export async function defaultRaiseDecision(args: string[]): Promise<{ status: number; stdout: string; stderr?: string }> {
|
|
750
|
+
const { ELANOUS_CLI_ENTRY } = await import('../cli/tasks-cli.js');
|
|
751
|
+
const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
|
|
752
|
+
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
753
|
+
const argv = [resolve(ELANOUS_CLI_ENTRY), '--config-dir', getElanousConfigDirOverride() ?? effectiveInstanceRoot(), ...args];
|
|
754
|
+
return new Promise((resolveResult, reject) => {
|
|
755
|
+
const child = spawn(process.execPath, argv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
756
|
+
let stdout = '';
|
|
757
|
+
let stderr = '';
|
|
758
|
+
child.stdout.on('data', (chunk: Buffer) => { stdout = (stdout + chunk.toString()).slice(-300); });
|
|
759
|
+
child.stderr.on('data', (chunk: Buffer) => { stderr = (stderr + chunk.toString()).slice(-300); });
|
|
760
|
+
child.once('error', reject);
|
|
761
|
+
child.once('close', (code) => resolveResult({ status: code ?? 1, stdout, stderr }));
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* `decision-card` — 막힌 과제를 근거 붙은 결정 카드로 올린다. 근거(서명 · 화면 · must-fix)가 하나도 없으면 «올리지 않는다».
|
|
767
|
+
* 같은 지문의 카드는 한 번만(카드 history 의 `live-move` 줄 · 먼저 적고 올린다) · 올리기 실패는 줄을 걷어 다음 틱이 다시 시도한다.
|
|
768
|
+
*/
|
|
769
|
+
export async function executeLiveDecisionCard(card: TaskCard | undefined, ctx: LiveDecisionCardContext, deps: LiveMoveDeps = {}): Promise<LiveMoveResult> {
|
|
770
|
+
const kind = 'decision-card' as const;
|
|
771
|
+
if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
|
|
772
|
+
const grounds = decisionCardGrounds(ctx.evidence);
|
|
773
|
+
if (!grounds.length) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'no evidence (signature · screen · must-fix) — no card raised' });
|
|
774
|
+
const signature = ctx.evidence.signature?.trim().slice(0, DECISION_SIGNATURE_KEEP) || undefined;
|
|
775
|
+
const screen = ctx.evidence.screen?.trim().slice(-DECISION_SCREEN_KEEP) || undefined;
|
|
776
|
+
const mustFix = (ctx.evidence.mustFix ?? []).filter((fix) => typeof fix === 'string' && fix.trim()).map((fix) => fix.trim());
|
|
777
|
+
const mark = `fp:${decisionCardFingerprint(ctx.runId, ctx.reason, { ...(signature ? { signature } : {}), ...(screen ? { screen } : {}), mustFix })}`;
|
|
778
|
+
const path = deps.statePath ?? taskAgentStatePath();
|
|
779
|
+
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
780
|
+
const ours = (entry: TaskCard['history'][number]) => entry.event === LIVE_MOVE_HISTORY_EVENT && entry.kind === kind && !!entry.detail?.includes(mark);
|
|
781
|
+
const claim = { claimed: false, missing: false };
|
|
782
|
+
updateTaskCard(path, card.id, (current) => {
|
|
783
|
+
if (!current) { claim.missing = true; return undefined; }
|
|
784
|
+
if ((current.history ?? []).some(ours)) return undefined;
|
|
785
|
+
claim.claimed = true;
|
|
786
|
+
return { ...current, history: [...(current.history ?? []), {
|
|
787
|
+
at, event: LIVE_MOVE_HISTORY_EVENT, kind, executed: false, executorResult: 'live' as const,
|
|
788
|
+
detail: `decision card claimed · ${mark}`, ...(ctx.runId ? { runId: ctx.runId } : {}), ...(ctx.pr !== undefined ? { pr: ctx.pr } : {}),
|
|
789
|
+
effect: 'none — decision card claimed, not yet raised',
|
|
790
|
+
}] };
|
|
791
|
+
});
|
|
792
|
+
if (claim.missing) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' });
|
|
793
|
+
if (!claim.claimed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: `already raised for ${mark}` });
|
|
794
|
+
const evidence = JSON.stringify({
|
|
795
|
+
card: card.id, runId: ctx.runId, pr: ctx.pr ?? null, stopReason: ctx.stopReason, grounds,
|
|
796
|
+
...(signature ? { signature } : {}), ...(screen ? { screen } : {}), mustFix, selected: kind, reason: ctx.reason,
|
|
797
|
+
});
|
|
798
|
+
const args = decisionRaiseArgs({ taskId: card.id, reason: ctx.reason, ...(ctx.risk ? { risk: ctx.risk } : {}), evidence });
|
|
799
|
+
let failure: string | undefined;
|
|
800
|
+
try {
|
|
801
|
+
const outcome = await (deps.raiseDecision ?? defaultRaiseDecision)(args);
|
|
802
|
+
if (outcome.status !== 0) failure = (outcome.stderr || outcome.stdout || `exit ${outcome.status}`).slice(-250);
|
|
803
|
+
} catch (error) { failure = (error instanceof Error ? error.message : String(error)).slice(-250); }
|
|
804
|
+
const mine = (entry: TaskCard['history'][number]) => ours(entry) && entry.at === at;
|
|
805
|
+
const detail = failure ? `decisions raise failed: ${failure}` : `decision card raised (${grounds.join(' · ')}) · ${mark}`;
|
|
806
|
+
try {
|
|
807
|
+
updateTaskCard(path, card.id, (current) => current?.history?.some(mine)
|
|
808
|
+
? { ...current, history: failure
|
|
809
|
+
? current.history.filter((entry) => !mine(entry))
|
|
810
|
+
: current.history.map((entry) => mine(entry) ? { ...entry, executed: true, ok: true, detail, effect: `decision card raised · grounds ${grounds.join('+')} · decision ledger (OP filter reads it)` } : entry) }
|
|
811
|
+
: undefined);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
return observe(deps, { kind, executed: !failure, card: card.id, ok: false, detail: `${detail} but card update failed: ${String(error).slice(-200)}` });
|
|
814
|
+
}
|
|
815
|
+
return observe(deps, { kind, executed: !failure, card: card.id, ok: !failure, detail });
|
|
816
|
+
}
|