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
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
* ③ land: 재게이트 통과 ⊕ 회수한 리뷰가 지금 머리의 pass «또는» warn ⊕ reviewed ⊕ must-fix 0(TA-LAND-WARN-MUSTFIX0)이면 겹침 증거 파일
|
|
13
13
|
* (`<state>/overlap-evidence/<card>-<pr>-<head12>.json` = {pr, head, gate:{head, baseCommit, passed}, reviewResult} · #26023 꼴)을
|
|
14
14
|
* 쓰고 `executeLiveLand` 를 «다시 부른다»(판정 로직 무변경 · 증거를 완전히 못 갖추면 land 하지 않는다 — fail-closed).
|
|
15
|
-
*
|
|
15
|
+
* LAND-ORDER-LEDGER — executeLiveLand 직후 겹침 쌍·착지 순서 원장(`land-order-ledger.ts` · `<state>/land-order-ledger.jsonl`)에
|
|
16
|
+
* 한 줄을 붙인다(같은 pr+head 한 줄 · 쓰기 실패는 삼킨다 — land 결과 무변 · 관측 `task-agent.land-order` appended|append-failed).
|
|
17
|
+
* - 상한: 같은 머리는 재게이트·리뷰 각 한 번 · 카드·PR 당 재게이트 `REJUDGE_HEAD_CAP` 번까지 — 넘으면 사유 줄을 남기고 멈춘다.
|
|
18
|
+
* - REJUDGE base-stale(OVERLAP-EVIDENCE-BASE-WINDOW 짝 · #26060): land 가 `base-stale-overlap`(재게이트 base 뒤 착지가 PR 파일을 건드렸다)으로
|
|
19
|
+
* 거절되면 그 시도는 `base-stale` 갈래로 적히고(judgement 1회 규칙과 섞이지 않는다), 다음 틱이 «같은 머리»를 새 base 위에서 한 번 더
|
|
20
|
+
* 재게이트한다(`staleBase` = 낡은 base · 멱등 키 = 머리 ⊕ staleBase · 상한에 포함). 그 재게이트가 통과하고 base 가 실제로 바뀌었을 때만
|
|
21
|
+
* 새 baseCommit 으로 증거를 다시 써 land 한다(`gateAt` 으로 앞 base-stale 시도를 소비) — 관측 `rejudge-on-head {step:'regate', reason:'base-stale-overlap …'}`.
|
|
16
22
|
* - 대상: 카드에 묶인 PR(URL 로 저장소 확인) ⊕ 그 PR 로 TA 가 이미 리뷰를 요청한 카드(= 런이 멈춰 TA 판단이 시작됐다).
|
|
17
23
|
* - 사람의 hold(draft PR)는 뒤집지 않는다 — 재게이트(`noMerge`)와 land 관문이 draft 를 거부한다.
|
|
18
24
|
* - 허용: 설정 `taskAgent.liveMoves` 그대로 — 재게이트·land 는 `propose-land`, 리뷰 재요청은 `review` 가 있을 때만(없으면 «했을 수»).
|
|
@@ -24,9 +30,10 @@ import { spawn } from 'node:child_process';
|
|
|
24
30
|
import { dirname, join } from 'node:path';
|
|
25
31
|
import { debug } from '../debug/log.js';
|
|
26
32
|
import type { DetachedSpawn } from '../cli/tasks-cli.js';
|
|
33
|
+
import { appendLandOrderRow, joinLandOrderRow } from './land-order-ledger.js';
|
|
27
34
|
import { recordLiveMoveHistory } from './live-move-history.js';
|
|
28
|
-
import { defaultPrHead, defaultReviewRepoCandidates, executeLiveLand, executeLiveReview, type LiveMoveDeps, type LiveMoveResult, type PrHeadView, type ReviewRepoCandidate, type TaskAgentLiveMove } from './live-moves.js';
|
|
29
|
-
import { capturedLandReview, captureReviewResults, pendingReviewResults } from './review-result-capture.js';
|
|
35
|
+
import { defaultPrHead, defaultReviewRepoCandidates, executeLiveLand, executeLiveReview, landRetryPending, type LiveMoveDeps, type LiveMoveResult, type PrHeadView, type ReviewRepoCandidate, type TaskAgentLiveMove } from './live-moves.js';
|
|
36
|
+
import { capturedLandReview, captureReviewResults, parseReviewResultOutput, pendingReviewResults, type ParsedReviewResult } from './review-result-capture.js';
|
|
30
37
|
import { cardPrNumber, readTaskCard, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
31
38
|
|
|
32
39
|
export const REJUDGE_ON_HEAD_EVENT = 'rejudge-on-head';
|
|
@@ -50,8 +57,16 @@ export interface RejudgeDeps {
|
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
const safe = (value: string) => value.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
53
|
-
export function regateResultPath(statePath: string, cardId: string, pr: number, head: string): string {
|
|
54
|
-
|
|
60
|
+
export function regateResultPath(statePath: string, cardId: string, pr: number, head: string, staleBase?: string): string {
|
|
61
|
+
const suffix = staleBase ? `-from-${safe(staleBase.slice(0, 12))}` : '';
|
|
62
|
+
return join(dirname(statePath), 'regate-results', `${safe(cardId)}-${pr}-${safe(head.slice(0, 12))}${suffix}.json`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** REJUDGE base-stale — 이 재게이트(요청 시각 `since`) «뒤»에 같은 머리 land 가 base-stale 로 거절된 마지막 시도. */
|
|
66
|
+
export function baseStaleAttemptAfter(card: Pick<TaskCard, 'landAttempts'>, pr: number, head: string, since: string): NonNullable<TaskCard['landAttempts']>[number] | undefined {
|
|
67
|
+
const from = Date.parse(since);
|
|
68
|
+
return (card.landAttempts ?? []).filter((entry) => entry.pr === pr && entry.head.toLowerCase() === head.toLowerCase() && entry.ok === false
|
|
69
|
+
&& entry.class === 'base-stale' && Number.isFinite(from) && Date.parse(entry.at) >= from).at(-1);
|
|
55
70
|
}
|
|
56
71
|
export function overlapEvidencePath(statePath: string, cardId: string, pr: number, head: string): string {
|
|
57
72
|
return join(dirname(statePath), 'overlap-evidence', `${safe(cardId)}-${pr}-${safe(head.slice(0, 12))}.json`);
|
|
@@ -183,47 +198,60 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
|
|
|
183
198
|
const step = (s: RejudgeStep, decision: string, reason: string, record: boolean) => emit({ card: card!.id, pr, fromHead, toHead, step: s, decision, reason }, record);
|
|
184
199
|
|
|
185
200
|
// 머리가 런 멈춤 때 그대로면 재판정하지 않는다 — 그 머리의 재게이트·리뷰·land 판단은 멈춤 때의 판단(슈퍼바이저 틱)이 이미 했다.
|
|
186
|
-
|
|
201
|
+
// TA-LAND-RETRY-ON-INFRA — 단, 그 머리의 land 가 TA 쪽 인프라 결손으로 실패해 재시도가 남았으면 아래 ①②③ 을 그대로 탄다
|
|
202
|
+
// (재게이트·회수한 리뷰·겹침 증거를 다시 갖춘 뒤에만 land — 관문은 그대로).
|
|
203
|
+
if (!regates.length && judgedHead && sameHead(toHead, judgedHead) && !landRetryPending(card, pr, toHead)) return target('head-unchanged', `PR #${pr} head is still the judged head`, pr, fromHead, toHead);
|
|
187
204
|
|
|
188
|
-
// ① 재게이트 — 지금 머리의
|
|
189
|
-
const regate = regates.
|
|
190
|
-
|
|
205
|
+
// ① 재게이트 — 지금 머리의 «마지막» 기록(REJUDGE base-stale 로 같은 머리를 다시 잰 기록이 뒤에 붙는다).
|
|
206
|
+
const regate = regates.filter((entry) => sameHead(entry.head, toHead)).at(-1);
|
|
207
|
+
// REJUDGE base-stale — 이 머리의 통과한 재게이트로 land 했더니 base-stale-overlap 으로 거절됐다 → 새 base 위에서 한 번 더 잰다.
|
|
208
|
+
const stale = regate?.passed === true ? baseStaleAttemptAfter(card, pr, toHead, regate.at) : undefined;
|
|
209
|
+
const staleBase = stale ? regate?.baseCommit : undefined;
|
|
210
|
+
if (stale && !staleBase) {
|
|
211
|
+
step('regate', 'not-regating', `base-stale-overlap on head ${toHead.slice(0, 12)} but the stale regate has no baseCommit — needs owner`, true);
|
|
212
|
+
return outcomes;
|
|
213
|
+
}
|
|
214
|
+
if (!regate || staleBase) {
|
|
215
|
+
const why = staleBase ? `base-stale-overlap — gate base ${staleBase.slice(0, 12)} went stale (${(stale!.reason ?? '').replace(/^base-stale-overlap:\s*/, '').slice(0, 160)})` : '';
|
|
191
216
|
if (regates.length >= REJUDGE_HEAD_CAP) {
|
|
192
|
-
step('regate', 'cap-reached', `re-gated ${regates.length}
|
|
217
|
+
step('regate', 'cap-reached', `${why ? `${why} · ` : ''}re-gated ${regates.length} times already (cap ${REJUDGE_HEAD_CAP}) — no more rejudge; needs owner`, true);
|
|
193
218
|
return outcomes;
|
|
194
219
|
}
|
|
195
|
-
if (!deps.liveMoves.has('propose-land')) step('regate', 'would-regate', 'propose-land not enabled in taskAgent.liveMoves — shadow
|
|
220
|
+
if (!deps.liveMoves.has('propose-land')) step('regate', 'would-regate', `${why ? `${why} · ` : ''}propose-land not enabled in taskAgent.liveMoves — shadow`, false);
|
|
196
221
|
else {
|
|
197
222
|
const at = now().toISOString();
|
|
198
|
-
const resultPath = regateResultPath(statePath, card.id, pr, toHead);
|
|
223
|
+
const resultPath = regateResultPath(statePath, card.id, pr, toHead, staleBase);
|
|
199
224
|
let claimed = false;
|
|
200
225
|
let capped = false;
|
|
201
226
|
updateTaskCard(statePath, card.id, (current) => {
|
|
202
227
|
const held = (current?.regates ?? []).filter((entry) => entry.pr === pr);
|
|
203
|
-
|
|
228
|
+
// 멱등 — 처음 재게이트는 «머리», base-stale 재게이트는 «머리 ⊕ 낡은 base» 가 키다(같은 머리·같은 base 로 두 번 안 잰다).
|
|
229
|
+
if (!current || held.some((entry) => sameHead(entry.head, toHead) && (staleBase ? entry.staleBase !== undefined && sameHead(entry.staleBase, staleBase) : true))) return undefined;
|
|
204
230
|
// 상한은 잠금 안의 «지금» 카드로 다시 잰다 — 다른 머리를 본 두 틱이 겹쳐도 상한을 넘지 않게.
|
|
205
231
|
if (held.length >= REJUDGE_HEAD_CAP) { capped = true; return undefined; }
|
|
206
232
|
claimed = true;
|
|
207
|
-
return { ...current, regates: [...(current.regates ?? []), { pr, head: toHead, at, resultPath }] };
|
|
233
|
+
return { ...current, regates: [...(current.regates ?? []), { pr, head: toHead, at, resultPath, ...(staleBase ? { staleBase } : {}) }] };
|
|
208
234
|
});
|
|
209
235
|
if (capped) {
|
|
210
|
-
step('regate', 'cap-reached', `re-gated ${REJUDGE_HEAD_CAP}
|
|
236
|
+
step('regate', 'cap-reached', `${why ? `${why} · ` : ''}re-gated ${REJUDGE_HEAD_CAP} times already (cap ${REJUDGE_HEAD_CAP} · claim-time recheck) — no more rejudge; needs owner`, true);
|
|
211
237
|
return outcomes;
|
|
212
238
|
}
|
|
213
239
|
if (claimed) {
|
|
214
240
|
try {
|
|
215
241
|
mkdirSync(dirname(resultPath), { recursive: true });
|
|
216
242
|
await (deps.requestRegate ?? defaultRequestRegate)({ card: card.id, pr, head: toHead, repoRoot: cwd, resultPath });
|
|
217
|
-
step('regate', 'regate-requested', `host regate (noMerge) launched detached → ${resultPath}`, true);
|
|
243
|
+
step('regate', 'regate-requested', `${why ? `${why} · re-gate on a new base · ` : ''}host regate (noMerge) launched detached → ${resultPath}`, true);
|
|
218
244
|
} catch (error) {
|
|
219
245
|
const detail = `regate launch failed: ${(error instanceof Error ? error.message : String(error)).slice(-200)}`;
|
|
220
246
|
updateTaskCard(statePath, card.id, (current) => current?.regates?.some((entry) => entry.pr === pr && entry.head === toHead && entry.at === at)
|
|
221
247
|
? { ...current, regates: current.regates.map((entry) => entry.pr === pr && entry.head === toHead && entry.at === at ? { ...entry, passed: false, status: 'unmeasured', detail } : entry) }
|
|
222
248
|
: undefined);
|
|
223
|
-
step('regate', 'regate-failed', detail
|
|
249
|
+
step('regate', 'regate-failed', `${why ? `${why} · ` : ''}${detail}`, true);
|
|
224
250
|
}
|
|
225
251
|
}
|
|
226
252
|
}
|
|
253
|
+
// base-stale 재게이트는 그 결과가 나와야 land 한다 — 낡은 base 의 증거로 이번 틱에 다시 land 하지 않는다.
|
|
254
|
+
if (staleBase) return outcomes;
|
|
227
255
|
} else if (regate.passed === undefined) {
|
|
228
256
|
step('regate', 'awaiting-regate-result', 'host regate still running', false);
|
|
229
257
|
}
|
|
@@ -254,6 +282,11 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
|
|
|
254
282
|
step('land', 'not-landing', regate ? (regate.passed === false ? `host regate not passed: ${regate.detail ?? regate.status ?? 'unknown'}` : 'host regate still running') : 'no host regate for this head', regate?.passed === false);
|
|
255
283
|
return outcomes;
|
|
256
284
|
}
|
|
285
|
+
// REJUDGE base-stale — 새 base 위 재게이트가 «같은» base 로 돌아왔다 — 증거가 또 낡았다고 거절될 것이므로 land 하지 않는다(재게이트도 멱등 키로 0).
|
|
286
|
+
if (regate.staleBase && regate.baseCommit && sameHead(regate.baseCommit, regate.staleBase)) {
|
|
287
|
+
step('land', 'not-landing', `base-stale-overlap — re-gate on head ${toHead.slice(0, 12)} returned the same base ${regate.staleBase.slice(0, 12)} (did not advance) — no land, no further re-gate on this base; needs owner`, true);
|
|
288
|
+
return outcomes;
|
|
289
|
+
}
|
|
257
290
|
if (!deps.liveMoves.has('propose-land')) { step('land', 'would-land', 'propose-land not enabled in taskAgent.liveMoves — shadow', false); return outcomes; }
|
|
258
291
|
|
|
259
292
|
// ③ land — 겹침 증거(#26023 꼴)를 «완전히» 쓴 뒤에만 executeLiveLand 를 그대로 부른다. 증거를 못 갖추면 land 하지 않는다(fail-closed).
|
|
@@ -263,21 +296,43 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
|
|
|
263
296
|
return outcomes;
|
|
264
297
|
}
|
|
265
298
|
const overlapEvidence = overlapEvidencePath(statePath, card.id, pr, toHead);
|
|
299
|
+
// #26023 꼴 그대로 — 증거 «파일» 내용은 무변 · 아래 원장 조인이 같은 모양을 쓴다(cardId 는 메모리에서만 싣는다).
|
|
300
|
+
const overlapEvidenceContent = { pr, head: toHead, gate: { head: toHead, baseCommit: regate.baseCommit, passed: true }, reviewResult };
|
|
266
301
|
try {
|
|
267
302
|
mkdirSync(dirname(overlapEvidence), { recursive: true });
|
|
268
303
|
// 임시 파일에 쓰고 바꿔 끼운다 — 겹친 틱이 같은 경로를 다시 써도 land 가 반쯤 쓴 JSON 을 읽지 않게.
|
|
269
304
|
const tmp = `${overlapEvidence}.${process.pid}.${Date.now()}.tmp`;
|
|
270
|
-
writeFileSync(tmp, JSON.stringify(
|
|
305
|
+
writeFileSync(tmp, JSON.stringify(overlapEvidenceContent, null, 2));
|
|
271
306
|
renameSync(tmp, overlapEvidence);
|
|
272
307
|
} catch (error) {
|
|
273
308
|
step('land', 'not-landing', `overlap evidence write failed: ${(error instanceof Error ? error.message : String(error)).slice(-200)}`, true);
|
|
274
309
|
return outcomes;
|
|
275
310
|
}
|
|
276
311
|
const before = { reviewRequests: latest.reviewRequests, landAttempts: latest.landAttempts };
|
|
277
|
-
|
|
312
|
+
// 주기 id — base-stale 재게이트 뒤의 land 는 새 base 를 붙여 앞 land 의 주기 표시(모듈 전역)와 갈린다 · gateAt 이 앞 base-stale 시도를 소비한다.
|
|
313
|
+
const cycleId = `rejudge:${card.id}:${toHead}${regate.staleBase ? `:${regate.baseCommit.slice(0, 12)}` : ''}`;
|
|
314
|
+
const landed = await executeLiveLand(latest, pr, { runId: card.runId ?? null, cycleId, cwd, review, overlapEvidence, gateAt: regate.at },
|
|
278
315
|
{ ...live, ...(deps.log ? { log: deps.log } : {}) });
|
|
316
|
+
// LAND-ORDER-LEDGER — executeLiveLand 직후 원장 한 줄(같은 pr+head 는 한 줄만). 쓰기가 실패해도 land 결과는 그대로(삼킨다).
|
|
317
|
+
const landOrderLog = (event: 'appended' | 'append-failed') => { try { log('task-agent.land-order', event, { pr, head: toHead }); } catch { /* fail-soft */ } };
|
|
318
|
+
try {
|
|
319
|
+
// review-results 파일을 못 읽으면 reviewVerdict 'missing' — 0 으로 쓰지 않는다(land 판정의 회수 결과와는 별개 파일).
|
|
320
|
+
let reviewForLedger: ParsedReviewResult | null = null;
|
|
321
|
+
try { reviewForLedger = parseReviewResultOutput(read(reviewResult)); } catch { reviewForLedger = null; }
|
|
322
|
+
appendLandOrderRow(statePath, joinLandOrderRow({
|
|
323
|
+
overlapEvidence: { ...overlapEvidenceContent, cardId: card.id },
|
|
324
|
+
reviewResult: reviewForLedger, landed, at: now().toISOString(),
|
|
325
|
+
}));
|
|
326
|
+
landOrderLog('appended');
|
|
327
|
+
} catch {
|
|
328
|
+
landOrderLog('append-failed');
|
|
329
|
+
}
|
|
279
330
|
recordMove(statePath, card.id, card.runId, 'propose-land', landed, pr, before, now, deps.log);
|
|
280
|
-
|
|
331
|
+
// TA-LAND-RETRY-ON-INFRA — 실패한 시도는 갈래·사유·재시도 여부를 사유 줄에 싣는다(다음 틱이 왜 다시 하는지/안 하는지).
|
|
332
|
+
const attempt = landed.landAttempt?.class
|
|
333
|
+
? ` · land-class ${landed.landAttempt.class} (${landed.landAttempt.reason}) · attempt ${landed.landAttempt.attempt} · ${landed.landAttempt.class === 'base-stale' ? 'base-stale → re-gate on a new base next tick (cap counts)' : landed.landAttempt.retryAllowed ? 'retry allowed on this head' : 'no retry on this head'}`
|
|
334
|
+
: '';
|
|
335
|
+
step('land', landed.executed && landed.ok ? 'landed' : 'land-rejected', `${landed.detail}${attempt} · overlap-evidence ${overlapEvidence}`, true);
|
|
281
336
|
return outcomes;
|
|
282
337
|
}
|
|
283
338
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REVIEW-CONVERGE-PRIOR-FIX — TA 가 같은 PR 에 «다시» 거는 리뷰가 직전 라운드를 보게 한다.
|
|
3
|
+
*
|
|
4
|
+
* - 왜: `liveReviewIntent` 의 «직전 반영분» 칸은 늘 «런 X 이 Y 로 멈춤 — 수확 가능» 고정 문면이었다 — 직전 리뷰의 must-fix 도,
|
|
5
|
+
* 그 뒤 바뀐 머리 범위도 없었다. 그래서 리뷰어가 head 마다 PR 전체를 처음부터 다시 보고 «새» must-fix 를 하나씩 냈고
|
|
6
|
+
* (실측 10-11 PR #26030: ecd32f7c02 → c35d7be0cd 라운드마다 새 지적) REJUDGE 상한 3 에 걸려 TA 경로가 닫혔다.
|
|
7
|
+
* - 무엇을: 같은 PR 의 «두 번째 이후» 리뷰 요청에서만 intent 에 ① 직전 머리 → 지금 머리 diff 요약(파일·줄 수)
|
|
8
|
+
* ② 직전 리뷰의 must-fix 목록(결과 파일 `review-results/<card>-<pr>-<head12>.json` — 못 읽으면 «직전 결과 미회수»)
|
|
9
|
+
* ③ 스코프 경계 한 줄(바뀌지 않은 줄의 새 must-fix 는 근거 줄을 대라)을 싣는다. 첫 리뷰는 종전 문면 그대로다.
|
|
10
|
+
* - 하지 않는 것: 판정 로직(must-fix 0 규칙 · #25962 · #26036)은 손대지 않는다. 새 결함을 «금지»하지 않는다 — 근거 줄을 요구할 뿐이다.
|
|
11
|
+
* - 관측: `task-agent` · `review-prior-fix-filled {card, pr, fromHead, toHead, priorMustFix}`(호출부가 남긴다).
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import type { TaskCard } from './task-hand.js';
|
|
16
|
+
|
|
17
|
+
const SHA40 = /^[0-9a-f]{40}$/i;
|
|
18
|
+
const MAX_ITEMS = 10;
|
|
19
|
+
const MAX_ITEM_CHARS = 600;
|
|
20
|
+
const MAX_FILES = 20;
|
|
21
|
+
|
|
22
|
+
export const PRIOR_RESULT_NOT_RECOVERED = '직전 결과 미회수';
|
|
23
|
+
export const PRIOR_FIX_SCOPE_LINE = '이번 리뷰는 직전 must-fix 가 해결됐는지 ⊕ 그 사이 바뀐 줄에서 새 결함이 생겼는지를 본다 · 직전에 이미 본 바뀌지 않은 줄에서 새 must-fix 를 만들지 말 것(새 결함이 확실하면 근거 줄을 대라)';
|
|
24
|
+
|
|
25
|
+
type ReviewRequest = NonNullable<TaskCard['reviewRequests']>[number];
|
|
26
|
+
|
|
27
|
+
export interface PriorDiffFile { path: string; added: number | null; deleted: number | null }
|
|
28
|
+
export interface PriorDiff { files: PriorDiffFile[]; added: number; deleted: number }
|
|
29
|
+
/** 직전 머리 → 지금 머리 diff — 못 재면 null(«0 줄»이 아니라 «미측정»). */
|
|
30
|
+
export type PriorDiffReader = (cwd: string | undefined, fromHead: string, toHead: string) => PriorDiff | null;
|
|
31
|
+
|
|
32
|
+
export type PriorMustFix =
|
|
33
|
+
| { status: 'recovered'; items: string[] }
|
|
34
|
+
| { status: 'not-recovered'; reason: string };
|
|
35
|
+
|
|
36
|
+
export interface PriorFixContext {
|
|
37
|
+
fromHead: string;
|
|
38
|
+
toHead: string;
|
|
39
|
+
diff: PriorDiff | null;
|
|
40
|
+
mustFix: PriorMustFix;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 같은 PR 의 직전 요청 — 지금 머리가 아닌 것 중 마지막(띄우기 오류가 난 요청은 리뷰가 안 돌았으니 건너뛴다). */
|
|
44
|
+
export function priorReviewRequest(requests: readonly ReviewRequest[] | undefined, pr: number, head: string): ReviewRequest | undefined {
|
|
45
|
+
return (requests ?? []).filter((entry) => entry.pr === pr && entry.head !== head && !entry.error && SHA40.test(entry.head)).at(-1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 결과 파일에서 must-fix «문면»을 읽는다 — `self review --json` 의 결과 줄(뒤에서부터 첫 결과 모양)만.
|
|
50
|
+
* 결과 모양 판정은 `parseReviewResultOutput` 과 같다(PR 번호 ⊕ (verdict ⊕ mustFix 배열 | error 문자열)).
|
|
51
|
+
*/
|
|
52
|
+
export function readPriorMustFix(path: string | undefined, pr: number, read: (path: string) => string = (p) => readFileSync(p, 'utf8')): PriorMustFix {
|
|
53
|
+
if (!path) return { status: 'not-recovered', reason: 'no result path' };
|
|
54
|
+
let text: string;
|
|
55
|
+
try { text = read(path); } catch (error) {
|
|
56
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
57
|
+
return { status: 'not-recovered', reason: code === 'ENOENT' ? 'result file not created' : `result file unreadable (${code ?? 'error'})` };
|
|
58
|
+
}
|
|
59
|
+
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
60
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
61
|
+
const line = lines[i]!;
|
|
62
|
+
if (!line.startsWith('{')) continue;
|
|
63
|
+
let value: unknown;
|
|
64
|
+
try { value = JSON.parse(line); } catch { continue; }
|
|
65
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
66
|
+
const v = value as Record<string, unknown>;
|
|
67
|
+
const isResult = v.pr !== undefined && (('verdict' in v && Array.isArray(v.mustFix)) || typeof v.error === 'string');
|
|
68
|
+
if (!isResult) continue;
|
|
69
|
+
const prRaw = typeof v.pr === 'number' ? v.pr : typeof v.pr === 'string' ? Number(String(v.pr).replace(/^#/, '')) : NaN;
|
|
70
|
+
if (prRaw !== pr) return { status: 'not-recovered', reason: `result PR ${Number.isFinite(prRaw) ? prRaw : 'unknown'} does not match #${pr}` };
|
|
71
|
+
if (typeof v.error === 'string') return { status: 'not-recovered', reason: `prior review error: ${v.error.slice(0, 200)}` };
|
|
72
|
+
if (v.reviewed === false) return { status: 'not-recovered', reason: 'prior review did not run (reviewed:false)' };
|
|
73
|
+
if (!Array.isArray(v.mustFix) || !v.mustFix.every((fix) => typeof fix === 'string')) return { status: 'not-recovered', reason: 'must-fix list unreadable' };
|
|
74
|
+
return { status: 'recovered', items: v.mustFix as string[] };
|
|
75
|
+
}
|
|
76
|
+
return { status: 'not-recovered', reason: text.trim() ? 'output has no JSON result line' : 'result file empty' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 기본 diff — `git diff --numstat <from> <to>`(그 PR 의 저장소 cwd). 커밋이 없거나 git 이 실패하면 null. */
|
|
80
|
+
export const defaultPriorDiff: PriorDiffReader = (cwd, fromHead, toHead) => {
|
|
81
|
+
if (!SHA40.test(fromHead) || !SHA40.test(toHead)) return null;
|
|
82
|
+
const out = spawnSync('git', ['diff', '--numstat', fromHead, toHead], { encoding: 'utf8', timeout: 20_000, ...(cwd ? { cwd } : {}) });
|
|
83
|
+
if (out.error || out.status !== 0 || typeof out.stdout !== 'string') return null;
|
|
84
|
+
return parseNumstat(out.stdout);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function parseNumstat(text: string): PriorDiff {
|
|
88
|
+
const files: PriorDiffFile[] = [];
|
|
89
|
+
let added = 0;
|
|
90
|
+
let deleted = 0;
|
|
91
|
+
for (const line of text.split('\n')) {
|
|
92
|
+
const match = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
|
|
93
|
+
if (!match) continue;
|
|
94
|
+
const a = match[1] === '-' ? null : Number(match[1]);
|
|
95
|
+
const d = match[2] === '-' ? null : Number(match[2]);
|
|
96
|
+
files.push({ path: match[3]!, added: a, deleted: d });
|
|
97
|
+
added += a ?? 0;
|
|
98
|
+
deleted += d ?? 0;
|
|
99
|
+
}
|
|
100
|
+
return { files, added, deleted };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 두 번째 이후 리뷰의 맥락 — 직전 요청이 없으면 null(첫 리뷰 · 종전 문면). */
|
|
104
|
+
export function buildPriorFixContext(
|
|
105
|
+
card: TaskCard, pr: number, head: string,
|
|
106
|
+
opts: { cwd?: string; priorDiff?: PriorDiffReader; readFile?: (path: string) => string } = {},
|
|
107
|
+
): PriorFixContext | null {
|
|
108
|
+
const prior = priorReviewRequest(card.reviewRequests, pr, head);
|
|
109
|
+
if (!prior) return null;
|
|
110
|
+
let diff: PriorDiff | null;
|
|
111
|
+
try { diff = (opts.priorDiff ?? defaultPriorDiff)(opts.cwd, prior.head, head); } catch { diff = null; }
|
|
112
|
+
return { fromHead: prior.head, toHead: head, diff, mustFix: readPriorMustFix(prior.resultPath, pr, opts.readFile) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** «직전 반영분» 블록 — 종전 런 줄 다음에 붙는다. */
|
|
116
|
+
export function priorFixBlock(ctx: PriorFixContext): string {
|
|
117
|
+
const lines = [`직전 리뷰 머리 → 지금 머리: ${ctx.fromHead.slice(0, 12)} → ${ctx.toHead.slice(0, 12)}`];
|
|
118
|
+
if (ctx.diff) {
|
|
119
|
+
lines.push(`그 사이 바뀐 것: 파일 ${ctx.diff.files.length}개 · +${ctx.diff.added} −${ctx.diff.deleted}`);
|
|
120
|
+
for (const file of ctx.diff.files.slice(0, MAX_FILES)) lines.push(` - ${file.path} (+${file.added ?? '?'} −${file.deleted ?? '?'})`);
|
|
121
|
+
if (ctx.diff.files.length > MAX_FILES) lines.push(` - … 외 ${ctx.diff.files.length - MAX_FILES}개`);
|
|
122
|
+
} else {
|
|
123
|
+
lines.push('그 사이 바뀐 것: diff 미측정(이 저장소에서 두 머리를 비교하지 못했다)');
|
|
124
|
+
}
|
|
125
|
+
if (ctx.mustFix.status === 'recovered') {
|
|
126
|
+
if (ctx.mustFix.items.length === 0) lines.push('직전 리뷰 must-fix: 0건');
|
|
127
|
+
else {
|
|
128
|
+
lines.push(`직전 리뷰 must-fix ${ctx.mustFix.items.length}건:`);
|
|
129
|
+
ctx.mustFix.items.slice(0, MAX_ITEMS).forEach((item, index) => lines.push(` ${index + 1}. ${item.replace(/\s+/g, ' ').trim().slice(0, MAX_ITEM_CHARS)}`));
|
|
130
|
+
if (ctx.mustFix.items.length > MAX_ITEMS) lines.push(` … 외 ${ctx.mustFix.items.length - MAX_ITEMS}건`);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(`직전 리뷰 must-fix: ${PRIOR_RESULT_NOT_RECOVERED} (${ctx.mustFix.reason}) — 목록이 없다고 «0건»으로 읽지 말 것`);
|
|
134
|
+
}
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 관측 칸 — must-fix 수(미회수면 null). */
|
|
139
|
+
export function priorFixObservation(cardId: string, pr: number, ctx: PriorFixContext): Record<string, unknown> {
|
|
140
|
+
return {
|
|
141
|
+
card: cardId, pr, fromHead: ctx.fromHead, toHead: ctx.toHead,
|
|
142
|
+
priorMustFix: ctx.mustFix.status === 'recovered' ? ctx.mustFix.items.length : null,
|
|
143
|
+
...(ctx.mustFix.status === 'not-recovered' ? { priorResult: 'not-recovered', priorResultReason: ctx.mustFix.reason } : {}),
|
|
144
|
+
diffFiles: ctx.diff ? ctx.diff.files.length : null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { launchBindingRunIds, launchRegistryDir, readLaunchBinding } from '../harness/launch-registry.js';
|
|
5
|
+
import { launchStampFromLedger } from '../harness/launch-stamp.js';
|
|
6
|
+
import { normalizeRunId } from '../harness/harness-space.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
8
|
+
import { loadRunLedger, runLedgerDir } from '../self-implement/run-ledger.js';
|
|
9
|
+
import { TASK_SEATS, updateTaskCard, type TaskCard, type TaskSeat } from './task-hand.js';
|
|
10
|
+
|
|
11
|
+
export interface RunBirth {
|
|
12
|
+
runId: string;
|
|
13
|
+
goalPath?: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
runsDir?: string;
|
|
16
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RunAdoptDeps {
|
|
20
|
+
/** 카드 상태 파일(시험 주입). 없으면 우주 뿌리(`instanceRoot`) 아래 `task-agent-actions.json` = `taskAgentStatePath()`. */
|
|
21
|
+
statePath?: string;
|
|
22
|
+
/** 우주 뿌리 해석 seam — 기본 `effectiveInstanceRoot()`(state·runsDir 를 만드는 같은 공용 해석). */
|
|
23
|
+
instanceRoot?: () => string;
|
|
24
|
+
registryDir?: string;
|
|
25
|
+
knownChecklistIds?: ReadonlySet<string>;
|
|
26
|
+
now?: () => Date;
|
|
27
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RunAdoptSkipReason = 'handed' | 'already-carded' | 'pod-child' | 'other-universe' | 'universe-mismatch';
|
|
31
|
+
export type RunAdoptResult = { card: TaskCard; adopted: true } | { adopted: false; reason: RunAdoptSkipReason };
|
|
32
|
+
|
|
33
|
+
function seatFromGoal(path: string | undefined): TaskSeat | undefined {
|
|
34
|
+
const seat = path?.replaceAll('\\', '/').match(/\/(?:seat-state|seats)\/(OP|TC|MK|UX)\//)?.[1];
|
|
35
|
+
return seat as TaskSeat | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function goalText(path: string | undefined, cwd: string): string {
|
|
39
|
+
if (!path) return '';
|
|
40
|
+
try {
|
|
41
|
+
const fullPath = isAbsolute(path) ? path : resolve(cwd, path);
|
|
42
|
+
return readFileSync(fullPath, 'utf8');
|
|
43
|
+
} catch { return ''; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checklistFromText(text: string, known: ReadonlySet<string>): string | undefined {
|
|
47
|
+
const explicit = /칸\s+([A-Za-z0-9][A-Za-z0-9_-]*)/.exec(text)?.[1];
|
|
48
|
+
if (explicit) return explicit;
|
|
49
|
+
const title = /(?:^|\n)제목:\s*([A-Za-z0-9][A-Za-z0-9_-]*?)-\d+\s+—/m.exec(text)?.[1];
|
|
50
|
+
return title && known.has(title) ? title : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 경로 정규화 — 심볼릭 링크(`/var`↔`/private/var` 등)·상대·후행 슬래시를 같은 값으로. 없는 꼬리는 있는 조상의 실경로에 붙인다. */
|
|
54
|
+
function canonicalPath(path: string): string {
|
|
55
|
+
const absolute = resolve(path);
|
|
56
|
+
let head = absolute;
|
|
57
|
+
const tail: string[] = [];
|
|
58
|
+
while (!existsSync(head)) {
|
|
59
|
+
const parent = dirname(head);
|
|
60
|
+
if (parent === head) return absolute;
|
|
61
|
+
tail.unshift(basename(head));
|
|
62
|
+
head = parent;
|
|
63
|
+
}
|
|
64
|
+
try { return join(realpathSync(head), ...tail); } catch { return absolute; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Adopt only a run belonging to this universe. The universe root is resolved once (`effectiveInstanceRoot()` unless a
|
|
68
|
+
* state path is injected) and both the card state file and the run's `runsDir` are compared against it after canonical
|
|
69
|
+
* path resolution. The runId check and write share updateTaskCard's lock.
|
|
70
|
+
* A birth without `runsDir` cannot prove its universe and is skipped as `other-universe` (conservative); a `runsDir` that is
|
|
71
|
+
* not `<root>/self-dev-runs` is skipped as `universe-mismatch` with both paths observed — never silently. */
|
|
72
|
+
export function adoptRunCard(birth: RunBirth, deps: RunAdoptDeps = {}): RunAdoptResult {
|
|
73
|
+
const root = deps.statePath ? dirname(deps.statePath) : (deps.instanceRoot ?? effectiveInstanceRoot)();
|
|
74
|
+
const statePath = deps.statePath ?? join(root, 'task-agent-actions.json');
|
|
75
|
+
const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
|
|
76
|
+
const skip = (reason: RunAdoptSkipReason, extra: Record<string, unknown> = {}): RunAdoptResult => {
|
|
77
|
+
log('task-agent', 'run-adopt-skipped', { runId: birth.runId, reason, ...extra });
|
|
78
|
+
return { adopted: false, reason };
|
|
79
|
+
};
|
|
80
|
+
if (birth.env.ELANOUS_TASK_CARD_ID) return skip('handed');
|
|
81
|
+
if (birth.env.ELANOUS_POD_NAME) return skip('pod-child');
|
|
82
|
+
if (!birth.runsDir) return skip('other-universe', { statePath, runsDir: null });
|
|
83
|
+
const expectedRunsDir = canonicalPath(join(root, 'self-dev-runs'));
|
|
84
|
+
const runsDir = canonicalPath(birth.runsDir);
|
|
85
|
+
if (runsDir !== expectedRunsDir) return skip('universe-mismatch', { statePath, runsDir: birth.runsDir, expectedRunsDir, canonicalRunsDir: runsDir });
|
|
86
|
+
if (!birth.runId || birth.runId !== normalizeRunId(birth.runId) || birth.runId.includes(sep)) throw new Error(`invalid runId: ${birth.runId}`);
|
|
87
|
+
const text = goalText(birth.goalPath, birth.cwd);
|
|
88
|
+
const firstLine = text.split(/\r?\n/).find(line => line.trim())?.trim() ?? `harness run ${birth.runId}`;
|
|
89
|
+
let stamp = launchStampFromLedger(null);
|
|
90
|
+
try { stamp = launchStampFromLedger(loadRunLedger(birth.runId, runLedgerDir(root))); } catch { /* unreadable stamp is unknown */ }
|
|
91
|
+
const origin = stamp.entrance;
|
|
92
|
+
const actor = stamp.actor;
|
|
93
|
+
const seat = TASK_SEATS.find(candidate => candidate === actor) ?? seatFromGoal(birth.goalPath);
|
|
94
|
+
const checklistId = checklistFromText(text, deps.knownChecklistIds ?? new Set());
|
|
95
|
+
const id = `ta-${birth.runId}`;
|
|
96
|
+
let adopted = false;
|
|
97
|
+
const card = updateTaskCard(statePath, id, (current, tasks) => {
|
|
98
|
+
if (current || Object.values(tasks).some(candidate => candidate.runId === birth.runId)) return undefined;
|
|
99
|
+
adopted = true;
|
|
100
|
+
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
101
|
+
return {
|
|
102
|
+
id, text: firstLine, status: 'launched', runId: birth.runId,
|
|
103
|
+
...(birth.goalPath ? { goal: birth.goalPath } : {}),
|
|
104
|
+
...(seat ? { seat } : {}),
|
|
105
|
+
...(checklistId ? { checklistId } : {}),
|
|
106
|
+
createdAt: at, history: [{ at, event: 'adopted', detail: origin }],
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
if (!adopted || !card) return skip('already-carded');
|
|
110
|
+
log('task-agent', 'run-adopted', { card: card.id, runId: birth.runId, origin, seat: seat ?? null, checklistId: checklistId ?? null });
|
|
111
|
+
return { card, adopted: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Explicit backfill for old launch bindings (`elanous tasks adopt-runs`); not part of the launch-time path.
|
|
115
|
+
* The binding's launch-time `podName`/`taskCardId` stand in for the env, so Pod children and handed runs are skipped.
|
|
116
|
+
* Bindings written before those fields existed carry no marker — the host registry is per-host `$TMPDIR`, and a Pod
|
|
117
|
+
* child writes its binding inside its Pod, so such legacy rows are host-launched runs. */
|
|
118
|
+
export function adoptUncardedRuns(deps: RunAdoptDeps = {}): { scanned: number; adopted: number; skipped: Partial<Record<RunAdoptSkipReason, number>> } {
|
|
119
|
+
const dir = deps.registryDir ?? launchRegistryDir();
|
|
120
|
+
const skipped: Partial<Record<RunAdoptSkipReason, number>> = {};
|
|
121
|
+
let scanned = 0;
|
|
122
|
+
let adopted = 0;
|
|
123
|
+
for (const runId of launchBindingRunIds(dir)) {
|
|
124
|
+
scanned++;
|
|
125
|
+
const binding = readLaunchBinding(runId, dir);
|
|
126
|
+
if (!binding?.runsDir) {
|
|
127
|
+
skipped['other-universe'] = (skipped['other-universe'] ?? 0) + 1;
|
|
128
|
+
(deps.log ?? ((category, event, data) => debug.log(category, event, data)))('task-agent', 'run-adopt-skipped', { runId, reason: 'other-universe' });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const result = adoptRunCard({ runId, cwd: binding.cwd, goalPath: binding.goalPath, runsDir: binding.runsDir, env: { ELANOUS_POD_NAME: binding.podName, ELANOUS_TASK_CARD_ID: binding.taskCardId } }, deps);
|
|
132
|
+
if (result.adopted) adopted++;
|
|
133
|
+
else skipped[result.reason] = (skipped[result.reason] ?? 0) + 1;
|
|
134
|
+
}
|
|
135
|
+
return { scanned, adopted, skipped };
|
|
136
|
+
}
|
package/src/task-agent/shadow.ts
CHANGED
|
@@ -58,10 +58,28 @@ export interface TaskAgentShadowDeps {
|
|
|
58
58
|
liveMoves?: ReadonlySet<TaskAgentLiveMove>;
|
|
59
59
|
/** live 수의 부작용(리뷰 띄우기·PR 머리 조회·상태 파일) — 시험은 주입한다. */
|
|
60
60
|
live?: LiveMoveDeps;
|
|
61
|
+
/** 카드 조회 상태 파일(시험 주입 · 운영 파일을 건드리지 않게). 기본 = `live.statePath` ⊕ `taskAgentStatePath()`. */
|
|
62
|
+
statePath?: string;
|
|
61
63
|
/** Process env for the launched-run id (`ELANOUS_RUN_ID`) — test seam; default process.env. */
|
|
62
64
|
env?: NodeJS.ProcessEnv;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/** 카드 조회 상태 파일 — 조회 경로 전부가 같은 파일을 본다. */
|
|
68
|
+
function cardStatePath(deps: TaskAgentShadowDeps): string {
|
|
69
|
+
return deps.statePath ?? deps.live?.statePath ?? taskAgentStatePath();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 카드 «원장»(기록·회수 대상) — 카드를 읽은 파일과 같다. 주입한 카드 리더(`readCard`)만 있으면 `live.statePath` 가 없을 때 모른다. */
|
|
73
|
+
function cardLedgerPath(deps: TaskAgentShadowDeps): string | undefined {
|
|
74
|
+
return deps.readCard ? deps.live?.statePath : cardStatePath(deps);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** live 수의 의존 — 실행부가 적는 원장(리뷰 요청·착지 시도)도 카드를 읽은 파일과 같게 맞춘다. */
|
|
78
|
+
function liveMoveDeps(deps: TaskAgentShadowDeps): LiveMoveDeps {
|
|
79
|
+
const ledger = cardLedgerPath(deps);
|
|
80
|
+
return { ...(deps.log ? { log: deps.log } : {}), ...deps.live, ...(ledger ? { statePath: ledger } : {}) };
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
/** 파일 산출물로 전달(elanous-out)하는 종결 종류 — 그 밖의 code-pr 밖 종류는 확인 증거를 판단부에서 기다린다. */
|
|
66
84
|
const DELIVERY_KINDS: ReadonlySet<CompletionKind> = new Set<CompletionKind>(['research-report', 'artifact', 'content']);
|
|
67
85
|
function isDeliveryKind(kind: CompletionKind | undefined): kind is DeliveryKind {
|
|
@@ -156,7 +174,7 @@ function shadowCompletion(card: TaskCard | undefined, lookupFailed: boolean): Co
|
|
|
156
174
|
export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, deps: TaskAgentShadowDeps = {}): Promise<TaskAgentShadowMove> {
|
|
157
175
|
// TA-REVIEW-RESULT-CAPTURE — 판단 «전»에 지난 live review 의 결과 파일을 회수한다(카드 원장을 아는 때만 · 던지지 않는다).
|
|
158
176
|
// 대기 중인 요청이 없으면 아무것도 쓰지 않는다(결과 파일 없는 카드의 동작은 종전과 같다).
|
|
159
|
-
const reviewLedger =
|
|
177
|
+
const reviewLedger = cardLedgerPath(deps);
|
|
160
178
|
if (reviewLedger) {
|
|
161
179
|
try { captureReviewResults(reviewLedger, { ...(deps.live?.now ? { now: deps.live.now } : {}), ...(deps.log ? { log: deps.log } : {}) }); } catch (error) {
|
|
162
180
|
// fail-soft — 판단은 계속하되 원인은 남긴다.
|
|
@@ -167,12 +185,14 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
|
|
|
167
185
|
let card: TaskCard | undefined;
|
|
168
186
|
let cardLookupFailed = false;
|
|
169
187
|
try {
|
|
170
|
-
card = successful ? (deps.readCard
|
|
188
|
+
card = successful ? (deps.readCard ? deps.readCard(successful.taskId) : readTaskCard(successful.taskId, cardStatePath(deps))) : undefined;
|
|
171
189
|
if (!card && successful && !deps.readCard) {
|
|
172
|
-
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(
|
|
190
|
+
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(cardStatePath(deps)).tasks ?? {};
|
|
173
191
|
const matches = Object.values(tasks).filter(candidate => candidate.status === 'launched' && candidate.text === successful.feature);
|
|
174
192
|
if (matches.length === 1) card = matches[0];
|
|
175
193
|
}
|
|
194
|
+
// TA-OWN-ALL-RUNS — 런 소유 카드: 입력 runId ⊕ 상속 ELANOUS_RUN_ID(Pod 자식의 부모 런) 중 카드 runId 와 «정확히» 같은 것만.
|
|
195
|
+
if (!card && !deps.readCard) card = runCardFor(input.runId, deps);
|
|
176
196
|
} catch {
|
|
177
197
|
// Card lookup is optional; preserve the original PR judgement when unavailable — but mark the completion kind as unmeasured.
|
|
178
198
|
cardLookupFailed = true;
|
|
@@ -282,7 +302,7 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
|
|
|
282
302
|
...(prResult.branch ? { branch: prResult.branch } : {}),
|
|
283
303
|
...(prResult.prUrl ? { prUrl: prResult.prUrl } : {}),
|
|
284
304
|
} } : {}),
|
|
285
|
-
},
|
|
305
|
+
}, liveMoveDeps(deps));
|
|
286
306
|
} else if (liveKind === 'propose-land') {
|
|
287
307
|
liveMove = await executeLiveLand(boundCard, prResult?.prNumber,
|
|
288
308
|
{ runId: input.runId, cycleId: input.cycleId, review: selfReview,
|
|
@@ -292,12 +312,12 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
|
|
|
292
312
|
...(prResult.branch ? { branch: prResult.branch } : {}),
|
|
293
313
|
...(prResult.prUrl ? { prUrl: prResult.prUrl } : {}),
|
|
294
314
|
} } : {}),
|
|
295
|
-
},
|
|
315
|
+
}, liveMoveDeps(deps));
|
|
296
316
|
} else {
|
|
297
317
|
const evidence: Record<string, string> = {};
|
|
298
318
|
if (boundCard && !delivery && judgeInput.prState === 'MERGED' && prResult) evidence[boundCard.id] = `#${prResult.prNumber}`;
|
|
299
319
|
else if (boundCard && delivery && deps.mode === 'live' && executorResult === 'done') evidence[boundCard.id] = join(delivery.projectTarget, 'elanous-out', boundCard.id);
|
|
300
|
-
liveMove = executeLiveGreenProposal(boundCard, evidence,
|
|
320
|
+
liveMove = executeLiveGreenProposal(boundCard, evidence, liveMoveDeps(deps));
|
|
301
321
|
}
|
|
302
322
|
}
|
|
303
323
|
// 판단한 수마다 live/shadow 를 남긴다 — OP 가 «오판 건수»·«그림자 일치율»을 카드·수 종류로 잰다.
|
|
@@ -315,6 +335,29 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
|
|
|
315
335
|
return liveMove ? { ...move, liveMove } : move;
|
|
316
336
|
}
|
|
317
337
|
|
|
338
|
+
/**
|
|
339
|
+
* 런 id 폴백 — 입력 runId(슈퍼바이저 · Pod 면 자식 런) → 상속 `ELANOUS_RUN_ID`(발사기가 넘긴 부모 런) 순으로,
|
|
340
|
+
* 카드 runId 와 정확히 같고 그 카드가 하나뿐일 때만 쓴다. 못 찾으면 추측하지 않고 이유 한 줄을 남긴다(조용한 미스 0).
|
|
341
|
+
*/
|
|
342
|
+
function runCardFor(runId: string | null | undefined, deps: TaskAgentShadowDeps): TaskCard | undefined {
|
|
343
|
+
const own = runId?.trim() || undefined;
|
|
344
|
+
const inherited = (deps.env ?? process.env)[HARNESS_RUN_ID_ENV]?.trim() || undefined;
|
|
345
|
+
if (!own && !inherited) return undefined;
|
|
346
|
+
const tasks = Object.values(readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(cardStatePath(deps)).tasks ?? {});
|
|
347
|
+
const tried: Array<{ source: 'run' | 'env'; id: string; bound: number }> = [];
|
|
348
|
+
for (const [source, id] of [['run', own], ['env', inherited]] as const) {
|
|
349
|
+
if (!id || tried.some((entry) => entry.id === id)) continue;
|
|
350
|
+
const bound = tasks.filter((candidate) => candidate.runId === id);
|
|
351
|
+
if (bound.length === 1) return bound[0];
|
|
352
|
+
tried.push({ source, id, bound: bound.length });
|
|
353
|
+
}
|
|
354
|
+
const reason = tried.map((entry) => `${entry.source === 'env' ? `env ${HARNESS_RUN_ID_ENV}` : 'runId'} ${entry.id}: ${entry.bound === 0 ? 'no card has this runId' : `${entry.bound} cards share this runId (ambiguous)`}`).join(' · ');
|
|
355
|
+
try {
|
|
356
|
+
(deps.log ?? ((c: string, e: string, d: Record<string, unknown>) => debug.log(c, e, d)))('task-agent', 'run-card-fallback-missed', { runId: own ?? null, inheritedRunId: inherited ?? null, tried, reason });
|
|
357
|
+
} catch { /* fail-soft */ }
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
318
361
|
/**
|
|
319
362
|
* 카드 history 에 live-move 한 줄 — 상태 파일은 카드를 읽은 곳과 같아야 한다: 주입한 경로(`live.statePath`) 또는
|
|
320
363
|
* 기본 리더를 썼을 때의 기본 경로. 주입한 카드 리더(`readCard`)만 있고 경로가 없으면 그 카드의 원장을 모른다 — 적지 않는다.
|
|
@@ -334,7 +377,7 @@ function recordLiveMoveOnCard(
|
|
|
334
377
|
};
|
|
335
378
|
try {
|
|
336
379
|
if (!move.card) { fail('no task card bound to this move'); return; }
|
|
337
|
-
const statePath =
|
|
380
|
+
const statePath = cardLedgerPath(deps);
|
|
338
381
|
if (!statePath) { fail('card ledger unknown (injected readCard without live.statePath)'); return; }
|
|
339
382
|
const card = move.card;
|
|
340
383
|
// live 인데 실행부 결과가 없으면 «무엇이 일어났는지» 모른다 — 줄을 지어내지 않고 이유를 남긴다.
|
|
@@ -358,11 +401,11 @@ function recordLiveMoveOnCard(
|
|
|
358
401
|
function liveCardFor(results: readonly SupervisorJobResult[], deps: TaskAgentShadowDeps, runId?: string | null): TaskCard | undefined {
|
|
359
402
|
try {
|
|
360
403
|
for (const result of results) {
|
|
361
|
-
const found = deps.readCard ? deps.readCard(result.taskId) : readTaskCard(result.taskId, deps
|
|
404
|
+
const found = deps.readCard ? deps.readCard(result.taskId) : readTaskCard(result.taskId, cardStatePath(deps));
|
|
362
405
|
if (found) return found;
|
|
363
406
|
}
|
|
364
407
|
if (deps.readCard) return undefined;
|
|
365
|
-
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(deps
|
|
408
|
+
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(cardStatePath(deps)).tasks ?? {};
|
|
366
409
|
// TA-LIVE-LAND-2 — a Pod run's results carry the orchestrator task id, not the card id, and its feature may be the
|
|
367
410
|
// authored goal rather than the handed sentence. `tasks hand --live` launched this process with ELANOUS_RUN_ID = the
|
|
368
411
|
// card's runId (TA-CARD-RUN-LINK); the supervisor's runId may be the Pod child's. Either id bound to exactly one card is the match.
|
|
@@ -373,6 +416,7 @@ function liveCardFor(results: readonly SupervisorJobResult[], deps: TaskAgentSha
|
|
|
373
416
|
}
|
|
374
417
|
const features = new Set(results.map((result) => result.feature));
|
|
375
418
|
const matches = Object.values(tasks).filter(candidate => candidate.status === 'launched' && features.has(candidate.text));
|
|
419
|
+
// runId ⊕ 상속 ELANOUS_RUN_ID 는 위에서 이미 봤다(판단부의 runCardFor 가 미스 이유를 남긴다).
|
|
376
420
|
return matches.length === 1 ? matches[0] : undefined;
|
|
377
421
|
} catch { return undefined; }
|
|
378
422
|
}
|