elanous 0.2.27 → 0.2.28
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/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +50 -17
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +3 -1
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +2 -1
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/self-review-cli.ts +34 -10
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +25 -2
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/msg-cli.ts +165 -9
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +80 -18
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +2 -0
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +393 -26
- package/src/cli/usage-cli.ts +9 -4
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +8 -3
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/domains/morning-report.ts +3 -1
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +101 -14
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +22 -6
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +34 -5
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +84 -14
- package/src/patents/ledger.ts +128 -0
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/release-loop/checklist.ts +11 -0
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +2 -1
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +3 -10
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +84 -17
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +43 -12
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +106 -7
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +2 -1
- package/src/task-agent/shadow.ts +13 -7
- package/src/task-agent/task-hand.ts +115 -6
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
- package/src/telegram.ts +30 -1
- package/src/user-config.ts +8 -1
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - 관측: 실행한 수마다 `debug.log('task-agent', 'live-move', {kind, card, ok, detail, live:true})`.
|
|
16
16
|
*/
|
|
17
17
|
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
-
import { createHash } from 'node:crypto';
|
|
18
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
19
19
|
import { closeSync, mkdirSync, openSync, readFileSync, statSync } from 'node:fs';
|
|
20
20
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
21
21
|
import { debug } from '../debug/log.js';
|
|
@@ -25,9 +25,9 @@ import type { DetachedSpawn } from '../cli/tasks-cli.js';
|
|
|
25
25
|
import { LIVE_MOVE_HISTORY_EVENT, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
26
26
|
import { LIVE_RETRY_MOVES } from './judge.js';
|
|
27
27
|
import { decisionCardGrounds, decisionRaiseArgs, type DecisionCardEvidence, type NextAction } from './actions.js';
|
|
28
|
-
import { reviewResultPath, TA_REVIEW_COMPLETION_CARD_ENV, TA_REVIEW_COMPLETION_RESULT_ENV, TA_REVIEW_COMPLETION_STATE_ENV } from './review-result-capture.js';
|
|
28
|
+
import { pendingReviewResults, reviewResultPath, TA_REVIEW_COMPLETION_CARD_ENV, TA_REVIEW_COMPLETION_RESULT_ENV, TA_REVIEW_COMPLETION_STATE_ENV } from './review-result-capture.js';
|
|
29
29
|
import { buildPriorFixContext, PRIOR_FIX_SCOPE_LINE, priorFixBlock, priorFixObservation, type PriorDiffReader, type PriorFixContext } from './review-prior-fix.js';
|
|
30
|
-
import { createLandWorktree, localHead, observeLandWorktree, type LandWorktree, type LandWorktreeFailure, type LandWorktreeInput, type LandWorktreeRemoval } from './land-worktree.js';
|
|
30
|
+
import { createLandWorktree, localBranch, localHead, observeLandWorktree, type LandWorktree, type LandWorktreeFailure, type LandWorktreeInput, type LandWorktreeRemoval } from './land-worktree.js';
|
|
31
31
|
|
|
32
32
|
export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green', 'propose-land', 'decision-card', ...LIVE_RETRY_MOVES] as const;
|
|
33
33
|
export type TaskAgentLiveMove = typeof TASK_AGENT_LIVE_MOVES[number];
|
|
@@ -89,7 +89,7 @@ export interface LiveMoveDeps {
|
|
|
89
89
|
raiseDecision?: (args: string[]) => Promise<{ status: number; stdout: string; stderr?: string }>;
|
|
90
90
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
91
91
|
/** TA-LAND-WORKTREE — cwd HEAD 읽기 · PR 머리 임시 워크트리 만들기(시험 주입 · 기본 = git). */
|
|
92
|
-
landWorktree?: { localHead?: (cwd: string) => string | null; create?: (input: LandWorktreeInput) => Promise<LandWorktree | LandWorktreeFailure> };
|
|
92
|
+
landWorktree?: { localHead?: (cwd: string) => string | null; localBranch?: (cwd: string) => string | null; create?: (input: LandWorktreeInput) => Promise<LandWorktree | LandWorktreeFailure> };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** `executed` = 부작용을 실제로 냈다(리뷰를 띄웠다 · 제안을 적었다). ok 이지만 이미 한 수(같은 머리·이미 제안)면 false. */
|
|
@@ -317,7 +317,7 @@ function observe(deps: LiveMoveDeps, result: LiveMoveResult): LiveMoveResult {
|
|
|
317
317
|
/** `review` — PR 머리마다 한 번. 카드에 먼저 적고(잠금 안) 그다음 띄운다 · 띄우기 실패는 카드에 오류로 남기고 같은 머리는 다시 안 띄운다. */
|
|
318
318
|
export async function executeLiveReview(
|
|
319
319
|
card: TaskCard | undefined, pr: number | undefined,
|
|
320
|
-
ctx: { runId: string | null; stopReason: string; cwd?: string; produced?: { headCommit?: string; branch?: string; prUrl?: string } },
|
|
320
|
+
ctx: { runId: string | null; stopReason: string; cwd?: string; forceNewReview?: boolean; produced?: { headCommit?: string; branch?: string; prUrl?: string } },
|
|
321
321
|
deps: LiveMoveDeps = {},
|
|
322
322
|
): Promise<LiveMoveResult> {
|
|
323
323
|
const kind = 'review' as const;
|
|
@@ -345,12 +345,16 @@ export async function executeLiveReview(
|
|
|
345
345
|
const path = deps.statePath ?? taskAgentStatePath();
|
|
346
346
|
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
347
347
|
// TA-REVIEW-RESULT-CAPTURE — 머리마다 한 번이라 경로도 머리마다 하나다. 요청 칸에 먼저 적는다(회수가 이 칸을 본다).
|
|
348
|
-
const resultPath =
|
|
348
|
+
const resultPath = ctx.forceNewReview
|
|
349
|
+
? reviewResultPath(path, card.id, pr, head.head).replace(/\.json$/, `-${randomUUID()}.json`)
|
|
350
|
+
: reviewResultPath(path, card.id, pr, head.head);
|
|
349
351
|
const claim: { claimed: boolean; missing: boolean; before?: TaskCard } = { claimed: false, missing: false };
|
|
350
352
|
updateTaskCard(path, card.id, (current) => {
|
|
351
353
|
if (!current) { claim.missing = true; return undefined; }
|
|
352
354
|
// 머리 «이력»으로 본다 — A→B→A 로 돌아와도 A 는 다시 요청하지 않는다.
|
|
353
|
-
if (
|
|
355
|
+
if (ctx.forceNewReview) {
|
|
356
|
+
if (pendingReviewResults(current).some((entry) => entry.pr === pr && entry.head === head!.head)) return undefined;
|
|
357
|
+
} else if ((current.reviewRequests ?? []).some((entry) => entry.pr === pr && entry.head === head!.head)) return undefined;
|
|
354
358
|
claim.claimed = true;
|
|
355
359
|
claim.before = current;
|
|
356
360
|
return { ...current, reviewRequests: [...(current.reviewRequests ?? []), { pr, head: head!.head, at, resultPath }] };
|
|
@@ -416,6 +420,12 @@ const LAND_BASE_STALE_LINE = /^✗ overlap-evidence: (base-stale-overlap: .*)$/;
|
|
|
416
420
|
const LAND_BASE_STALE_CONSEQUENCE_LINES: readonly RegExp[] = [/^✗ expected-head: overlap merge approval unavailable for the reviewed head$/];
|
|
417
421
|
/** 실패가 아닌 ✗ 줄 — `pr land` 가 «계속합니다»로 넘어간다(판정에 쓰지 않는다). */
|
|
418
422
|
const LAND_NON_FATAL_LINES: readonly RegExp[] = [/^✗ active-run-files: .*계속합니다\.?$/];
|
|
423
|
+
const TSC_RUNTIME_MISSING = /(?:^|\n)(?:sh: )?(?:env: )?(?:[^\s]*\/)?(?:node|bun|tsc): (?:No such file or directory|command not found)/m;
|
|
424
|
+
const TSC_NOT_RUN = /(?:tsc 종료코드 127|tsc 실행 실패 \([^\n]*code=ENOENT)/;
|
|
425
|
+
const TSC_GATE_REFUSED = '✗ typecheck: scripts/ci-typecheck-changed.ts blocked pr land.';
|
|
426
|
+
function missingTscRuntime(text: string): boolean {
|
|
427
|
+
return TSC_NOT_RUN.test(text) && TSC_RUNTIME_MISSING.test(text);
|
|
428
|
+
}
|
|
419
429
|
/** executeLiveLand 쪽 인프라 사유(출력 줄이 아니라 실행 사실). */
|
|
420
430
|
export const LAND_INFRA_EXECUTOR_REASONS = ['land-process-error', 'land-no-result-line'] as const;
|
|
421
431
|
const [LAND_PROCESS_ERROR, LAND_NO_RESULT_LINE] = LAND_INFRA_EXECUTOR_REASONS;
|
|
@@ -435,6 +445,12 @@ export function classifyLandFailure(outcome: { status: number; stdout?: string;
|
|
|
435
445
|
// 결과 줄이 «하나도» 없이 0 아닌 코드로 끝났다 — 관문까지 못 간 채 죽었다(크래시 · 신호).
|
|
436
446
|
if (!marked.length) return outcome.status !== 0 ? { class: 'infra', reason: LAND_NO_RESULT_LINE } : { class: 'judgement', reason: 'no-failure-line' };
|
|
437
447
|
const failures = marked.map((line) => line.replace(/^\[dry-run\] /, '')).filter((line) => line.startsWith('✗') && !LAND_NON_FATAL_LINES.some((re) => re.test(line)));
|
|
448
|
+
// The gate did not measure TypeScript when its interpreter was absent. The
|
|
449
|
+
// exact typecheck refusal alone remains judgement; require rc127/ENOENT and
|
|
450
|
+
// the missing runtime line, with no second failing gate.
|
|
451
|
+
if (failures.length === 1 && failures[0] === TSC_GATE_REFUSED && missingTscRuntime(lines.join('\n'))) {
|
|
452
|
+
return { class: 'infra', reason: 'tsc-runtime-missing' };
|
|
453
|
+
}
|
|
438
454
|
const infra = new Set<string>();
|
|
439
455
|
const unlisted: string[] = [];
|
|
440
456
|
for (const line of failures) {
|
|
@@ -497,7 +513,15 @@ function observeLandWorktreeCreateCapped(deps: LiveMoveDeps, data: { card: strin
|
|
|
497
513
|
|
|
498
514
|
/** 같은 머리에 한 번 더 시도해도 되나 — 앞 시도가 «전부» 끝났고(ok:false) «전부» infra 이고 상한 밑일 때만. */
|
|
499
515
|
function landRetryAllowed(prior: readonly LandAttempt[]): boolean {
|
|
500
|
-
return prior.length > 0 && prior.length < LAND_ATTEMPTS_PER_HEAD_ON_INFRA
|
|
516
|
+
return prior.length > 0 && prior.length < LAND_ATTEMPTS_PER_HEAD_ON_INFRA
|
|
517
|
+
&& prior.every((entry) => entry.ok === false && (entry.class === 'infra' || legacyMissingTscRuntime(entry)));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** A previous release classified a missing node interpreter as judgement; only that exact recorded shape may be reopened. */
|
|
521
|
+
function legacyMissingTscRuntime(entry: LandAttempt): boolean {
|
|
522
|
+
return entry.ok === false && entry.class === 'judgement' && entry.reason === TSC_GATE_REFUSED
|
|
523
|
+
&& typeof entry.detail === 'string'
|
|
524
|
+
&& classifyLandFailure({ status: 1, stderr: entry.detail }).reason === 'tsc-runtime-missing';
|
|
501
525
|
}
|
|
502
526
|
|
|
503
527
|
/**
|
|
@@ -566,7 +590,8 @@ export async function executeLiveLand(
|
|
|
566
590
|
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
567
591
|
const local = (deps.landWorktree?.localHead ?? localHead)(cwd);
|
|
568
592
|
const landKey = `land:${card.id}:${view.head.slice(0, 12)}`;
|
|
569
|
-
const
|
|
593
|
+
const branch = local?.toLowerCase() === view.head.toLowerCase() ? (deps.landWorktree?.localBranch ?? localBranch)(cwd) : null;
|
|
594
|
+
const needsLandWorktree = !!local && (local.toLowerCase() !== view.head.toLowerCase() || branch !== view.branch);
|
|
570
595
|
const createCap = landWorktreeCreateCap();
|
|
571
596
|
let claimed = false;
|
|
572
597
|
let missing = false;
|
|
@@ -603,7 +628,8 @@ export async function executeLiveLand(
|
|
|
603
628
|
const attempt = prior.length + 1;
|
|
604
629
|
if (attempt > 1) {
|
|
605
630
|
const last = prior.at(-1)!;
|
|
606
|
-
|
|
631
|
+
const legacy = legacyMissingTscRuntime(last);
|
|
632
|
+
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: legacy ? 'tsc-runtime-missing-legacy' : last.reason ?? 'unknown', class: 'infra', decision: legacy ? 'legacy-reclassified-retrying' : 'retrying' }); } catch { /* fail-soft */ }
|
|
607
633
|
}
|
|
608
634
|
if (cycle) landedCycles.add(cycle);
|
|
609
635
|
// TA-LAND-WORKTREE — `pr land --expected-head` 는 «로컬 HEAD = 고정 머리»를 요구한다. cwd 가 다른 머리면(Pod 런 PR · 호스트 checkout)
|
|
@@ -618,6 +644,11 @@ export async function executeLiveLand(
|
|
|
618
644
|
if ('reason' in made) {
|
|
619
645
|
const reason = made.reason;
|
|
620
646
|
observeLandWorktree({ card: card.id, pr, head: view.head, created: made.created, removed: made.removed, reason }, deps.log);
|
|
647
|
+
if (made.kind === 'worktree-conflict') {
|
|
648
|
+
try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'worktree-conflict', {
|
|
649
|
+
card: card.id, pr, head: view.head, branch: view.branch ?? null, ownerPath: made.ownerPath ?? 'unknown', reason,
|
|
650
|
+
}); } catch { /* fail-soft */ }
|
|
651
|
+
}
|
|
621
652
|
let failures: LandWorktreeCreateFailure[] = [];
|
|
622
653
|
let recordFailureError: unknown;
|
|
623
654
|
try {
|
|
@@ -655,7 +686,7 @@ export async function executeLiveLand(
|
|
|
655
686
|
} catch (error) {
|
|
656
687
|
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 */ }
|
|
657
688
|
}
|
|
658
|
-
observeLandWorktree({ card: card.id, pr, head: view.head, created:
|
|
689
|
+
observeLandWorktree({ card: card.id, pr, head: view.head, created: made.created ?? null, removed: false, reason: `cwd HEAD ${local.slice(0, 12)} · branch ${branch ?? 'unknown'} differs from PR ${view.head.slice(0, 12)} · ${view.branch ?? 'unknown'}` }, deps.log);
|
|
659
690
|
}
|
|
660
691
|
// 증거 경로는 «원래» cwd 기준으로 고정한다 — 임시 트리로 cwd 를 바꿔도 상대경로가 다른 파일을 가리키지 않게(같은 cwd 면 그대로).
|
|
661
692
|
const overlapEvidence = ctx.overlapEvidence && worktree ? resolve(cwd, ctx.overlapEvidence) : ctx.overlapEvidence;
|
|
@@ -681,7 +712,7 @@ export async function executeLiveLand(
|
|
|
681
712
|
// 성공이든 실패든 걷는다.
|
|
682
713
|
let removal: LandWorktreeRemoval;
|
|
683
714
|
try { removal = worktree.cleanup(); } catch (error) { removal = { removed: false, reason: String(error).slice(-200) }; }
|
|
684
|
-
observeLandWorktree({ card: card.id, pr, head: view.head, created:
|
|
715
|
+
observeLandWorktree({ card: card.id, pr, head: view.head, created: worktree.created ?? null, removed: removal.removed, ...(removal.branchRemoved !== undefined ? { branchRemoved: removal.branchRemoved } : {}),
|
|
685
716
|
reason: removal.reason ?? `land ${ok ? 'ok' : 'failed'} — removed` }, deps.log);
|
|
686
717
|
}
|
|
687
718
|
const landAttempt: LiveMoveResult['landAttempt'] = failure
|
|
@@ -159,7 +159,7 @@ export function remoteLaunchScript(target: ParentHostConfig, args: readonly stri
|
|
|
159
159
|
const elanous = target.elanous ?? DEFAULT_REMOTE_ELANOUS;
|
|
160
160
|
const configDir = target.configDir ?? DEFAULT_REMOTE_CONFIG_DIR;
|
|
161
161
|
const log = remoteParentLogPath(target, env.ELANOUS_RUN_ID ?? 'unknown');
|
|
162
|
-
const envPart = Object.entries(env).filter(([key]) => /^[A-Z_][A-Z0-9_]*$/.test(key)).map(([key, value]) => `${key}=${shellQuote(value)}`).join(' ');
|
|
162
|
+
const envPart = [...Object.entries(env).filter(([key]) => key !== 'ELANOUS_STATE_DIR' && /^[A-Z_][A-Z0-9_]*$/.test(key)).map(([key, value]) => `${key}=${shellQuote(value)}`), `ELANOUS_STATE_DIR=${remotePath(configDir)}`].join(' ');
|
|
163
163
|
const argv = [remotePath(elanous), '--config-dir', remotePath(configDir), ...remoteParentArgs(args, target.podPool).map(shellQuote)].join(' ');
|
|
164
164
|
// 준비가 실패하면 «확실히 안 띄웠다»(not-launched=…)를 찍고 끝낸다 — 그때만 호출부가 로컬로 돌아간다.
|
|
165
165
|
// 띄운 뒤에는 짧게 살아 있는지 보고 pid 를 찍는다(조기 종료면 early-exit — 역시 «안 돈다»가 확실).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { listChecklist, setItemIfSnapshot, type Checklist } from '../release-loop/checklist.js';
|
|
3
|
+
import { judgeLandedCell, parseJudgeSignals, type GreenVerdict } from '../release-loop/landed-to-green.js';
|
|
4
|
+
import { cardPrNumber, readTaskAgentState, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
5
|
+
|
|
6
|
+
export interface LandedTaskPr { number: number; url: string; headRefOid: string; state: string; baseRefName: string; mergedAt: string; files?: Array<{ path: string }> }
|
|
7
|
+
export interface PostlandDeps {
|
|
8
|
+
statePath?: string;
|
|
9
|
+
versions: readonly string[];
|
|
10
|
+
checklist?: (version: string) => Checklist;
|
|
11
|
+
setStatus?: typeof setItemIfSnapshot;
|
|
12
|
+
updateCard?: typeof updateTaskCard;
|
|
13
|
+
judge?: typeof judgeLandedCell;
|
|
14
|
+
log?: typeof debug.log;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read-only PR binding for the legacy completion guard, including when judgment or ledger I/O fails. */
|
|
18
|
+
export function boundMergedTaskCells(pr: Pick<LandedTaskPr, 'number' | 'url'>, statePath = taskAgentStatePath()): string[] {
|
|
19
|
+
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {};
|
|
20
|
+
const cards = Object.values(tasks).filter((card) => card.taLand && card.checklistId && cardPrNumber(card) === pr.number
|
|
21
|
+
&& typeof card.pr === 'object' && card.pr.url?.replace(/\/+$/, '') === pr.url.replace(/\/+$/, ''));
|
|
22
|
+
return [...new Set(cards.map((card) => card.checklistId!))];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** When the PR API omits URL/head, keep candidate TA cells out of the prose-only green path. */
|
|
26
|
+
export function candidateMergedTaskCells(number: number, statePath = taskAgentStatePath()): string[] {
|
|
27
|
+
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {};
|
|
28
|
+
return [...new Set(Object.values(tasks).filter((card) => card.taLand && card.checklistId && cardPrNumber(card) === number).map((card) => card.checklistId!))];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A bound TA card closes only after its exact PR and a unique release cell are confirmed. */
|
|
32
|
+
export function closeMergedTaskCard(pr: LandedTaskPr, cwd: string, deps: PostlandDeps): string | null {
|
|
33
|
+
const log = deps.log ?? debug.log.bind(debug);
|
|
34
|
+
if (pr.state !== 'MERGED' || pr.baseRefName !== 'main' || !/^[a-f0-9]{40}$/i.test(pr.headRefOid) || !pr.url) return null;
|
|
35
|
+
const path = deps.statePath ?? taskAgentStatePath();
|
|
36
|
+
const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(path).tasks ?? {};
|
|
37
|
+
const cards = Object.values(tasks).filter((card) => card.taLand && card.checklistId && cardPrNumber(card) === pr.number
|
|
38
|
+
&& typeof card.pr === 'object' && card.pr.url?.replace(/\/+$/, '') === pr.url.replace(/\/+$/, ''));
|
|
39
|
+
if (cards.length !== 1) {
|
|
40
|
+
log('task-agent', 'postland-card-skipped', { pr: pr.number, reason: cards.length ? 'ambiguous-card' : 'no-bound-card' });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const card = cards[0]!;
|
|
44
|
+
if (!(card.landAttempts ?? []).some((attempt) => attempt.pr === pr.number && attempt.head.toLowerCase() === pr.headRefOid.toLowerCase())) {
|
|
45
|
+
log('task-agent', 'postland-card-skipped', { pr: pr.number, card: card.id, reason: 'land-head-not-bound' });
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const candidateVersions = card.checklistVersion ? [card.checklistVersion] : deps.versions;
|
|
49
|
+
const cells = candidateVersions.flatMap((version) => {
|
|
50
|
+
const snapshot = (deps.checklist ?? listChecklist)(version);
|
|
51
|
+
const item = snapshot.items.find((row) => row.id === card.checklistId);
|
|
52
|
+
return item ? [{ version, snapshot, item }] : [];
|
|
53
|
+
});
|
|
54
|
+
if (cells.length !== 1) {
|
|
55
|
+
log('task-agent', 'postland-card-skipped', { pr: pr.number, card: card.id, reason: cells.length ? 'ambiguous-cell-version' : 'cell-not-found' });
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const { version, item } = cells[0]!;
|
|
59
|
+
const expected = { title: item.title, status: item.status, evidence: item.evidence };
|
|
60
|
+
const expectedTitle = expected.title;
|
|
61
|
+
const prior = (card as TaskCard & { postLand?: { pr: number; head: string; outcome: 'complete' | 'partial'; evidence: string[] } }).postLand;
|
|
62
|
+
const marker = `TA postland #${pr.number}/${pr.headRefOid.slice(0, 12)}:`;
|
|
63
|
+
const currentEvidenceLine = item.evidence?.split('\n').find((line) => line.startsWith(marker));
|
|
64
|
+
const terminalBound = card.status === 'done' && card.prTerminal?.pr === pr.number
|
|
65
|
+
&& card.prTerminal.head.toLowerCase() === pr.headRefOid.toLowerCase()
|
|
66
|
+
&& card.prTerminal.url.replace(/\/+$/, '') === pr.url.replace(/\/+$/, '');
|
|
67
|
+
if (prior?.pr === pr.number && prior.head.toLowerCase() === pr.headRefOid.toLowerCase()
|
|
68
|
+
&& prior.outcome === 'complete' && terminalBound && item.status === 'green' && currentEvidenceLine
|
|
69
|
+
&& prior.evidence?.length && prior.evidence.every((part) => currentEvidenceLine.includes(part))) return item.id;
|
|
70
|
+
const alreadyGreen = item.status === 'green';
|
|
71
|
+
let verdict: GreenVerdict | undefined = alreadyGreen ? 'proposed' : undefined;
|
|
72
|
+
let judgeError: string | undefined;
|
|
73
|
+
if (!alreadyGreen) {
|
|
74
|
+
try { verdict = (deps.judge ?? judgeLandedCell)({ version, id: item.id, title: expectedTitle, pr: pr.number }, { cwd }); }
|
|
75
|
+
catch (error) { judgeError = String(error).slice(0, 240); }
|
|
76
|
+
}
|
|
77
|
+
const complete = verdict === 'proposed';
|
|
78
|
+
const criterion = expectedTitle.split(/\r?\n/).filter((line) => /^\s*판정(?: 신호)?:/.test(line)).map((line) => line.trim());
|
|
79
|
+
const remaining = complete ? [] : criterion.length ? criterion : ['판정 줄 없음 — 칸 주인의 판정 필요'];
|
|
80
|
+
const allPaths = (pr.files ?? []).map((file) => file.path).filter(Boolean);
|
|
81
|
+
const paths = allPaths.slice(0, 20);
|
|
82
|
+
const omitted = allPaths.length - paths.length;
|
|
83
|
+
const observations = parseJudgeSignals(expectedTitle).map((signal) => signal.observe).filter((value): value is string => !!value);
|
|
84
|
+
const judgeEvidence = alreadyGreen ? '기존 green 상태 확인(새 측정 0)' : judgeError
|
|
85
|
+
? `판정 실행 오류: ${judgeError}`
|
|
86
|
+
: `판정 ${verdict ?? 'error'}: ${observations.length ? observations.join(' · ') : '측정 명령 없음'}${verdict === 'proposed' ? ' → exit 0' : ''}`;
|
|
87
|
+
const changed = paths.length ? `changed: ${paths.join(' · ')}${omitted ? ` · 외 ${omitted}개(${pr.url}에서 전체 확인)` : ''}`
|
|
88
|
+
: `changed: 경로 목록 미제공(${pr.url} 참조)`;
|
|
89
|
+
const filled = [`PR #${pr.number} merged, head ${pr.headRefOid}`, `카드 land head 대조 일치`, changed, judgeEvidence];
|
|
90
|
+
const note = complete
|
|
91
|
+
? `${marker} ${filled.join(' | ')}; ${alreadyGreen ? '기존 green 유지' : '판정 신호 통과 → green'}.`
|
|
92
|
+
: `${marker} 이 조각이 채운 것: ${filled.join(' | ')}; 남은 것: ${remaining.join(' | ')}; 다음 조각 retitle 후보: ${remaining[0]}`;
|
|
93
|
+
const priorLines = item.evidence?.split('\n').filter((line) => !line.startsWith(marker)) ?? [];
|
|
94
|
+
const evidence = [...priorLines, note].join('\n');
|
|
95
|
+
const desiredStatus = complete ? 'green' : item.status === 'red' ? 'red' : 'yellow';
|
|
96
|
+
if (prior?.pr === pr.number && prior.head.toLowerCase() === pr.headRefOid.toLowerCase()
|
|
97
|
+
&& prior.outcome === (complete ? 'complete' : 'partial') && terminalBound
|
|
98
|
+
&& item.status === desiredStatus && item.evidence === evidence) return item.id;
|
|
99
|
+
const at = new Date().toISOString();
|
|
100
|
+
let wrote = false;
|
|
101
|
+
// The card lock protects the binding through the ledger write. A failed ledger write
|
|
102
|
+
// throws before the card changes, so the next pass can retry both records.
|
|
103
|
+
(deps.updateCard ?? updateTaskCard)(path, card.id, (current, currentTasks) => {
|
|
104
|
+
const matching = Object.values(currentTasks).filter((candidate) => candidate.taLand && cardPrNumber(candidate) === pr.number
|
|
105
|
+
&& typeof candidate.pr === 'object' && candidate.pr.url?.replace(/\/+$/, '') === pr.url.replace(/\/+$/, ''));
|
|
106
|
+
if (matching.length !== 1 || matching[0]?.id !== card.id) return undefined;
|
|
107
|
+
if (!current || cardPrNumber(current) !== pr.number || current.checklistId !== item.id
|
|
108
|
+
|| current.checklistVersion !== card.checklistVersion || typeof current.pr !== 'object'
|
|
109
|
+
|| current.pr.url?.replace(/\/+$/, '') !== pr.url.replace(/\/+$/, '')
|
|
110
|
+
|| !(current.landAttempts ?? []).some((attempt) => attempt.pr === pr.number && attempt.head.toLowerCase() === pr.headRefOid.toLowerCase())) return undefined;
|
|
111
|
+
const existing = (current as TaskCard & { postLand?: { pr: number; head: string; outcome: 'complete' | 'partial' } }).postLand;
|
|
112
|
+
const currentTerminalBound = current.status === 'done' && current.prTerminal?.pr === pr.number
|
|
113
|
+
&& current.prTerminal.head.toLowerCase() === pr.headRefOid.toLowerCase()
|
|
114
|
+
&& current.prTerminal.url.replace(/\/+$/, '') === pr.url.replace(/\/+$/, '');
|
|
115
|
+
if (existing?.pr === pr.number && existing.head.toLowerCase() === pr.headRefOid.toLowerCase()
|
|
116
|
+
&& existing.outcome === (complete ? 'complete' : 'partial') && currentTerminalBound
|
|
117
|
+
&& item.status === desiredStatus && item.evidence === evidence) return undefined;
|
|
118
|
+
const currentVersions = current.checklistVersion ? [current.checklistVersion] : deps.versions;
|
|
119
|
+
const currentCells = currentVersions.flatMap((candidateVersion) =>
|
|
120
|
+
(deps.checklist ?? listChecklist)(candidateVersion).items.some((row) => row.id === item.id) ? [candidateVersion] : []);
|
|
121
|
+
if (currentCells.length !== 1 || currentCells[0] !== version) return undefined;
|
|
122
|
+
(deps.setStatus ?? setItemIfSnapshot)(version, item.id, { evidence, status: desiredStatus }, 'task-agent', expected);
|
|
123
|
+
wrote = true;
|
|
124
|
+
const sameOutcome = existing?.pr === pr.number && existing.head.toLowerCase() === pr.headRefOid.toLowerCase()
|
|
125
|
+
&& existing.outcome === (complete ? 'complete' : 'partial');
|
|
126
|
+
return { ...current,
|
|
127
|
+
status: 'done' as const,
|
|
128
|
+
prTerminal: { pr: pr.number, url: pr.url, head: pr.headRefOid, state: 'MERGED' as const, at },
|
|
129
|
+
postLand: { pr: pr.number, head: pr.headRefOid, at, outcome: complete ? 'complete' as const : 'partial' as const, evidence: filled, remaining },
|
|
130
|
+
history: sameOutcome ? current.history : [...current.history, { at, event: 'postland-card', pr: pr.number, head: pr.headRefOid, detail: complete ? `green ${version}/${item.id}` : `${desiredStatus} ${version}/${item.id}; ${remaining.join(' | ')}` }],
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
if (!wrote) {
|
|
134
|
+
log('task-agent', 'postland-card-pending', { pr: pr.number, card: card.id, version, id: item.id, reason: 'card-binding-changed-before-write' });
|
|
135
|
+
return item.id;
|
|
136
|
+
}
|
|
137
|
+
log('task-agent', 'postland-card-closed', { pr: pr.number, card: card.id, version, id: item.id, outcome: complete ? 'complete' : 'partial', verdict });
|
|
138
|
+
return item.id;
|
|
139
|
+
}
|
|
@@ -40,9 +40,12 @@ import type { DetachedSpawn } from '../cli/tasks-cli.js';
|
|
|
40
40
|
import { remoteLedgerMirrorDir } from './parent-host.js';
|
|
41
41
|
import { appendLandOrderRow, joinLandOrderRow } from './land-order-ledger.js';
|
|
42
42
|
import { recordLiveMoveHistory } from './live-move-history.js';
|
|
43
|
+
import { runRepairCycle, type RepairLaunchRequest } from './repair-cycle.js';
|
|
44
|
+
import { repairMustFixFromReview, repairReviewDigest } from './repair-review.js';
|
|
45
|
+
import type { RepairCasOutcome, RepairCasRequest } from './repair-branch-cas.js';
|
|
43
46
|
import { defaultLandPrHead, defaultPrHead, defaultReviewRepoCandidates, executeLiveLand, executeLiveReview, landRetryPending, type LiveMoveDeps, type LiveMoveResult, type PrHeadView, type ReviewRepoCandidate, type TaskAgentLiveMove } from './live-moves.js';
|
|
44
|
-
import { capturedLandReview, captureReviewResults, parseReviewResultOutput, pendingReviewResults, type ParsedReviewResult } from './review-result-capture.js';
|
|
45
|
-
import { cardPrNumber, readTaskCard, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
47
|
+
import { capturedLandReview, captureReviewResults, parseReviewResultOutput, pendingReviewResults, reviewResultPath, type ParsedReviewResult } from './review-result-capture.js';
|
|
48
|
+
import { boundPrTerminal, cardPrNumber, LIVE_MOVE_RESULT_EVENT, readTaskCard, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
46
49
|
|
|
47
50
|
export const REJUDGE_ON_HEAD_EVENT = 'rejudge-on-head';
|
|
48
51
|
/** 카드·PR 당 재게이트하는 머리 수 상한(머리가 바뀔 때마다 하나) — 넘으면 멈추고 사람에게 넘긴다. */
|
|
@@ -52,7 +55,7 @@ export const CATCH_UP_CAP = 2;
|
|
|
52
55
|
/** 떼어 띄운 재게이트 결과를 기다리는 상한 — 지나도 파일이 없으면 «결과 없음»으로 적는다(같은 머리는 다시 안 돌린다). */
|
|
53
56
|
export const REGATE_RESULT_WAIT_MS = 90 * 60_000;
|
|
54
57
|
|
|
55
|
-
export type RejudgeStep = 'regate' | 'review' | 'ready' | 'land' | 'target' | 'catch-up';
|
|
58
|
+
export type RejudgeStep = 'regate' | 'review' | 'ready' | 'land' | 'target' | 'catch-up' | 'repair';
|
|
56
59
|
export type CatchUpDecision = 'pushed' | 'conflict' | 'cap-reached' | 'failed';
|
|
57
60
|
export interface RejudgeOutcome {
|
|
58
61
|
card: string; pr: number | null; fromHead: string | null; toHead: string | null; step: RejudgeStep; decision: string; reason: string;
|
|
@@ -82,6 +85,10 @@ export interface RejudgeDeps {
|
|
|
82
85
|
readyPr?: (pr: number, cwd: string) => Promise<{ ok: boolean; reason: string }>;
|
|
83
86
|
/** 재게이트를 떼어 띄운다 — 시험은 주입한다. 기본은 `tasks regate-run` 자식(stdio ignore · 결과는 파일). */
|
|
84
87
|
requestRegate?: (request: RegateRequest) => Promise<void>;
|
|
88
|
+
/** Source PR/head repair worker; absent uses the detached HQ local parent. */
|
|
89
|
+
requestRepair?: (request: RepairLaunchRequest) => Promise<void>;
|
|
90
|
+
applyRepair?: (request: RepairCasRequest) => RepairCasOutcome;
|
|
91
|
+
notifyRepair?: (seat: string, body: string) => void;
|
|
85
92
|
/** TA-REJUDGE-REBASE-LOOP — origin/main 을 PR 가지에 3-way merge 하고 일반 push. 시험은 주입한다. */
|
|
86
93
|
catchUpMain?: (request: CatchUpRequest) => Promise<CatchUpResult>;
|
|
87
94
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
@@ -405,7 +412,9 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
|
|
|
405
412
|
if (!card || pr === undefined) return target('no-pr', 'card has no bound PR');
|
|
406
413
|
const reviews = (card.reviewRequests ?? []).filter((entry) => entry.pr === pr);
|
|
407
414
|
// 원격 부모는 node-b 의 liveMoves 가 꺼져 리뷰 이력이 0일 수 있다. HQ 거울의 종결 증거가 있을 때만 시작한다.
|
|
408
|
-
|
|
415
|
+
const adopted = card.adoptedPr?.pr === pr && typeof card.pr === 'object'
|
|
416
|
+
&& !!card.pr.url && sameUrl(card.adoptedPr.url, card.pr.url) && /^[a-f0-9]{40}$/i.test(card.adoptedPr.head);
|
|
417
|
+
if (!reviews.length && !adopted && (!card.parentHost || !(deps.remoteRunTerminal ?? remoteBoundRunTerminal)(card)))
|
|
409
418
|
return target('not-started', 'no task-agent review on this PR yet (remote terminal unproven or run not stopped/judged)', pr);
|
|
410
419
|
// 떼어 띄운 재게이트 결과를 «머리와 무관하게» 먼저 회수한다 — 결과가 나오기 전에 머리가 또 바뀌어도 그 머리의 결과는 카드에 남는다.
|
|
411
420
|
for (const pending of (card.regates ?? []).filter((entry) => entry.pr === pr && entry.passed === undefined && entry.resultPath)) {
|
|
@@ -442,10 +451,99 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
|
|
|
442
451
|
if ('reason' in found) return target('pr-unreadable', found.reason, pr, fromHead);
|
|
443
452
|
const { cwd, view } = found;
|
|
444
453
|
const toHead = view.head;
|
|
454
|
+
if (view.state === 'MERGED' || view.state === 'CLOSED') {
|
|
455
|
+
const at = now().toISOString();
|
|
456
|
+
updateTaskCard(statePath, card.id, (current) => {
|
|
457
|
+
if (!current || cardPrNumber(current) !== pr || typeof current.pr !== 'object' || current.pr.url !== url) return undefined;
|
|
458
|
+
const previous = boundPrTerminal(current);
|
|
459
|
+
if (previous?.state === view.state && previous.head === toHead) return undefined;
|
|
460
|
+
return { ...current, prTerminal: { pr, url, head: toHead, state: view.state as 'MERGED' | 'CLOSED', at } };
|
|
461
|
+
});
|
|
462
|
+
return target('pr-not-open', `PR #${pr} is ${view.state}`, pr, fromHead, toHead);
|
|
463
|
+
}
|
|
445
464
|
if (view.state !== 'OPEN') return target('pr-not-open', `PR #${pr} is ${view.state}`, pr, fromHead, toHead);
|
|
465
|
+
// An explicitly rejudged PR can be reopened. Clear the old marker before normal head judgement.
|
|
466
|
+
if (boundPrTerminal(card)) {
|
|
467
|
+
updateTaskCard(statePath, card.id, (current) => current && cardPrNumber(current) === pr && typeof current.pr === 'object' && current.pr.url === url
|
|
468
|
+
? { ...current, prTerminal: undefined } : undefined);
|
|
469
|
+
card = readTaskCard(card.id, statePath) ?? card;
|
|
470
|
+
}
|
|
446
471
|
const step = (s: RejudgeStep, decision: string, reason: string, record: boolean, extra: Partial<RejudgeOutcome> = {}) =>
|
|
447
472
|
emit({ card: card!.id, pr, fromHead, toHead, step: s, decision, reason, ...extra }, record);
|
|
448
473
|
|
|
474
|
+
// Adoption is a first judgement for the current head, even when the implementation run
|
|
475
|
+
// already asked for a review. A captured failure needs repair; an accepted result must
|
|
476
|
+
// reach the host regate instead of stopping at the old same-head shortcut below.
|
|
477
|
+
const adoptedReview = adopted ? (card.history ?? []).filter((item) => item.event === LIVE_MOVE_RESULT_EVENT
|
|
478
|
+
&& item.kind === 'review' && item.pr === pr && item.head && sameHead(item.head, toHead)
|
|
479
|
+
&& item.reviewed === true && ['pass', 'warn', 'fail'].includes(item.verdict ?? '')).at(-1) : undefined;
|
|
480
|
+
if (adopted && !adoptedReview) {
|
|
481
|
+
if (reviews.some((entry) => sameHead(entry.head, toHead))) {
|
|
482
|
+
step('review', 'review-pending', `PR #${pr} head ${toHead.slice(0, 12)} already has a review request; await its result`, false);
|
|
483
|
+
} else if (!deps.liveMoves.has('review')) {
|
|
484
|
+
step('review', 'would-review', 'adopted PR first judgement · review not enabled in HQ taskAgent.liveMoves — shadow', false);
|
|
485
|
+
} else {
|
|
486
|
+
const before = { reviewRequests: card.reviewRequests, landAttempts: card.landAttempts };
|
|
487
|
+
const result = await executeLiveReview(card, pr, { runId: card.runId ?? null, stopReason: 'adopted-pr-first-judgement', cwd }, { ...live, ...(deps.log ? { log: deps.log } : {}) });
|
|
488
|
+
recordMove(statePath, card.id, card.runId, 'review', result, pr, before, now, deps.log);
|
|
489
|
+
step('review', result.executed ? 'review-requested' : 'review-request-failed', result.detail, true);
|
|
490
|
+
}
|
|
491
|
+
return outcomes;
|
|
492
|
+
}
|
|
493
|
+
const capturedForHead = (card.history ?? []).filter((item) => item.event === LIVE_MOVE_RESULT_EVENT
|
|
494
|
+
&& item.kind === 'review' && item.pr === pr && item.head && sameHead(item.head, toHead)
|
|
495
|
+
&& item.reviewed === true && ['pass', 'warn', 'fail'].includes(item.verdict ?? '')).at(-1);
|
|
496
|
+
if (capturedForHead && (capturedForHead.verdict === 'fail' || (capturedForHead.mustFix ?? 0) > 0)) {
|
|
497
|
+
const capturedPath = capturedForHead.resultPath;
|
|
498
|
+
const requestMatches = !!capturedPath && (card.reviewRequests ?? []).some((entry) => entry.pr === pr
|
|
499
|
+
&& sameHead(entry.head, toHead) && entry.resultPath === capturedPath);
|
|
500
|
+
let mustFix: string[] | null = null;
|
|
501
|
+
try {
|
|
502
|
+
if (requestMatches && /^[0-9a-f]{64}$/.test(capturedForHead.reviewSha256 ?? '')) {
|
|
503
|
+
const text = read(capturedPath!);
|
|
504
|
+
if (repairReviewDigest(text) === capturedForHead.reviewSha256) mustFix = repairMustFixFromReview(text, pr, toHead);
|
|
505
|
+
}
|
|
506
|
+
} catch { /* fail closed below */ }
|
|
507
|
+
if (!mustFix && (deps.liveMoves.has('narrow-relaunch') || deps.liveMoves.has('salvage-relaunch') || deps.liveMoves.has('alternative'))
|
|
508
|
+
&& !(card.repairs ?? []).some((entry) => entry.pr === pr && (entry.status === 'requested' || entry.status === 'unconfirmed'))) {
|
|
509
|
+
if (pendingReviewResults(card).some((entry) => entry.pr === pr && sameHead(entry.head, toHead))) {
|
|
510
|
+
step('review', 'review-pending', 'replacement exact-head review pending before repair', false);
|
|
511
|
+
return outcomes;
|
|
512
|
+
}
|
|
513
|
+
const originalPath = reviewResultPath(statePath, card.id, pr, toHead);
|
|
514
|
+
const retried = (card.reviewRequests ?? []).some((entry) => entry.pr === pr && sameHead(entry.head, toHead)
|
|
515
|
+
&& entry.resultPath && entry.resultPath !== originalPath);
|
|
516
|
+
if (deps.liveMoves.has('review') && !retried) {
|
|
517
|
+
const before = { reviewRequests: card.reviewRequests, landAttempts: card.landAttempts };
|
|
518
|
+
const result = await executeLiveReview(card, pr, { runId: card.runId ?? null, stopReason: 'captured-review-identity-unavailable', cwd, forceNewReview: true },
|
|
519
|
+
{ ...live, ...(deps.log ? { log: deps.log } : {}) });
|
|
520
|
+
recordMove(statePath, card.id, card.runId, 'review', result, pr, before, now, deps.log);
|
|
521
|
+
step('review', result.executed ? 'review-requested' : 'review-request-failed', result.detail, true);
|
|
522
|
+
return outcomes;
|
|
523
|
+
}
|
|
524
|
+
step('repair', 'repair-stopped', 'captured review result identity missing or changed; request a new review', true);
|
|
525
|
+
return outcomes;
|
|
526
|
+
}
|
|
527
|
+
if (mustFix?.length || (card.repairs ?? []).some((entry) => entry.pr === pr && (entry.status === 'requested' || entry.status === 'unconfirmed'))) {
|
|
528
|
+
if (!view.branch) {
|
|
529
|
+
step('repair', 'repair-stopped', 'PR branch name unavailable', true);
|
|
530
|
+
return outcomes;
|
|
531
|
+
}
|
|
532
|
+
const result = await runRepairCycle({ card, statePath, repo: cwd, pr, head: toHead, branch: view.branch,
|
|
533
|
+
mustFix: mustFix ?? [], liveMoves: deps.liveMoves, now: now(), readFile: read,
|
|
534
|
+
...(deps.requestRepair ? { request: deps.requestRepair } : {}),
|
|
535
|
+
...(deps.applyRepair ? { apply: deps.applyRepair } : {}),
|
|
536
|
+
...(deps.notifyRepair ? { notify: deps.notifyRepair } : {}) });
|
|
537
|
+
step('repair', result.decision, result.reason, true);
|
|
538
|
+
return outcomes;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (adopted && (adoptedReview?.verdict === 'fail' || !capturedLandReview(card, pr, toHead)
|
|
542
|
+
|| capturedLandReview(card, pr, toHead)?.mustFixCount !== 0)) {
|
|
543
|
+
step('review', 'repair-needed', `PR #${pr} head ${toHead.slice(0, 12)} review ${adoptedReview?.verdict ?? 'unknown'} · must-fix ${adoptedReview?.mustFix ?? 'unknown'} — owner repair`, true);
|
|
544
|
+
return outcomes;
|
|
545
|
+
}
|
|
546
|
+
|
|
449
547
|
// A remote --ta-land parent leaves its PR draft. Only a captured review for this exact
|
|
450
548
|
// head may let HQ make it ready; the regate runner deliberately rejects draft PRs.
|
|
451
549
|
let readyConfirmed = false;
|
|
@@ -527,7 +625,7 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
|
|
|
527
625
|
};
|
|
528
626
|
|
|
529
627
|
// 기존 live 경로의 같은 머리 조기 반환은 유지한다. 완료 훅의 그림자 판단만 통과 리뷰를 다시 본다.
|
|
530
|
-
if (!regates.length && judgedHead && sameHead(toHead, judgedHead) && !landRetryPending(card, pr, toHead)) {
|
|
628
|
+
if (!adopted && !regates.length && judgedHead && sameHead(toHead, judgedHead) && !landRetryPending(card, pr, toHead)) {
|
|
531
629
|
const unchangedReview = capturedLandReview(card, pr, toHead);
|
|
532
630
|
if (!deps.shadowSameHead || deps.liveMoves.size !== 0 || !unchangedReview || !['pass', 'warn'].includes(unchangedReview.verdict) || unchangedReview.mustFixCount !== 0) {
|
|
533
631
|
return target('head-unchanged', `PR #${pr} head is still the judged head`, pr, fromHead, toHead);
|
|
@@ -687,7 +785,8 @@ function recordMove(statePath: string, cardId: string, runId: string | undefined
|
|
|
687
785
|
export function rejudgeCandidates(tasks: Readonly<Record<string, TaskCard>>): string[] {
|
|
688
786
|
return Object.values(tasks).filter((card) => {
|
|
689
787
|
const pr = cardPrNumber(card);
|
|
690
|
-
return pr !== undefined && ((card.reviewRequests ?? []).some((entry) => entry.pr === pr)
|
|
691
|
-
|| (card.status === 'launched' && card.taLand === true && !!card.parentHost && !!card.runId && !!card.runChildId)
|
|
788
|
+
return pr !== undefined && !boundPrTerminal(card) && ((card.reviewRequests ?? []).some((entry) => entry.pr === pr)
|
|
789
|
+
|| (card.status === 'launched' && card.taLand === true && ((!!card.parentHost && !!card.runId && !!card.runChildId)
|
|
790
|
+
|| (card.adoptedPr?.pr === pr && typeof card.pr === 'object' && !!card.pr.url && sameUrl(card.adoptedPr.url, card.pr.url)))));
|
|
692
791
|
}).map((card) => card.id);
|
|
693
792
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Bind remote TA parents from the HQ ledger mirror before `rejudge --all` selects PR cards. */
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { loadRunLedger } from '../self-implement/run-ledger.js';
|
|
5
|
+
import { bindCardEvidence, podChildRunIds, readCardRunSnapshot, refreshCardRunBinding } from './card-evidence.js';
|
|
6
|
+
import { remoteLedgerMirrorDir } from './parent-host.js';
|
|
7
|
+
import { cardPrNumber, type TaskCard } from './task-hand.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A remote child PR can appear after the last HQ card refresh. A card without a PR is otherwise absent from
|
|
11
|
+
* `rejudgeCandidates`, so the normal rejudge tick never reaches `mirrorRemoteCardLedger` or the binding replay.
|
|
12
|
+
* Only an existing, launched TA-owned parent with a parent→child edge in its own host mirror is admitted. The
|
|
13
|
+
* launch registry (including foreign/test universes) and child-only ledgers are never scanned or adopted here.
|
|
14
|
+
*/
|
|
15
|
+
export async function bindMirroredRemoteRejudgeCards(
|
|
16
|
+
tasks: Readonly<Record<string, TaskCard>>, statePath: string,
|
|
17
|
+
): Promise<{ scanned: number; bound: number; skipped: number }> {
|
|
18
|
+
const root = dirname(statePath);
|
|
19
|
+
let scanned = 0;
|
|
20
|
+
let bound = 0;
|
|
21
|
+
let skipped = 0;
|
|
22
|
+
for (const card of Object.values(tasks)) {
|
|
23
|
+
if (card.status !== 'launched' || card.taLand !== true || !card.parentHost || !card.runId
|
|
24
|
+
|| cardPrNumber(card) !== undefined || card.refSource === 'manual') continue;
|
|
25
|
+
scanned++;
|
|
26
|
+
const dir = remoteLedgerMirrorDir(root, card.parentHost.host);
|
|
27
|
+
try {
|
|
28
|
+
const snapshot = await readCardRunSnapshot(card, {
|
|
29
|
+
loadLedger: (id) => loadRunLedger(id, dir),
|
|
30
|
+
podFinishes: () => [],
|
|
31
|
+
});
|
|
32
|
+
const parent = snapshot.host;
|
|
33
|
+
const evidence = bindCardEvidence(card, snapshot);
|
|
34
|
+
const childRunId = evidence.childRunId;
|
|
35
|
+
const pr = evidence.pr;
|
|
36
|
+
const child = childRunId ? snapshot.children[childRunId] : null;
|
|
37
|
+
// A PR event alone is not a completed remote run. The rejudge candidate must have the same exact
|
|
38
|
+
// child PR and terminal status that remoteBoundRunTerminal will later require.
|
|
39
|
+
if (snapshot.readErrors?.length || !parent || !childRunId || !pr?.url || !child
|
|
40
|
+
|| !podChildRunIds(parent, card.runId).includes(childRunId)
|
|
41
|
+
|| !child.some((entry) => entry.event === 'pr-opened' && entry.data?.number === pr.number
|
|
42
|
+
&& entry.data?.url === pr.url)
|
|
43
|
+
|| !child.some((entry) => entry.event === 'run-status'
|
|
44
|
+
&& ['completed', 'failed', 'cancelled'].includes(String(entry.data?.runStatus)))) { skipped++; continue; }
|
|
45
|
+
const result = await refreshCardRunBinding(card.id, statePath, {
|
|
46
|
+
// Reuse the verified snapshot so a second mirror read cannot silently change the selected PR.
|
|
47
|
+
loadLedger: (id) => id === card.runId ? [...parent] : snapshot.children[id] ? [...snapshot.children[id]!] : null,
|
|
48
|
+
// The remote mirror is the sole evidence for this prepass. Host logs can contain unrelated child receipts.
|
|
49
|
+
podFinishes: () => [],
|
|
50
|
+
});
|
|
51
|
+
if (result.card?.runId === card.runId && cardPrNumber(result.card) !== undefined
|
|
52
|
+
&& result.card.runChildId && podChildRunIds(parent, card.runId).includes(result.card.runChildId)) bound++;
|
|
53
|
+
else skipped++;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
skipped++;
|
|
56
|
+
try { debug.log('task-agent', 'remote-rejudge-bind-failed', {
|
|
57
|
+
card: card.id, runId: card.runId, host: card.parentHost.host, reason: error instanceof Error ? error.message : String(error),
|
|
58
|
+
}); } catch { /* observation must not stop the other cards */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { scanned, bound, skipped };
|
|
62
|
+
}
|