elanous 0.2.31 → 0.2.32
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/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
- package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.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 +17 -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/graphs/implement-loop.yaml +5 -0
- package/package.json +1 -1
- package/scripts/ci-typecheck-changed.ts +26 -1
- package/scripts/measure-si-task-cpu.ts +143 -0
- package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
- package/scripts/release-loop/prepare-node.ts +102 -4
- package/src/acp/core-turn-bridge.ts +16 -4
- package/src/acp/elanous-extensions.ts +3 -0
- package/src/acp/server.ts +3 -0
- package/src/acp/tool-trace.ts +186 -11
- package/src/agent/self-ambient-cache.ts +156 -0
- package/src/agent/self-ambient.ts +8 -1
- package/src/agent/task-notification.ts +5 -0
- package/src/agent-substrate/pr-reviewer.ts +3 -1
- package/src/ask-user-question/tool.ts +22 -0
- package/src/autopilot/build/llm-conflict-merge.ts +56 -10
- package/src/autopilot/pr-manager.ts +23 -5
- package/src/capture/frame-state-detect.ts +39 -0
- package/src/capture/tui-self-report.ts +134 -6
- package/src/chat/index.ts +61 -15
- package/src/chat/pickers/modal-runtime.ts +6 -0
- package/src/chat/pickers/modals.ts +8 -2
- package/src/cli/ask-bare.ts +1 -0
- package/src/cli/import-cli.ts +12 -0
- package/src/cli/log-event-names.ts +10 -0
- package/src/cli/msg-cli.ts +1 -0
- package/src/cli/pod-cli.ts +14 -3
- package/src/cli/pr-cli.ts +67 -32
- package/src/cli/pr-land-base-read.ts +34 -0
- package/src/cli/pr-land-heavy-remote.ts +81 -25
- package/src/cli/pr-land-overlap-evidence.ts +46 -2
- package/src/cli/pr-land-pod-shadow-job.ts +26 -3
- package/src/cli/pr-land-refusal.ts +20 -5
- package/src/cli/pty-takeover-cli.ts +88 -14
- package/src/cli/sim-cli.ts +18 -0
- package/src/context-bus/ops-events.ts +6 -2
- package/src/core-turn/run-core-turn.ts +1 -0
- package/src/core-turn/run-goal-loop.ts +21 -0
- package/src/core-turn/types.ts +3 -0
- package/src/dashboard/acp-active-model.ts +42 -1
- package/src/dashboard/background-jobs-status.ts +66 -0
- package/src/dashboard/index.ts +117 -44
- package/src/dashboard/input/agent-wake.ts +242 -0
- package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
- package/src/dashboard/input/external-input-host.ts +23 -9
- package/src/dashboard/input/mouse-wiring.ts +108 -11
- package/src/dashboard/log-click-runtime.ts +118 -2
- package/src/dashboard/turn-preamble.ts +116 -40
- package/src/dashboard/turn-session-layers.ts +4 -2
- package/src/dashboard/turn-start-segments.ts +222 -0
- package/src/display/prompt-row-owner.ts +146 -0
- package/src/display/resize-listener.ts +33 -2
- package/src/fold-stack.ts +4 -2
- package/src/git-fs/index.ts +263 -36
- package/src/harness/gate-reserve.ts +87 -0
- package/src/harness/harness-incidents.ts +13 -0
- package/src/harness/harness-queue.ts +15 -0
- package/src/index.ts +8 -6
- package/src/input/external-input-queue.ts +34 -9
- package/src/llm/route-decision.ts +36 -0
- package/src/llm/tool-loop-steered-rejection.ts +8 -0
- package/src/llm-request-shape.ts +209 -0
- package/src/llm.ts +166 -267
- package/src/loops/orchestrator/tick.ts +24 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/mss/logging/render-categories.ts +5 -0
- package/src/ops/overview.ts +59 -2
- package/src/pr-tool/census.ts +19 -4
- package/src/pr-tool/inflight-registry.ts +143 -0
- package/src/pty-shell/pty-manifest.ts +12 -3
- package/src/pty-shell/tui-run-state.ts +34 -3
- package/src/seat-loop/traffic-signal.ts +2 -1
- package/src/self-dev/ask-launch-flow.ts +13 -0
- package/src/self-dev/draft-residue.ts +243 -24
- package/src/self-dev/shard-plan.ts +11 -3
- package/src/self-dev/shard-run.ts +103 -10
- package/src/self-implement/elanous-tui-spawn.ts +14 -0
- package/src/self-implement/follow-up-placement.ts +79 -0
- package/src/self-implement/gate-artifacts.ts +15 -4
- package/src/self-implement/gate-baseline.ts +96 -5
- package/src/self-implement/gate-cli.ts +7 -2
- package/src/self-implement/gate-remote.ts +26 -1
- package/src/self-implement/host-regate.ts +36 -4
- package/src/self-implement/orchestrator.ts +101 -11
- package/src/self-implement/run-ledger.ts +16 -14
- package/src/session-runtime/index.ts +22 -267
- package/src/shell-runner/background-surface.ts +15 -5
- package/src/skills/tools/agent.ts +39 -6
- package/src/skills/tools/list-dir.ts +11 -0
- package/src/skills/tools/schedule-wakeup.ts +408 -0
- package/src/skills/tools/shell.ts +245 -3
- package/src/status/bar.ts +26 -0
- package/src/steward/digest-delivery.ts +118 -0
- package/src/steward/hold-hygiene.ts +120 -0
- package/src/steward/triage.ts +10 -0
- package/src/surface/shell-runner-boot.ts +5 -2
- package/src/task-agent/task-hand.ts +24 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
- package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
- package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
- package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
- package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
- package/src/tui/chat/execution-badge.ts +5 -1
- package/src/tui/input-lag-probe.ts +230 -0
- package/src/tui.ts +4 -0
- package/src/ux-sim/scenario.ts +255 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
- package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
- /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
package/src/cli/pr-cli.ts
CHANGED
|
@@ -58,13 +58,16 @@ import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
|
|
|
58
58
|
import { parseReleaseNoteSection } from '../release-loop/release-note.js';
|
|
59
59
|
import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
|
|
60
60
|
import { admitLandingMerge } from '../self-implement/frozen-merges.js';
|
|
61
|
-
import { verifyAllSameHeadReviews, verifyAncestorAutoPass, verifyOverlapEvidence } from './pr-land-overlap-evidence.js';
|
|
61
|
+
import { verifyAllSameHeadReviews, verifyAncestorAutoPass, verifyOverlapEvidence, verifyOverlapEvidenceVirtualRebase } from './pr-land-overlap-evidence.js';
|
|
62
|
+
import type { VerifyOverlapEvidenceInput } from './pr-land-overlap-evidence.js';
|
|
62
63
|
import { verifyVirtualRebasePatch } from './pr-land-virtual-rebase.js';
|
|
63
|
-
import { observePrLandRefusal } from './pr-land-refusal.js';
|
|
64
|
+
import { observeDirectPrLandRefusal, observePrLandRefusal } from './pr-land-refusal.js';
|
|
64
65
|
import { ensurePrLandHeavyRemote, type HeavyRemoteInput, type HeavyRemoteResult } from './pr-land-heavy-remote.js';
|
|
66
|
+
import { readPrBaseNameWithRetry } from './pr-land-base-read.js';
|
|
65
67
|
import { runHeavyOnPodShadow } from './pr-land-pod-shadow.js';
|
|
66
68
|
import type { LandingPodShadowInput, LandingPodShadowVerdict } from './pr-land-pod-shadow-job.js';
|
|
67
|
-
import { taskAgentStatePath } from '../task-agent/task-hand.js';
|
|
69
|
+
import { readTaskAgentState, taskAgentStatePath, type TaskCard } from '../task-agent/task-hand.js';
|
|
70
|
+
import { INFLIGHT_REGISTRY_FILE, removeInflightForPr } from '../pr-tool/inflight-registry.js';
|
|
68
71
|
import { PR_LABEL_NAMES } from '../github/pr-labels.js';
|
|
69
72
|
import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
|
|
70
73
|
import { censusOpenPrs, guardNewPr, parseDiffHunks, readOpenPrs, type Seat } from '../pr-tool/census.js';
|
|
@@ -122,6 +125,8 @@ export interface PrLandDeps {
|
|
|
122
125
|
readFile?: (path: string) => string;
|
|
123
126
|
/** 증거 파일 없는 already-ancestor 자동 판정이 리뷰 결과를 찾는 자리(기본 `<TA 상태 디렉터리>/review-results`). */
|
|
124
127
|
reviewResultsDir?: string;
|
|
128
|
+
/** INFLIGHT-REGISTRY — 착지 뒤 등록부 줄을 찾을 과제 카드 상태(기본 `taskAgentStatePath()` · 등록부는 그 옆). */
|
|
129
|
+
taskStatePath?: string;
|
|
125
130
|
listUnfinishedRuns?: () => FederatedUnfinishedRunLedgerQuery | readonly FederatedUnfinishedRunLedgerEntry[];
|
|
126
131
|
queryRunningRuns?: () => RunningRunsResult;
|
|
127
132
|
listOpenPrs?: () => readonly { number: number; headRefName: string }[] | null;
|
|
@@ -392,6 +397,7 @@ export function exportLeakBlock(
|
|
|
392
397
|
allow: boolean,
|
|
393
398
|
out: { log: (message: string) => void; error: (message: string) => void },
|
|
394
399
|
cwd: string = process.cwd(),
|
|
400
|
+
onRefusal?: (message: string) => void,
|
|
395
401
|
): boolean {
|
|
396
402
|
if (!changedFiles || changedFiles.length === 0) return true;
|
|
397
403
|
let result: ExportLeakCheck;
|
|
@@ -425,7 +431,9 @@ export function exportLeakBlock(
|
|
|
425
431
|
}
|
|
426
432
|
if (blockedHits.length === 0) { out.log('✓ public-export-leak: 바뀐 파일이 공개본에 사적 흔적을 싣지 않는다(prepare 와 같은 변환 뒤).'); return true; }
|
|
427
433
|
const write = allow ? out.log : out.error;
|
|
428
|
-
|
|
434
|
+
const refusal = `${allow ? '⚠' : '✗'} public-export-leak: 바뀐 파일 ${blockedHits.length}곳이 공개본에 사적 흔적을 싣는다 — 다음 release prepare 가 여기서 실패한다:`;
|
|
435
|
+
write(refusal);
|
|
436
|
+
if (!allow) onRefusal?.(refusal);
|
|
429
437
|
for (const hit of blockedHits.slice(0, 20)) write(` ${hit.file}:${hit.line} · ${hit.marker}${missingReason.includes(hit) ? ' · 사유 없음' : ''}`);
|
|
430
438
|
if (blockedHits.length > 20) write(` … 외 ${blockedHits.length - 20}곳`);
|
|
431
439
|
write(' 확인: bun scripts/public-export.ts --leak-check --all --files <파일…>');
|
|
@@ -1060,7 +1068,7 @@ function emitLandingOverlapAdvisory(
|
|
|
1060
1068
|
}
|
|
1061
1069
|
|
|
1062
1070
|
/** The pinned overlap evidence decision is shared by landing and its read-only preview. */
|
|
1063
|
-
function verifyPinnedOverlapDecision(input: {
|
|
1071
|
+
export function verifyPinnedOverlapDecision(input: {
|
|
1064
1072
|
opts: PrLandOpts;
|
|
1065
1073
|
deps: PrLandDeps;
|
|
1066
1074
|
run: CmdRunner;
|
|
@@ -1078,13 +1086,14 @@ function verifyPinnedOverlapDecision(input: {
|
|
|
1078
1086
|
let ancestorResyncNeeded = false;
|
|
1079
1087
|
if (decision === 'unavailable' && opts.overlapEvidence !== undefined) {
|
|
1080
1088
|
const ref = resolveRemoteBranchRef(run, cwd, base);
|
|
1081
|
-
const
|
|
1089
|
+
const evidenceInput: VerifyOverlapEvidenceInput = {
|
|
1082
1090
|
evidencePath: opts.overlapEvidence, pr: Number(opts.pr), expectedHead: opts.expectedHead!,
|
|
1083
1091
|
remote: ref.remote, branch: ref.branch, cwd, run, readFile,
|
|
1084
1092
|
reviewResultsDir: deps.reviewResultsDir ?? join(dirname(taskAgentStatePath()), 'review-results'),
|
|
1085
1093
|
listDir: (dir) => readdirSync(dir), reviewMtime: (path) => statSync(path).mtimeMs,
|
|
1086
1094
|
reviewRealPath: (path) => realpathSync(path), overlappingLandings,
|
|
1087
|
-
}
|
|
1095
|
+
};
|
|
1096
|
+
const verdict = verifyOverlapEvidence(evidenceInput);
|
|
1088
1097
|
accepted = verdict.ok;
|
|
1089
1098
|
if (verdict.ok) {
|
|
1090
1099
|
out.log(verdict.mode === 'already-ancestor'
|
|
@@ -1092,7 +1101,24 @@ function verifyPinnedOverlapDecision(input: {
|
|
|
1092
1101
|
: verdict.mode === 'base-advanced-disjoint'
|
|
1093
1102
|
? `✓ overlap-evidence: base-advanced-disjoint — gate base ${verdict.gateBase.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} 까지 ${verdict.windowCommits}개 커밋이 PR 파일을 안 건드린다 · review pass|warn must-fix 0 (${verdict.reviewResult})`
|
|
1094
1103
|
: `✓ overlap-evidence: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · gate passed on that base · review pass must-fix 0 (${verdict.reviewResult})`);
|
|
1095
|
-
} else
|
|
1104
|
+
} else {
|
|
1105
|
+
const pureNotRebased = verdict.baseTip !== undefined && verdict.reason ===
|
|
1106
|
+
`not-rebased: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} is not an ancestor of ${opts.expectedHead!.slice(0, 12)}`;
|
|
1107
|
+
const measuredOverlap = verdict.baseTip !== undefined && verdict.reason.startsWith('base-stale-overlap:');
|
|
1108
|
+
if (pureNotRebased || measuredOverlap) {
|
|
1109
|
+
const patch = verifyOverlapEvidenceVirtualRebase(evidenceInput, verdict.baseTip!);
|
|
1110
|
+
accepted = patch.ok;
|
|
1111
|
+
record('virtual-rebase', patch.ok, {
|
|
1112
|
+
pr: Number(opts.pr), head: opts.expectedHead, baseTip: verdict.baseTip,
|
|
1113
|
+
reason: patch.ok ? 'same-patch-id' : patch.reason,
|
|
1114
|
+
reviewResult: patch.ok ? patch.reviewResult : null,
|
|
1115
|
+
evidencePath: opts.overlapEvidence,
|
|
1116
|
+
});
|
|
1117
|
+
if (patch.ok) {
|
|
1118
|
+
out.log(`✓ overlap-evidence: virtual-rebase — ${ref.remote}/${ref.branch} ${verdict.baseTip!.slice(0, 12)} 와 head ${opts.expectedHead!.slice(0, 12)} 충돌 없음 · patch-id ${patch.patchId.slice(0, 12)} 동일 · review pass|warn must-fix 0 (${patch.reviewResult})`);
|
|
1119
|
+
} else refuse(`✗ overlap-evidence: ${verdict.reason} · virtual-rebase: ${patch.reason}`);
|
|
1120
|
+
} else refuse(`✗ overlap-evidence: ${verdict.reason}`);
|
|
1121
|
+
}
|
|
1096
1122
|
} else if (decision === 'unavailable') {
|
|
1097
1123
|
const verdict = verifyAncestorAutoPass({
|
|
1098
1124
|
pr: Number(opts.pr), expectedHead: opts.expectedHead!, cwd, run, readFile, overlappingLandings,
|
|
@@ -1121,12 +1147,12 @@ function verifyPinnedOverlapDecision(input: {
|
|
|
1121
1147
|
ancestorResyncNeeded = !accepted;
|
|
1122
1148
|
record('virtual-rebase', patch.ok, {
|
|
1123
1149
|
pr: Number(opts.pr), head: opts.expectedHead, baseTip: baseTip || null,
|
|
1124
|
-
reason: patch.ok ? 'same-patch-id' : patch.reason,
|
|
1150
|
+
reason: patch.ok ? 'same-patch-id' : ('reason' in patch ? patch.reason : 'virtual-rebase-unmeasured'),
|
|
1125
1151
|
reviewResult: reviews.ok ? reviews.reviewResult : null,
|
|
1126
1152
|
});
|
|
1127
|
-
if (patch.ok) {
|
|
1153
|
+
if (patch.ok && reviews.ok) {
|
|
1128
1154
|
out.log(`✓ overlap-evidence: virtual-rebase — ${ref.remote}/${ref.branch} ${baseTip.slice(0, 12)} 와 head ${opts.expectedHead!.slice(0, 12)} 충돌 없음 · patch-id ${patch.patchId.slice(0, 12)} 동일 · review pass|warn must-fix 0 (${reviews.reviewResult})`);
|
|
1129
|
-
} else refuse(`✗ virtual-rebase: ${patch.reason} · ${verdict.reason}`);
|
|
1155
|
+
} else refuse(`✗ virtual-rebase: ${'reason' in patch ? patch.reason : 'virtual-rebase-unmeasured'} · ${verdict.reason}`);
|
|
1130
1156
|
} else refuse(`✗ ancestor-auto: ${verdict.reason}`);
|
|
1131
1157
|
}
|
|
1132
1158
|
}
|
|
@@ -1287,12 +1313,16 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1287
1313
|
totalElapsedMs: Math.max(0, Date.now() - landStartedAt), ok, ...extra,
|
|
1288
1314
|
});
|
|
1289
1315
|
};
|
|
1290
|
-
//
|
|
1291
|
-
// Option syntax and prerequisite
|
|
1316
|
+
// Observe actual merge refusals, including the measured remote gate and public export leak.
|
|
1317
|
+
// Option syntax and unmeasured prerequisite errors do not describe a recovery edge.
|
|
1292
1318
|
const refuse = (message: string, context: { isDraft?: boolean } = {}): void => {
|
|
1293
1319
|
out.error(message);
|
|
1294
1320
|
observePrLandRefusal(message, context);
|
|
1295
1321
|
};
|
|
1322
|
+
const refuseDirect = (message: string): void => {
|
|
1323
|
+
out.error(message);
|
|
1324
|
+
observeDirectPrLandRefusal(message);
|
|
1325
|
+
};
|
|
1296
1326
|
if (opts.title !== undefined && opts.titleFile !== undefined) {
|
|
1297
1327
|
out.error('✗ title: --title과 --title-file은 함께 사용할 수 없습니다.');
|
|
1298
1328
|
return 1;
|
|
@@ -1829,7 +1859,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1829
1859
|
if (opts.heavyRemote) {
|
|
1830
1860
|
const main = overlapCheckedBase!;
|
|
1831
1861
|
const verdict = heavyGate(main, 'initial');
|
|
1832
|
-
if (!verdict?.ok) {
|
|
1862
|
+
if (!verdict?.ok) { refuseDirect(`✗ heavy-remote: 원격 게이트 ${verdict?.reason ?? 'unmeasured'} — 로컬 게이트로 폴백하지 않는다.`); return 1; }
|
|
1833
1863
|
verifiedHeavyBase = main;
|
|
1834
1864
|
out.log(`✓ heavy-remote: host=${verdict.host ?? 'unknown'} head=${opts.expectedHead!.slice(0, 12)} main=${main.slice(0, 12)} source=${verdict.source} elapsed=${verdict.elapsedMs}ms`);
|
|
1835
1865
|
out.log(`✓ typecheck-roots: ${JSON.stringify(verdict.rootPlan ?? { mode: 'not-applicable', roots: null, promoted: false, reason: 'no-root-plan-in-gate-output' })}`);
|
|
@@ -1850,22 +1880,19 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1850
1880
|
// A move during the wait is the same race as a move during the gate: rerun or fail closed.
|
|
1851
1881
|
// gh merge pins the head only. A retargeted PR would land on a branch the heavy gate never measured.
|
|
1852
1882
|
// Unreadable baseRefName is not "main". Normal pr land (no --heavy-remote) does not use this check.
|
|
1853
|
-
const confirmHeavyPrBase = (): number | undefined => {
|
|
1883
|
+
const confirmHeavyPrBase = async (): Promise<number | undefined> => {
|
|
1854
1884
|
if (!opts.heavyRemote) return undefined;
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
if (typeof name === 'string') baseName = name.trim();
|
|
1864
|
-
}
|
|
1865
|
-
} catch { /* unreadable is not main */ }
|
|
1885
|
+
const baseRead = await readPrBaseNameWithRetry({
|
|
1886
|
+
pr: String(opts.pr), cwd, run,
|
|
1887
|
+
sleep: deps.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms))),
|
|
1888
|
+
});
|
|
1889
|
+
const baseName = baseRead.baseRefName;
|
|
1890
|
+
record('heavy-remote-pr-base-read', baseName !== null, {
|
|
1891
|
+
pr: Number(opts.pr), baseRefName: baseName, attempts: baseRead.attempts,
|
|
1892
|
+
});
|
|
1866
1893
|
if (baseName !== 'main') {
|
|
1867
|
-
record('heavy-remote-pr-base', false, { pr: Number(opts.pr), baseRefName: baseName
|
|
1868
|
-
|
|
1894
|
+
record('heavy-remote-pr-base', false, { pr: Number(opts.pr), baseRefName: baseName, attempts: baseRead.attempts });
|
|
1895
|
+
refuseDirect(`✗ heavy-remote: PR base=${baseName ?? 'unreadable'} ≠ main — 재타기팅된 PR은 통과한 head/main 쌍이 아니다. 병합하지 않는다.`);
|
|
1869
1896
|
return 1;
|
|
1870
1897
|
}
|
|
1871
1898
|
return undefined;
|
|
@@ -1885,7 +1912,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1885
1912
|
};
|
|
1886
1913
|
const confirmHeavyBaseStable = async (): Promise<number | undefined> => {
|
|
1887
1914
|
if (!opts.heavyRemote) return undefined;
|
|
1888
|
-
const retargeted = confirmHeavyPrBase();
|
|
1915
|
+
const retargeted = await confirmHeavyPrBase();
|
|
1889
1916
|
if (retargeted !== undefined) return retargeted;
|
|
1890
1917
|
let stable = false;
|
|
1891
1918
|
for (let attempt = 0; attempt <= HEAVY_BASE_STABILITY_RETRIES; attempt += 1) {
|
|
@@ -1899,7 +1926,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1899
1926
|
if (staleOverlap !== undefined) return staleOverlap;
|
|
1900
1927
|
const verdict = heavyGate(main, 'late-recheck');
|
|
1901
1928
|
if (!verdict?.ok) {
|
|
1902
|
-
|
|
1929
|
+
refuseDirect(`✗ heavy-remote: 병합 직전 원격 게이트 ${verdict?.reason ?? 'unmeasured'} — 로컬 게이트로 폴백하지 않는다.`);
|
|
1903
1930
|
return 1;
|
|
1904
1931
|
}
|
|
1905
1932
|
verifiedHeavyBase = main;
|
|
@@ -1911,7 +1938,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1911
1938
|
}
|
|
1912
1939
|
// A late regate can take minutes. Re-read baseRefName after that wait and before success:
|
|
1913
1940
|
// the pre-loop check is stale if the PR was retargeted while origin/main was being remeasured.
|
|
1914
|
-
const retargetedAfter = confirmHeavyPrBase();
|
|
1941
|
+
const retargetedAfter = await confirmHeavyPrBase();
|
|
1915
1942
|
if (retargetedAfter !== undefined) return retargetedAfter;
|
|
1916
1943
|
return undefined;
|
|
1917
1944
|
};
|
|
@@ -1990,7 +2017,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1990
2017
|
nextMdWarning(body ?? '', changedPaths, out);
|
|
1991
2018
|
if (!conflictMarkerBlock(changedPaths, cwd, out)) return 1;
|
|
1992
2019
|
publicLeakWarning(changedPaths, deps.runPublicLeakGate ?? ((files, sink) => runPrLandPublicLeakGate(files, sink, cwd, base)), out);
|
|
1993
|
-
if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out, cwd)) return 1;
|
|
2020
|
+
if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out, cwd, observeDirectPrLandRefusal)) return 1;
|
|
1994
2021
|
if (!await exportImportBlock(changedPaths, () => (deps.runExportImportCheck ?? runPrLandExportImportCheck)(changedPaths ?? [], cwd), out)) return 1;
|
|
1995
2022
|
if (publicDocPaths(changedPaths, () => true).length > 0) {
|
|
1996
2023
|
const top = run('git', ['rev-parse', '--show-toplevel'], { cwd });
|
|
@@ -2347,6 +2374,14 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
2347
2374
|
}
|
|
2348
2375
|
if (opts.forceFreeze) debug.log('release-loop.freeze', 'force-merge', { pr: prNumber, reason: opts.forceFreeze.trim() });
|
|
2349
2376
|
record('merge', true, { result: merged.kind, ...(merged.remoteBranchDeletion ? { remoteBranchDeletion: merged.remoteBranchDeletion.detail } : {}) });
|
|
2377
|
+
// INFLIGHT-REGISTRY — 착지하면 비행 중 등록부의 그 런 줄을 지운다(색인 실패는 착지를 막지 않는다).
|
|
2378
|
+
try {
|
|
2379
|
+
const statePath = deps.taskStatePath ?? taskAgentStatePath();
|
|
2380
|
+
const cards = Object.values(readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {});
|
|
2381
|
+
removeInflightForPr(prNumber, cards, { path: join(dirname(statePath), INFLIGHT_REGISTRY_FILE) });
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
try { debug.log('pr-tool.inflight', 'remove-failed', { pr: prNumber, error: String((error as Error)?.message ?? error).slice(0, 200) }, { level: 'warn' }); } catch { /* fail-soft */ }
|
|
2384
|
+
}
|
|
2350
2385
|
if (merged.remoteBranchDeletion) {
|
|
2351
2386
|
out.log(`⚠ remote branch deletion: ${merged.remoteBranchDeletion.detail}`);
|
|
2352
2387
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CmdRunner } from '../autopilot/pr-manager.js';
|
|
2
|
+
|
|
3
|
+
export const PR_LAND_BASE_READ_RETRIES = 2;
|
|
4
|
+
export const PR_LAND_BASE_READ_RETRY_DELAY_MS = 100;
|
|
5
|
+
|
|
6
|
+
export interface PrBaseReadResult {
|
|
7
|
+
baseRefName: string | null;
|
|
8
|
+
attempts: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Retry only an unreadable GitHub base response; a readable non-main base is a real retarget. */
|
|
12
|
+
export async function readPrBaseNameWithRetry(input: {
|
|
13
|
+
pr: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
run: CmdRunner;
|
|
16
|
+
sleep: (ms: number) => Promise<void>;
|
|
17
|
+
}): Promise<PrBaseReadResult> {
|
|
18
|
+
for (let attempt = 1; attempt <= PR_LAND_BASE_READ_RETRIES + 1; attempt += 1) {
|
|
19
|
+
const viewed = input.run('gh', ['pr', 'view', input.pr, '--json', 'baseRefName,url'], { cwd: input.cwd });
|
|
20
|
+
let baseRefName: string | null = null;
|
|
21
|
+
try {
|
|
22
|
+
if (viewed.ok) {
|
|
23
|
+
const parsed: unknown = JSON.parse(viewed.out);
|
|
24
|
+
const name = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
25
|
+
? (parsed as { baseRefName?: unknown }).baseRefName : undefined;
|
|
26
|
+
if (typeof name === 'string' && name.trim()) baseRefName = name.trim();
|
|
27
|
+
}
|
|
28
|
+
} catch { /* Retry unreadable responses; do not treat them as main. */ }
|
|
29
|
+
|
|
30
|
+
if (baseRefName !== null) return { baseRefName, attempts: attempt };
|
|
31
|
+
if (attempt <= PR_LAND_BASE_READ_RETRIES) await input.sleep(PR_LAND_BASE_READ_RETRY_DELAY_MS);
|
|
32
|
+
}
|
|
33
|
+
return { baseRefName: null, attempts: PR_LAND_BASE_READ_RETRIES + 1 };
|
|
34
|
+
}
|
|
@@ -51,6 +51,8 @@ export interface HeavyRemoteResult {
|
|
|
51
51
|
reason?: string;
|
|
52
52
|
elapsedMs: number;
|
|
53
53
|
evidencePath: string;
|
|
54
|
+
/** Redacted stdout/stderr from a failed remote gate; never a reusable pass. */
|
|
55
|
+
failureLogPath?: string;
|
|
54
56
|
rootPlan?: RootPlanSummary | null;
|
|
55
57
|
interferenceMeasured?: boolean;
|
|
56
58
|
}
|
|
@@ -60,6 +62,8 @@ export interface HeavyRemoteDeps {
|
|
|
60
62
|
runRemote?: (input: Parameters<typeof runOnRemote>[0], runner: GateRemoteRunner) => RemoteRunOutcome;
|
|
61
63
|
settings?: ReturnType<typeof resolveGateRemoteSettings>;
|
|
62
64
|
now?: () => number;
|
|
65
|
+
/** Test seam — same cadence as self gate (`dispatchHeavyCheck`): sleep at most 15s while a host stays busy. */
|
|
66
|
+
sleep?: (ms: number) => void;
|
|
63
67
|
observe?: (event: string, data: Record<string, unknown>) => void;
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -157,6 +161,38 @@ function failedGateSummary(stdout: Buffer, stderr: Buffer): string | undefined {
|
|
|
157
161
|
return bounded.length ? bounded.join(' | ') : undefined;
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
/** The self-gate baseline prints one failure per line after its count header. Keep only distinct test paths here. */
|
|
165
|
+
function failedGateFiles(stdout: Buffer, stderr: Buffer): string | undefined {
|
|
166
|
+
const files = { introduced: new Set<string>(), preexisting: new Set<string>() };
|
|
167
|
+
for (const line of `${stdout.toString('utf8')}\n${stderr.toString('utf8')}`.split(/\r?\n/)) {
|
|
168
|
+
const match = /^- (introduced|preexisting):\s+(\S+\.(?:test|spec)\.[cm]?[jt]sx?)(?:\s+>|\s|$)/.exec(line.trim());
|
|
169
|
+
if (match) files[match[1] as keyof typeof files].add(match[2]!);
|
|
170
|
+
}
|
|
171
|
+
const parts = (['introduced', 'preexisting'] as const).filter((kind) => files[kind].size > 0)
|
|
172
|
+
.map((kind) => { const names = [...files[kind]].sort(); return `${kind}-files=${names.slice(0, 5).map((name) => redactSecretText(name).slice(0, 160)).join(',')}${names.length > 5 ? `,+${names.length - 5}` : ''}`; });
|
|
173
|
+
return parts.length ? parts.join(' · ') : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Remote scratch is deleted on exit. Preserve a bounded, redacted copy before returning a refusal. */
|
|
177
|
+
function writeFailedGateLog(path: string, stdout: Buffer, stderr: Buffer): string | undefined {
|
|
178
|
+
// One latest refusal log per PR; repeated attempts replace it atomically instead of accumulating files.
|
|
179
|
+
const logPath = `${path}.failure.log`;
|
|
180
|
+
const tempPath = `${logPath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
181
|
+
const maxBytes = 2 * 1024 * 1024;
|
|
182
|
+
const render = (label: string, bytes: Buffer): string => {
|
|
183
|
+
const safe = Buffer.from(redactSecretText(bytes.toString('utf8')), 'utf8');
|
|
184
|
+
const clipped = safe.length > maxBytes
|
|
185
|
+
? `${safe.subarray(0, maxBytes / 2).toString('utf8')}\n[remote log middle truncated]\n${safe.subarray(-maxBytes / 2).toString('utf8')}` : safe.toString('utf8');
|
|
186
|
+
return `=== ${label} (${bytes.length} bytes${safe.length > maxBytes ? ', middle truncated' : ''}) ===\n${clipped}\n`;
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
190
|
+
writeFileSync(tempPath, render('stdout', stdout) + render('stderr', stderr), { mode: 0o600, flag: 'wx' });
|
|
191
|
+
renameSync(tempPath, logPath);
|
|
192
|
+
return logPath;
|
|
193
|
+
} catch { rmSync(tempPath, { force: true }); return undefined; }
|
|
194
|
+
}
|
|
195
|
+
|
|
160
196
|
/** No local gate fallback: host refusal, an unmeasured run, and a failed gate all block landing. */
|
|
161
197
|
export function ensurePrLandHeavyRemote(input: HeavyRemoteInput, deps: HeavyRemoteDeps = {}): HeavyRemoteResult {
|
|
162
198
|
const path = evidencePath(input);
|
|
@@ -170,48 +206,66 @@ export function ensurePrLandHeavyRemote(input: HeavyRemoteInput, deps: HeavyRemo
|
|
|
170
206
|
}
|
|
171
207
|
const cached = matchingEvidence(path, input);
|
|
172
208
|
if (cached) return { ok: true, host: cached.host, source: 'cached', elapsedMs: 0, evidencePath: path, rootPlan: cached.rootPlan, interferenceMeasured: cached.interferenceMeasured };
|
|
173
|
-
const
|
|
209
|
+
const now = deps.now ?? Date.now;
|
|
210
|
+
const sleep = deps.sleep ?? ((ms: number) => { if (ms > 0) Bun.sleepSync(ms); });
|
|
211
|
+
const started = now();
|
|
212
|
+
const deadline = started + settings.slotWaitSeconds * 1000;
|
|
174
213
|
let outcome: RemoteRunOutcome = { kind: 'busy' };
|
|
175
214
|
const primaryHost: PrLandHeavyHost = settings.host;
|
|
176
215
|
let selectedHost: PrLandHeavyHost = primaryHost;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
216
|
+
const hosts = [primaryHost, ...PR_LAND_HEAVY_HOSTS.filter((candidate) => candidate !== primaryHost)];
|
|
217
|
+
wait: for (;;) {
|
|
218
|
+
for (const host of hosts) {
|
|
219
|
+
selectedHost = host;
|
|
220
|
+
try {
|
|
221
|
+
outcome = (deps.runRemote ?? runOnRemote)({
|
|
222
|
+
repo: input.repo, host, mirror: settings.mirror, commit: input.head,
|
|
223
|
+
refs: [input.main], hostCap: gateRemoteHostCap(settings, host), installPwa: true, stableCheckout: true,
|
|
224
|
+
// Test the tree GitHub will merge: scratch-checkout the reviewed head, merge the pinned main tip there,
|
|
225
|
+
// then run typecheck and self gate (changed tests and PWA reach/build) at that exact pair.
|
|
226
|
+
// The detached integration commit stays on the selected host and never changes the PR branch.
|
|
227
|
+
argv: (sha) => ['sh', '-c',
|
|
187
228
|
'git -c user.name=elanous-gate -c user.email=gate@localhost -c commit.gpgsign=false merge --no-ff --no-edit "$1" && TSC_BASE_REF="$1" bun run scripts/ci-typecheck-changed.ts --local && bun bin/elanous.mjs self gate --changed --base "$1" --local --shards 4 && bun scripts/pr-land-extra-remote-gates.ts "$1"',
|
|
188
229
|
'sh', sha(input.main)],
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
230
|
+
}, deps.runner ?? realGateRemoteRunner);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
outcome = { kind: 'infra', reason: `exception: ${error instanceof Error ? error.message : String(error)}` };
|
|
233
|
+
}
|
|
234
|
+
// A completed run or infrastructure failure is a verdict. Only a full host slot asks for another host.
|
|
235
|
+
if (outcome.kind !== 'busy') break wait;
|
|
192
236
|
}
|
|
193
|
-
//
|
|
194
|
-
|
|
237
|
+
// Both allowlisted hosts were busy. Wait the same slotWaitSeconds window as self gate, then retry.
|
|
238
|
+
debug.log('pr.land', 'host-busy', { host: selectedHost, occupied: outcome.occupied ?? null, slotWaitSeconds: settings.slotWaitSeconds });
|
|
239
|
+
const left = deadline - now();
|
|
240
|
+
if (left <= 0) break;
|
|
241
|
+
sleep(Math.min(15_000, left));
|
|
242
|
+
if (now() >= deadline) break;
|
|
195
243
|
}
|
|
196
|
-
const elapsedMs = Math.max(0,
|
|
244
|
+
const elapsedMs = Math.max(0, now() - started);
|
|
197
245
|
const observe = deps.observe ?? ((event: string, data: Record<string, unknown>) => debug.log('pr.land', event, data));
|
|
198
246
|
const actualHost = outcome.kind === 'ran' ? outcome.host : undefined;
|
|
199
247
|
const base = { pr: input.pr, head: input.head, main: input.main, host: actualHost ?? null, attemptedHost: selectedHost, elapsedMs };
|
|
200
248
|
// The outcome must identify the selected allowlisted host; the configured host alone is not evidence.
|
|
201
249
|
if (outcome.kind !== 'ran' || outcome.commit !== input.head || outcome.rc !== 0 || outcome.host !== selectedHost) {
|
|
202
250
|
const summary = outcome.kind === 'ran' && outcome.rc !== 0 ? failedGateSummary(outcome.stdout, outcome.stderr) : undefined;
|
|
251
|
+
// A different host cannot attest to this gate, so do not promote its file list into the refusal.
|
|
252
|
+
const files = outcome.kind === 'ran' && outcome.rc !== 0 && outcome.host === selectedHost && outcome.commit === input.head
|
|
253
|
+
? failedGateFiles(outcome.stdout, outcome.stderr) : undefined;
|
|
254
|
+
const failureLogPath = outcome.kind === 'ran' ? writeFailedGateLog(path, outcome.stdout, outcome.stderr) : undefined;
|
|
203
255
|
const reason = outcome.kind === 'ran'
|
|
204
256
|
? outcome.host !== selectedHost
|
|
205
|
-
? `gate-host-mismatch (${outcome.host})`
|
|
206
|
-
: `gate-rc-${outcome.rc}${outcome.commit !== input.head ? '-head-mismatch' : ''}${summary ? ` · remote: ${summary}` : ''}`
|
|
257
|
+
? `gate-host-mismatch (${outcome.host})${failureLogPath ? ` · remote-log: ${failureLogPath}` : ''}`
|
|
258
|
+
: `gate-rc-${outcome.rc}${outcome.commit !== input.head ? '-head-mismatch' : ''}${files ? ` · ${files}` : ''}${summary ? ` · remote: ${summary}` : ''}${failureLogPath ? ` · remote-log: ${failureLogPath}` : ' · remote-log: unavailable'}`
|
|
207
259
|
: outcome.kind === 'busy' ? 'gate-host-busy' : outcome.reason;
|
|
208
|
-
observe('heavy-remote', { ...base, outcome: outcome.kind === 'ran' ? 'fail' : 'unmeasured', reason });
|
|
209
|
-
return { ok: false, ...(actualHost ? { host: actualHost } : {}), attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path };
|
|
260
|
+
observe('heavy-remote', { ...base, outcome: outcome.kind === 'ran' ? 'fail' : 'unmeasured', reason, failureLogPath: failureLogPath ?? null });
|
|
261
|
+
return { ok: false, ...(actualHost ? { host: actualHost } : {}), attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path, ...(failureLogPath ? { failureLogPath } : {}) };
|
|
210
262
|
}
|
|
211
263
|
const extraGates = extraGatesFromOutput(outcome.stdout);
|
|
212
264
|
if (!extraGates) {
|
|
213
|
-
|
|
214
|
-
|
|
265
|
+
const failureLogPath = writeFailedGateLog(path, outcome.stdout, outcome.stderr);
|
|
266
|
+
const reason = `extra-global-unmeasured${failureLogPath ? ` · remote-log: ${failureLogPath}` : ' · remote-log: unavailable'}`;
|
|
267
|
+
observe('heavy-remote', { ...base, outcome: 'unmeasured', reason, failureLogPath: failureLogPath ?? null });
|
|
268
|
+
return { ok: false, host: selectedHost, attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path, ...(failureLogPath ? { failureLogPath } : {}) };
|
|
215
269
|
}
|
|
216
270
|
const evidence: HeavyRemoteEvidence = {
|
|
217
271
|
schema: 2, pr: input.pr, head: input.head, main: input.main, host: selectedHost,
|
|
@@ -225,8 +279,10 @@ export function ensurePrLandHeavyRemote(input: HeavyRemoteInput, deps: HeavyRemo
|
|
|
225
279
|
renameSync(temp, path);
|
|
226
280
|
} catch (error) {
|
|
227
281
|
rmSync(temp, { force: true });
|
|
228
|
-
|
|
229
|
-
|
|
282
|
+
const failureLogPath = writeFailedGateLog(path, outcome.stdout, outcome.stderr);
|
|
283
|
+
const reason = `evidence-write-failed: ${error instanceof Error ? error.message : String(error)}${failureLogPath ? ` · remote-log: ${failureLogPath}` : ' · remote-log: unavailable'}`;
|
|
284
|
+
observe('heavy-remote', { ...base, outcome: 'unmeasured', reason, failureLogPath: failureLogPath ?? null });
|
|
285
|
+
return { ok: false, host: selectedHost, attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path, ...(failureLogPath ? { failureLogPath } : {}) };
|
|
230
286
|
}
|
|
231
287
|
observe('heavy-remote', { ...base, outcome: 'pass', evidencePath: path, rootPlan: evidence.rootPlan, interferenceMeasured: evidence.interferenceMeasured });
|
|
232
288
|
return { ok: true, host: selectedHost, source: 'remote', elapsedMs, evidencePath: path, rootPlan: evidence.rootPlan, interferenceMeasured: evidence.interferenceMeasured };
|
|
@@ -25,6 +25,7 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
25
25
|
import { debug } from '../debug/log.js';
|
|
26
26
|
import type { CmdRunner } from '../autopilot/pr-manager.js';
|
|
27
27
|
import { parseReviewResultOutput } from '../task-agent/review-result-capture.js';
|
|
28
|
+
import { verifyVirtualRebasePatch } from './pr-land-virtual-rebase.js';
|
|
28
29
|
|
|
29
30
|
const SHA40 = /^[0-9a-f]{40}$/i;
|
|
30
31
|
|
|
@@ -45,7 +46,7 @@ export type OverlapEvidenceVerdict =
|
|
|
45
46
|
| { ok: true; mode: 'already-ancestor'; reviewResult: string }
|
|
46
47
|
| { ok: true; mode: 'rebased'; baseTip: string; reviewResult: string }
|
|
47
48
|
| { ok: true; mode: 'base-advanced-disjoint'; baseTip: string; gateBase: string; windowCommits: number; reviewResult: string }
|
|
48
|
-
| { ok: false; reason: string };
|
|
49
|
+
| { ok: false; reason: string; baseTip?: string };
|
|
49
50
|
|
|
50
51
|
export interface VerifyOverlapEvidenceInput {
|
|
51
52
|
evidencePath: string;
|
|
@@ -222,6 +223,47 @@ function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>,
|
|
|
222
223
|
return { ok: true, reviewResult: e.reviewResult };
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
/** Reuse the evidence-selected review when a pinned head needs a read-only virtual rebase. */
|
|
227
|
+
export function verifyOverlapEvidenceVirtualRebase(input: VerifyOverlapEvidenceInput, baseTip: string):
|
|
228
|
+
{ ok: true; reviewResult: string; patchId: string } | { ok: false; reason: string; baseTip?: string } {
|
|
229
|
+
const head = input.expectedHead.toLowerCase();
|
|
230
|
+
if (!SHA40.test(head) || !SHA40.test(baseTip)) return { ok: false, reason: 'virtual-rebase-invalid-sha' };
|
|
231
|
+
let raw: unknown;
|
|
232
|
+
try { raw = JSON.parse(input.readFile(input.evidencePath)); }
|
|
233
|
+
catch { return { ok: false, reason: 'evidence-unreadable' }; }
|
|
234
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'evidence-not-object' };
|
|
235
|
+
const evidence = raw as Partial<OverlapEvidenceFile>;
|
|
236
|
+
const evidencePr = typeof evidence.pr === 'number' ? evidence.pr
|
|
237
|
+
: typeof evidence.pr === 'string' && /^[0-9]+$/.test(evidence.pr) ? Number(evidence.pr) : NaN;
|
|
238
|
+
if (!Number.isSafeInteger(evidencePr) || evidencePr !== input.pr) return { ok: false, reason: 'evidence-pr-mismatch' };
|
|
239
|
+
if (typeof evidence.head !== 'string' || evidence.head.toLowerCase() !== head) return { ok: false, reason: 'evidence-head-mismatch' };
|
|
240
|
+
const gate = evidence.gate;
|
|
241
|
+
if (!gate || typeof gate !== 'object') return { ok: false, reason: 'gate-missing' };
|
|
242
|
+
if (gate.passed !== true) return { ok: false, reason: 'gate-not-passed' };
|
|
243
|
+
if (typeof gate.head !== 'string' || gate.head.toLowerCase() !== head) return { ok: false, reason: 'gate-head-mismatch' };
|
|
244
|
+
if (typeof gate.baseCommit !== 'string' || !SHA40.test(gate.baseCommit)) return { ok: false, reason: 'gate-base-unmeasured' };
|
|
245
|
+
const review = checkReview(input, evidence, head, baseTip);
|
|
246
|
+
if (!review.ok) return review;
|
|
247
|
+
const attempts = checkSelectedReviewAttempts(input, evidence, head);
|
|
248
|
+
if (!attempts.ok) return attempts;
|
|
249
|
+
const remote = input.run('git', ['ls-remote', input.remote, `refs/heads/${input.branch}`], { cwd: input.cwd });
|
|
250
|
+
if (!remote.ok || remote.out.trim().split(/\s+/)[0]?.toLowerCase() !== baseTip.toLowerCase()) {
|
|
251
|
+
return { ok: false, reason: 'virtual-rebase-base-advanced' };
|
|
252
|
+
}
|
|
253
|
+
const local = input.run('git', ['rev-parse', '--verify', `refs/remotes/${input.remote}/${input.branch}`], { cwd: input.cwd });
|
|
254
|
+
if (!local.ok || local.out.trim().toLowerCase() !== baseTip.toLowerCase()) {
|
|
255
|
+
return { ok: false, reason: 'virtual-rebase-base-stale-or-unmeasured' };
|
|
256
|
+
}
|
|
257
|
+
const patch = verifyVirtualRebasePatch({ cwd: input.cwd, baseTip, head });
|
|
258
|
+
if (!patch.ok) return patch;
|
|
259
|
+
const oldBase = patch.oldBase.toLowerCase();
|
|
260
|
+
const gateBase = gate.baseCommit.toLowerCase();
|
|
261
|
+
if (!input.run('git', ['merge-base', '--is-ancestor', gateBase, oldBase], { cwd: input.cwd }).ok) {
|
|
262
|
+
return { ok: false, reason: 'gate-base-not-ancestor-of-old-base' };
|
|
263
|
+
}
|
|
264
|
+
return { ok: true, reviewResult: review.reviewResult, patchId: patch.patchId };
|
|
265
|
+
}
|
|
266
|
+
|
|
225
267
|
type WindowResult = { ok: true; windowCommits: number } | { ok: false; reason: string; windowCommits?: number };
|
|
226
268
|
|
|
227
269
|
/**
|
|
@@ -431,7 +473,9 @@ export function verifyOverlapEvidence(input: VerifyOverlapEvidenceInput): Overla
|
|
|
431
473
|
gateBase: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.gateBase : undefined) : verdict.gateBase) ?? null,
|
|
432
474
|
windowCommits: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.windowCommits : undefined) : verdict.windowCommits) ?? null,
|
|
433
475
|
});
|
|
434
|
-
if (!verdict.ok) return
|
|
476
|
+
if (!verdict.ok) return (verdict.reason.startsWith('not-rebased:') || verdict.reason.startsWith('base-stale-overlap:')) && verdict.baseTip
|
|
477
|
+
? { ok: false, reason: verdict.reason, baseTip: verdict.baseTip }
|
|
478
|
+
: { ok: false, reason: verdict.reason };
|
|
435
479
|
if (verdict.mode === 'already-ancestor') return { ok: true, mode: 'already-ancestor', reviewResult: verdict.reviewResult };
|
|
436
480
|
if (verdict.mode === 'base-advanced-disjoint') {
|
|
437
481
|
return { ok: true, mode: 'base-advanced-disjoint', baseTip: verdict.baseTip!, gateBase: verdict.gateBase, windowCommits: verdict.windowCommits, reviewResult: verdict.reviewResult };
|
|
@@ -7,6 +7,9 @@ import type { RootPlanSummary } from './pr-land-heavy-remote.js';
|
|
|
7
7
|
|
|
8
8
|
const SHA = /^[0-9a-f]{40}$/i;
|
|
9
9
|
const MARKER = 'ELANOUS_PRLAND_SHADOW';
|
|
10
|
+
/** Shadow evidence is optional; it must not hold a ready landing for the Pod's 45-minute deadline. */
|
|
11
|
+
export const LANDING_POD_SHADOW_HOST_TIMEOUT_MS = 10 * 60_000;
|
|
12
|
+
export const LANDING_POD_SHADOW_STATUS_UNREADABLE_MS = 60_000;
|
|
10
13
|
|
|
11
14
|
export interface LandingPodShadowInput { head: string; main: string }
|
|
12
15
|
export interface LandingPodShadowVerdict {
|
|
@@ -42,12 +45,32 @@ export function landingPodShadowScript(head: string, main: string): string {
|
|
|
42
45
|
export async function runLandingPodShadowJob(input: LandingPodShadowInput, deps: {
|
|
43
46
|
runPod?: (options: RunPodCommandOptions) => Promise<PodCommandResult>;
|
|
44
47
|
readLog?: (path: string) => string;
|
|
48
|
+
/** Deterministic timer seam for the host deadline. */
|
|
49
|
+
armTimeout?: (onTimeout: () => void, ms: number) => () => void;
|
|
45
50
|
} = {}): Promise<LandingPodShadowVerdict> {
|
|
46
51
|
const script = landingPodShadowScript(input.head, input.main);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const armTimeout = deps.armTimeout ?? ((onTimeout: () => void, ms: number) => {
|
|
54
|
+
const timer = setTimeout(onTimeout, ms);
|
|
55
|
+
return () => clearTimeout(timer);
|
|
50
56
|
});
|
|
57
|
+
const disarm = armTimeout(() => {
|
|
58
|
+
const reason = new Error(`pod shadow: host timeout after ${LANDING_POD_SHADOW_HOST_TIMEOUT_MS}ms`);
|
|
59
|
+
controller.abort(reason);
|
|
60
|
+
}, LANDING_POD_SHADOW_HOST_TIMEOUT_MS);
|
|
61
|
+
let job: PodCommandResult;
|
|
62
|
+
try {
|
|
63
|
+
// Wait for the runner to observe abort and finish Job/Secret/lease cleanup before landing continues.
|
|
64
|
+
job = await (deps.runPod ?? runPodCommand)({
|
|
65
|
+
command: ['bash', '-lc', script], clone: true, source: { kind: 'commit', sha: input.head },
|
|
66
|
+
returnLogs: true, deadlineSeconds: 45 * 60,
|
|
67
|
+
statusUnreadableMs: LANDING_POD_SHADOW_STATUS_UNREADABLE_MS,
|
|
68
|
+
hostDeadlineAtMs: Date.now() + LANDING_POD_SHADOW_HOST_TIMEOUT_MS,
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
});
|
|
71
|
+
} finally {
|
|
72
|
+
disarm();
|
|
73
|
+
}
|
|
51
74
|
let log: string;
|
|
52
75
|
try { log = (deps.readLog ?? ((path: string) => readFileSync(path, 'utf8')))(join(job.artifactsDir, 'child.log')); }
|
|
53
76
|
catch (error) {
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
import { readJourneyEdge, recordJourneyEdge } from '../self-dev/graph-journey-route.js';
|
|
2
2
|
import { debug } from '../debug/log.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
export type PrLandRefusal = 'draft' | 'head-branch' | 'not-ancestor' | 'review-missing'
|
|
4
|
+
/** Observed `pr land` refusal reasons. Classification never retries a landing. */
|
|
5
|
+
export type PrLandRefusal = 'draft' | 'head-branch' | 'not-ancestor' | 'review-missing'
|
|
6
|
+
| 'gate-host-busy' | 'gate-merge-conflict' | 'gate-touch-clean'
|
|
7
|
+
| 'pr-base-unreadable' | 'public-export-leak' | 'other';
|
|
6
8
|
|
|
7
9
|
export function classifyPrLandRefusal(message: string, context: { isDraft?: boolean } = {}): PrLandRefusal {
|
|
8
10
|
if (context.isDraft === true && message.startsWith('✗ expected-head: PR OPEN non-draft')) return 'draft';
|
|
9
11
|
if (message.startsWith('✗ branch: PR head branch와 현재 브랜치가 다릅니다.')) return 'head-branch';
|
|
10
12
|
if (message.startsWith('✗ ancestor-auto: ancestor-auto-not-ancestor:')) return 'not-ancestor';
|
|
11
13
|
if (/^✗ (?:ancestor-auto|overlap-evidence): (?:review-missing|review-not-reviewed|review-error|review-no-result|review-unreadable|review-pr-mismatch|review-head-mismatch|review-path-not-for-this-pr-head|review-verdict-null|review-must-fix-unknown)(?:\b|$)/.test(message)) return 'review-missing';
|
|
14
|
+
if (message.startsWith('✗ heavy-remote: PR base=unreadable ≠ main')) return 'pr-base-unreadable';
|
|
15
|
+
if (/^✗ heavy-remote: (?:원격|병합 직전 원격|node-b) 게이트 gate-host-busy(?:\b|\s|$)/.test(message)) return 'gate-host-busy';
|
|
16
|
+
if (message.startsWith('✗ heavy-remote: 원격 게이트 gate-rc-1 · remote: stdout: CONFLICT (content): Merge conflict')) return 'gate-merge-conflict';
|
|
17
|
+
if (message.startsWith('✗ heavy-remote: 원격 게이트 gate-rc-1 · remote: stderr: 건드린 src/scripts 파일과 baseline 밖 test 파일은 tsc 클린이어야 합니다(touch-clean).')) return 'gate-touch-clean';
|
|
18
|
+
if (message.startsWith('✗ public-export-leak: 바뀐 파일')) return 'public-export-leak';
|
|
12
19
|
return 'other';
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
/** Direct landing failures are observed only when today's refusal shape is recognized. */
|
|
23
|
+
export function observeDirectPrLandRefusal(message: string): void {
|
|
24
|
+
if (classifyPrLandRefusal(message) === 'other') return;
|
|
25
|
+
observePrLandRefusal(message);
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/** Read the declared recovery edge and observe the refusal. No recovery command runs here. */
|
|
16
29
|
export function observePrLandRefusal(message: string, context: { isDraft?: boolean } = {}): void {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
try {
|
|
31
|
+
const route = readJourneyEdge('merge', classifyPrLandRefusal(message, context));
|
|
32
|
+
if (route.to !== null) recordJourneyEdge(route, route.to, 'pr-land-refused; recovery-execution=none');
|
|
33
|
+
else debug.log('self-implement', 'graph-edge-unresolved', { from: 'merge', label: route.label, reason: 'pr-land-refused; recovery-execution=none' });
|
|
34
|
+
} catch { /* An observation failure must never change the landing refusal. */ }
|
|
20
35
|
}
|