elanous 0.2.19 → 0.2.20
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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/demo/editor-recipes.yaml +22 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +2 -1
- package/scripts/release-loop/gate-node.ts +296 -25
- package/scripts/release-loop/gate-timing.ts +116 -7
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +4 -1
- package/scripts/release-loop/unattended-release.ts +10 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/release-watch.ts +20 -3
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/release-cli.ts +20 -5
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/tasks-cli.ts +94 -8
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
- package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +11 -1
- package/src/harness/harness-cli-command.ts +419 -52
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +62 -0
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +11 -1
- package/src/index.ts +69 -6
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +38 -3
- package/src/nexus/api/loop-edges.ts +4 -2
- package/src/nexus/api/ops-api.ts +22 -7
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/feature-store.ts +14 -11
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/pr-preview.ts +84 -16
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/orchestrator.ts +218 -5
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/board.ts +1 -1
- package/src/task-agent/card-evidence.ts +99 -10
- package/src/task-agent/judge.ts +7 -0
- package/src/task-agent/live-moves.ts +120 -20
- package/src/task-agent/mission.ts +1 -1
- package/src/task-agent/shadow.ts +10 -1
- package/src/task-agent/task-hand.ts +7 -2
- package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
- package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
- package/src/telegram-commands.ts +41 -0
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/user-config.ts +56 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
- /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// LAND-GLOBAL-CHECKS — 착지 게이트가 «전역 검사»를 바뀐 파일로 끌어온다.
|
|
3
|
+
//
|
|
4
|
+
// `pr land` 는 바뀐 파일만 본다. 그런데 저장소 전체를 훑는 정적 시험(전역 검사)은 «그 시험 파일»이
|
|
5
|
+
// 안 바뀌면 착지에서 한 번도 안 돈다 — 0.2.19 회귀 9 중 넷이 이 꼴이었다
|
|
6
|
+
// (hook-order-sweep #24719 · next.md B11 #24688 · machines 생성 Markdown #24729 · harness 하위 명령 목록 #24661).
|
|
7
|
+
//
|
|
8
|
+
// ⭐ 표는 «여기 한 곳»이다(경로 꼴 → 시험). 새 전역 검사를 붙이려면 이 표에 줄을 더한다.
|
|
9
|
+
// 재는 명령: `bun scripts/land-global-checks.ts` (표) · `bun scripts/land-global-checks.ts <바뀐 파일…>` (끌려오는 시험)
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
type LandGlobalCheck = {
|
|
14
|
+
/** 바뀐 파일 경로(저장소 루트 기준 · `/` 구분)에 대는 꼴. */
|
|
15
|
+
pattern: RegExp;
|
|
16
|
+
/** 끌려오는 전역 시험 파일. */
|
|
17
|
+
test: string;
|
|
18
|
+
/** 왜 이 꼴이 이 시험을 부르나 — 회귀 근거. */
|
|
19
|
+
why: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const LAND_GLOBAL_CHECKS: readonly LandGlobalCheck[] = [
|
|
23
|
+
{ pattern: /^apps\/pwa\/.+\.tsx$/, test: 'test/hook-order-sweep.test.ts', why: 'PWA 컴포넌트 훅 순서(#24719)' },
|
|
24
|
+
{ pattern: /^release\/next\.md$/, test: 'scripts/release-story/draft.test.ts', why: '판 노트 초안·브랜드 규칙(#24688)' },
|
|
25
|
+
{ pattern: /^docs\/ops\/machines\.yaml$/, test: 'test/machines-ledger-data.test.ts', why: '기계 원장 데이터 ↔ 생성 Markdown(#24729)' },
|
|
26
|
+
// 새 CLI 하위 명령은 `src/index.ts` 또는 `src/cli/` 의 등록 모듈에서 생긴다 — 입구 목록 시험(#24661).
|
|
27
|
+
{ pattern: /^src\/(?:index|cli\/(?:[^/]+\/)*[^/]+)(?<!\.test)\.ts$/, test: 'src/index.test.ts', why: 'CLI 입구·하위 명령 목록(#24661)' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** 바뀐 파일 → 끌려오는 전역 시험(표 순서 · 중복 없음). */
|
|
31
|
+
export function landGlobalChecks(changedFiles: readonly string[], table: readonly LandGlobalCheck[] = LAND_GLOBAL_CHECKS): string[] {
|
|
32
|
+
const tests: string[] = [];
|
|
33
|
+
for (const { pattern, test } of table) {
|
|
34
|
+
if (!tests.includes(test) && changedFiles.some((file) => pattern.test(file.replace(/\\/g, '/')))) tests.push(test);
|
|
35
|
+
}
|
|
36
|
+
return tests;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type LandGlobalChecksRun = { status: number | null; output: string; error?: string };
|
|
40
|
+
|
|
41
|
+
type LandGlobalChecksGateIo = {
|
|
42
|
+
changedFiles: readonly string[];
|
|
43
|
+
cwd: string;
|
|
44
|
+
log: (message: string) => void;
|
|
45
|
+
error: (message: string) => void;
|
|
46
|
+
/** 시험 주입용 — 기본은 결정적 러너(`scripts/test-deterministic.ts`)로 끌려온 시험만 돌린다. */
|
|
47
|
+
runTests?: (tests: readonly string[], cwd: string) => LandGlobalChecksRun;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function runDeterministic(tests: readonly string[], cwd: string): LandGlobalChecksRun {
|
|
51
|
+
const r = spawnSync('bun', ['run', 'scripts/test-deterministic.ts', ...tests], {
|
|
52
|
+
cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, timeout: 15 * 60_000,
|
|
53
|
+
});
|
|
54
|
+
return { status: r.status, output: `${r.stdout ?? ''}\n${r.stderr ?? ''}`, ...(r.error ? { error: r.error.message } : {}) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 반환: true = 해당 없음 또는 끌려온 시험 통과 · false = 끌려온 시험이 실패했다(착지를 막는다).
|
|
59
|
+
* ⛔ 러너가 «못 돌았다»(spawn 실패·시간 초과)면 던진다 — 호출부(`gateVerdict`)가 「검사 못 함」으로 말한다.
|
|
60
|
+
*/
|
|
61
|
+
export function runLandGlobalChecksGate(io: LandGlobalChecksGateIo): boolean {
|
|
62
|
+
const tests = landGlobalChecks(io.changedFiles);
|
|
63
|
+
if (tests.length === 0) {
|
|
64
|
+
io.log(`[land-global-checks] 해당 없음 — 바뀐 파일 ${io.changedFiles.length}개가 전역 검사 표(${LAND_GLOBAL_CHECKS.length}줄)의 꼴에 안 걸린다.`);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
io.log(`[land-global-checks] 전역 검사 ${tests.length}개를 끌어온다: ${tests.join(' · ')}`);
|
|
68
|
+
const result = (io.runTests ?? runDeterministic)(tests, io.cwd);
|
|
69
|
+
if (result.error !== undefined || result.status === null) {
|
|
70
|
+
throw new Error(`전역 검사 러너가 못 돌았다 — ${result.error ?? 'status=null'}`);
|
|
71
|
+
}
|
|
72
|
+
if (result.status !== 0) {
|
|
73
|
+
const tail = result.output.split('\n').filter((line) => /\(fail\)|^\s*\d+ (pass|fail)|^Ran /.test(line)).slice(-12);
|
|
74
|
+
for (const line of tail) io.error(`[land-global-checks] ${line.trim()}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (import.meta.main) {
|
|
81
|
+
const changed = process.argv.slice(2);
|
|
82
|
+
if (changed.length === 0) {
|
|
83
|
+
console.log(`전역 검사 표 ${LAND_GLOBAL_CHECKS.length}줄`);
|
|
84
|
+
for (const row of LAND_GLOBAL_CHECKS) console.log(`${row.pattern.source} → ${row.test} · ${row.why}`);
|
|
85
|
+
} else {
|
|
86
|
+
const tests = landGlobalChecks(changed);
|
|
87
|
+
console.log(`끌려오는 전역 시험 ${tests.length}개${tests.length ? `: ${tests.join(' · ')}` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -15,7 +15,8 @@ export const PCH_REGRESS_MAP: Record<string, readonly string[]> = {
|
|
|
15
15
|
'PCH-8': ['src/lib/chat-harness-ask.test.ts', 'src/components/chat/HarnessAskCard.test.tsx'],
|
|
16
16
|
'PCH-9': ['src/components/chat/ChatPendingDecision.test.tsx', 'src/components/chat/ChatDecisionsChip.test.tsx'],
|
|
17
17
|
'PCH-10': ['src/lib/chat-runtime.meta.test.ts'], // /rewind · /undo meta commands.
|
|
18
|
-
|
|
18
|
+
// Full fork and turn-bearing rewind/undo branches; live PWA behavior still needs observation.
|
|
19
|
+
'PCH-11': ['src/lib/chat-fork.test.ts', 'src/lib/chat-branch.test.ts'],
|
|
19
20
|
'PCH-12': ['src/lib/chat-runtime.run-skill.test.ts'],
|
|
20
21
|
'PCH-13': ['src/components/chat/ChatConversationList.test.tsx'], // Chat search; export has no identified PWA test.
|
|
21
22
|
'PCH-14': ['src/lib/compact-mode.test.ts'], // Compact; plan toggle has no identified PWA test.
|
|
@@ -19,6 +19,8 @@ import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
|
19
19
|
import { runPodCommand, parsePodCpu, resolvePodCpu, type RunPodCommandOptions, type PodCommandResult, type PodCpu } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
20
20
|
import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
|
|
21
21
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
22
|
+
import { POD_INSTALL_SLOTS_DEFAULT, POD_INSTALL_SLOTS_HOST_PATH, installSlotScript } from '../../src/task-orchestrator/surfaces/pod-install-slots.js';
|
|
23
|
+
import { gateShardsPath, writeGateShards, type GateShard } from '../../src/release-loop/gate-shards.js';
|
|
22
24
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
23
25
|
|
|
24
26
|
interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }>; timedOut?: boolean }
|
|
@@ -51,7 +53,16 @@ export interface GateOptions {
|
|
|
51
53
|
* with these is not re-run in isolation; the verdict still classifies them against the baseline as before. */
|
|
52
54
|
knownFailures?: readonly string[];
|
|
53
55
|
/** GATE-SPEED A3① — shard Pod CPU request/limit (`release.loop.gatePodCpu`); omitted = request 1 / limit 4. */
|
|
54
|
-
cpu?: PodCpu
|
|
56
|
+
cpu?: PodCpu;
|
|
57
|
+
/** GATE-INSTALL-CACHE — concurrent `bun install`s per node (`release.loop.gatePodInstallSlots`); omitted = 4 · 0 = off. */
|
|
58
|
+
installSlots?: number;
|
|
59
|
+
/** GATE-MEM-ADMIT — seconds a shard waits for pool admission (memory headroom) before it launches anyway with
|
|
60
|
+
* `admission-timeout` (`release.loop.gatePodAdmissionWaitSeconds`); omitted = {@link GATE_ADMISSION_WAIT_SECONDS_DEFAULT} · 0 = off. */
|
|
61
|
+
admissionWaitSeconds?: number;
|
|
62
|
+
/** GATE-MEM-ADMIT — an injected admission (tests · embedders); config never sets it. Absent = the gate's own pool's. */
|
|
63
|
+
admission?: GateAdmission;
|
|
64
|
+
/** GATE-LIVE-OBS — the shard state file (`gateShardsPath(version)`) this sweep keeps current; absent = not written. */
|
|
65
|
+
shardsFile?: { path: string; version: string } };
|
|
55
66
|
}
|
|
56
67
|
export interface GateResult {
|
|
57
68
|
outcome: 'ok' | 'regression' | 'error';
|
|
@@ -70,7 +81,7 @@ export interface GateResult {
|
|
|
70
81
|
baselineDeferred?: { cutFiles: number; swept: number };
|
|
71
82
|
durationMs: number;
|
|
72
83
|
error?: string;
|
|
73
|
-
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
84
|
+
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed' | 'timeout'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
74
85
|
partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
|
|
75
86
|
/** GATE-PARTIAL — this verdict re-ran only these files and carried the prior gate's results for the rest. */
|
|
76
87
|
partial?: { priorCommit: string; files: number };
|
|
@@ -205,6 +216,138 @@ export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-20
|
|
|
205
216
|
/** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
|
|
206
217
|
export const POD_SHARD_FILE_CAP = 27;
|
|
207
218
|
|
|
219
|
+
/** GATE-TIMEOUT-HEAL (0.2.20) — the per-shard Job deadline is the shard's planned wall-clock × 1.5, never under the
|
|
220
|
+
* configured floor (`release.loop.gatePodShardTimeoutSeconds` · default 1200 s) and never over the cap (or the floor,
|
|
221
|
+
* if that is higher). 0.2.19: the largest shard was planned at 18.9 min, ran 20–27 min (install 220–340 s under load)
|
|
222
|
+
* and 11 of 24 shards were cut at the flat 1200 s with nothing left to read. */
|
|
223
|
+
export const POD_SHARD_TIMEOUT_FLOOR_SECONDS = 1200;
|
|
224
|
+
export const POD_SHARD_TIMEOUT_CAP_SECONDS = 3600;
|
|
225
|
+
export function shardDeadlineSeconds(plannedSeconds: number, configured?: number): number {
|
|
226
|
+
const floor = configured ?? POD_SHARD_TIMEOUT_FLOOR_SECONDS;
|
|
227
|
+
const scaled = Number.isFinite(plannedSeconds) && plannedSeconds > 0 ? Math.ceil(plannedSeconds * 1.5) : 0;
|
|
228
|
+
return Math.min(Math.max(floor, POD_SHARD_TIMEOUT_CAP_SECONDS), Math.max(floor, scaled));
|
|
229
|
+
}
|
|
230
|
+
/** Seconds the Pod keeps after the in-shell soft deadline: kill grace, artifact emission, and slack for what the Job
|
|
231
|
+
* deadline counts before the shell starts and the shell cannot see (scheduling · init containers). */
|
|
232
|
+
export const POD_SHARD_EMIT_RESERVE_SECONDS = 120;
|
|
233
|
+
export const softDeadlineSeconds = (deadline: number) => deadline > 2 * POD_SHARD_EMIT_RESERVE_SECONDS
|
|
234
|
+
? deadline - POD_SHARD_EMIT_RESERVE_SECONDS : Math.max(1, Math.floor(deadline * 0.8));
|
|
235
|
+
/** GATE-TIMEOUT-HEAL ② — a shard runs as consecutive parts of about this many planned seconds (at most
|
|
236
|
+
* {@link POD_SHARD_FILE_CAP} files), each with its own log and junit, so a shard cut by its deadline still leaves the
|
|
237
|
+
* results of every part that finished (F1/F2 read them) instead of nothing. */
|
|
238
|
+
export const POD_SHARD_PART_SECONDS = 120;
|
|
239
|
+
export function shardParts(paths: readonly string[], seconds: (file: string) => number, target = POD_SHARD_PART_SECONDS): string[][] {
|
|
240
|
+
const parts: string[][] = [];
|
|
241
|
+
let current: string[] = [];
|
|
242
|
+
let sum = 0;
|
|
243
|
+
for (const file of paths) {
|
|
244
|
+
current.push(file);
|
|
245
|
+
sum += seconds(file);
|
|
246
|
+
if (sum >= target || current.length >= POD_SHARD_FILE_CAP) { parts.push(current); current = []; sum = 0; }
|
|
247
|
+
}
|
|
248
|
+
if (current.length) parts.push(current);
|
|
249
|
+
return parts;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* The shard's Pod shell. Install once (behind the node's install slots), then run each part under the soft deadline,
|
|
253
|
+
* leaving `install.log`/`install.rc`, `part-<i>.log`/`.rc`/`.junit.xml`, and `shard.timeout` (the part index) when the
|
|
254
|
+
* deadline cut the shard. A part without `.rc` did not finish. The shell itself always exits 0 (as before).
|
|
255
|
+
*/
|
|
256
|
+
export function gateShardShell(o: { parts: readonly (readonly string[])[]; cdpPatterns: readonly string[]; softSeconds: number; cachePrefix: string; installSlots: number }): string {
|
|
257
|
+
const run = o.parts.map((part, index) => {
|
|
258
|
+
const ignores = o.cdpPatterns.filter((pattern) => part.includes(pattern)).flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
259
|
+
return `run_part ${index} ${['bun', 'run', 'test:deterministic', ...ignores, ...part.map(asPath)].map(quote).join(' ')}`;
|
|
260
|
+
}).join(' && ');
|
|
261
|
+
return [
|
|
262
|
+
'mkdir -p "$HOME/outbox"; O="$HOME/outbox"',
|
|
263
|
+
...(o.cachePrefix ? [o.cachePrefix] : []),
|
|
264
|
+
// Seconds since this container's PID 1 started (the clone ran before this shell) — 0 when /proc cannot tell.
|
|
265
|
+
`pod_age=$(awk -v t="$(cut -d' ' -f22 /proc/1/stat 2>/dev/null)" -v hz="$(getconf CLK_TCK 2>/dev/null)" '{ if (t > 0 && hz > 0) print int($1 - t / hz); else print 0 }' /proc/uptime 2>/dev/null)`,
|
|
266
|
+
'case "$pod_age" in ""|*[!0-9]*) pod_age=0;; esac',
|
|
267
|
+
`gate_end=$(( $(date +%s) + ${Math.max(1, Math.floor(o.softSeconds))} - pod_age ))`,
|
|
268
|
+
...(o.installSlots > 0 ? [installSlotScript({ slots: o.installSlots })] : []),
|
|
269
|
+
'run_part() {',
|
|
270
|
+
' local i=$1; shift',
|
|
271
|
+
' local left=$(( gate_end - $(date +%s) ))',
|
|
272
|
+
' if [ "$left" -le 0 ]; then echo "$i" > "$O/shard.timeout"; return 1; fi',
|
|
273
|
+
' timeout -k 10 "$left" "$@" --reporter=junit --reporter-outfile="$O/part-$i.junit.xml" > "$O/part-$i.log" 2>&1',
|
|
274
|
+
' local prc=$?',
|
|
275
|
+
' if [ "$(date +%s)" -ge "$gate_end" ] && { [ "$prc" -eq 124 ] || [ "$prc" -eq 137 ] || [ "$prc" -eq 143 ]; }; then echo "$i" > "$O/shard.timeout"; return 1; fi',
|
|
276
|
+
' echo "$prc" > "$O/part-$i.rc"',
|
|
277
|
+
'}',
|
|
278
|
+
'if cd .. && cd repo; then',
|
|
279
|
+
...(o.installSlots > 0 ? [' install_slot_acquire >> "$O/install.log" 2>&1'] : []),
|
|
280
|
+
' { bun install && (cd apps/pwa && bun install); } >> "$O/install.log" 2>&1; irc=$?',
|
|
281
|
+
...(o.installSlots > 0 ? [' install_slot_release'] : []),
|
|
282
|
+
' echo "$irc" > "$O/install.rc"',
|
|
283
|
+
` if [ "$irc" -eq 0 ]; then ${run || 'true'}; fi`,
|
|
284
|
+
'else echo 5 > "$O/install.rc"; fi',
|
|
285
|
+
'true',
|
|
286
|
+
].join('\n');
|
|
287
|
+
}
|
|
288
|
+
/** Removes bun's own summary lines so one summary over several runs can follow (same rule as the sweep's merge). */
|
|
289
|
+
const stripSummary = (output: string) => output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, '');
|
|
290
|
+
export interface ShardArtifacts {
|
|
291
|
+
/** The verdict text: install log ⊕ every finished part (its summary removed) ⊕ one summary over the finished parts. */
|
|
292
|
+
output?: string;
|
|
293
|
+
junit?: string;
|
|
294
|
+
/** 0|1 when every part finished · the failing part's exit when one crashed · undefined when the deadline cut the shard. */
|
|
295
|
+
rc?: number;
|
|
296
|
+
timedOut: boolean;
|
|
297
|
+
/** Files of the parts that finished with a readable summary. */
|
|
298
|
+
finished: string[];
|
|
299
|
+
/** Raw logs of the parts that did not finish (diagnostics · last started file). */
|
|
300
|
+
unfinishedLog?: string;
|
|
301
|
+
}
|
|
302
|
+
/** Reads a shard Pod's outbox: the part layout ({@link gateShardShell}) or the older single-run one (shard.log · shard.rc · junit.xml). */
|
|
303
|
+
export function readShardArtifacts(dir: string, parts: readonly (readonly string[])[]): ShardArtifacts {
|
|
304
|
+
const read = (name: string) => { const path = join(dir, name); return existsSync(path) ? readFileSync(path, 'utf8') : undefined; };
|
|
305
|
+
const rcOf = (text: string | undefined) => { const value = text?.trim() ?? ''; return /^\d+$/.test(value) && Number.isSafeInteger(Number(value)) ? Number(value) : undefined; };
|
|
306
|
+
const installRc = rcOf(read('install.rc'));
|
|
307
|
+
const install = read('install.log');
|
|
308
|
+
if (installRc === undefined && install === undefined) {
|
|
309
|
+
const junit = read('junit.xml');
|
|
310
|
+
const rc = rcOf(read('shard.rc'));
|
|
311
|
+
return { output: read('shard.log'), ...(junit !== undefined ? { junit } : {}), ...(rc !== undefined ? { rc } : {}), timedOut: false, finished: [] };
|
|
312
|
+
}
|
|
313
|
+
const timedOut = read('shard.timeout') !== undefined;
|
|
314
|
+
if (installRc !== 0) return { output: install, ...(installRc !== undefined ? { rc: installRc } : {}), timedOut, finished: [] };
|
|
315
|
+
const good: Array<{ log: string; junit?: string; rc: 0 | 1; files: readonly string[] }> = [];
|
|
316
|
+
const bad: string[] = [];
|
|
317
|
+
let badRc: number | undefined;
|
|
318
|
+
parts.forEach((files, index) => {
|
|
319
|
+
const log = read(`part-${index}.log`);
|
|
320
|
+
const rc = rcOf(read(`part-${index}.rc`));
|
|
321
|
+
const clean = log?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
322
|
+
if (clean !== undefined && (rc === 0 || rc === 1) && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/)) {
|
|
323
|
+
const junit = read(`part-${index}.junit.xml`);
|
|
324
|
+
good.push({ log: clean, rc, files, ...(junit !== undefined ? { junit } : {}) });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (log !== undefined) bad.push(`# part ${index} (${rc === undefined ? 'unfinished' : `rc=${rc}`})\n${log}`);
|
|
328
|
+
// A finished part without a readable summary is not a verdict: its exit, or 2 when it claimed 0|1.
|
|
329
|
+
if (rc !== undefined && badRc === undefined) badRc = rc === 0 || rc === 1 ? 2 : rc;
|
|
330
|
+
});
|
|
331
|
+
const total = good.reduce((sum, part) => {
|
|
332
|
+
const n = (label: string) => { const value = summaryCount(part.log, label); return Number.isNaN(value) ? 0 : value; };
|
|
333
|
+
const ran = lastMatch(part.log, /Ran (\d+) tests? across (\d+) files?/)!;
|
|
334
|
+
return { pass: sum.pass + n('pass'), fail: sum.fail + n('fail'), errors: sum.errors + n('error'), ran: sum.ran + Number(ran[1]), files: sum.files + Number(ran[2]) };
|
|
335
|
+
}, { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
|
|
336
|
+
const output = [install ?? '', ...good.map((part) => stripSummary(part.log)),
|
|
337
|
+
...(good.length ? [`${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n`] : [])].join('\n');
|
|
338
|
+
const junits = good.flatMap((part) => part.junit !== undefined ? [part.junit] : []);
|
|
339
|
+
const complete = good.length === parts.length;
|
|
340
|
+
const rc = complete ? (good.some((part) => part.rc === 1) ? 1 : 0) : timedOut && badRc === undefined ? undefined : (badRc ?? 2);
|
|
341
|
+
return {
|
|
342
|
+
output,
|
|
343
|
+
...(junits.length ? { junit: junits.join('\n') } : {}),
|
|
344
|
+
...(rc !== undefined ? { rc } : {}),
|
|
345
|
+
timedOut: !complete && timedOut && badRc === undefined,
|
|
346
|
+
finished: good.flatMap((part) => [...part.files]),
|
|
347
|
+
...(bad.length ? { unfinishedLog: bad.join('\n') } : {}),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
208
351
|
// review-model-ab: Pod 에서 출력 없이 멈춤 2/2(10-04 0.2.11 · 로컬 단독 4/0 · 1.7s) — GATE-STALL 수리 전까지 Pod 밖.
|
|
209
352
|
export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts', 'scripts/review-model-ab.test.ts'];
|
|
210
353
|
/** Nightly-only whole-repository cases; gate runs the fixture cases in these same files. */
|
|
@@ -368,6 +511,54 @@ export function limitedLocalCommand(cmd: string, args: string[], cwd: string, li
|
|
|
368
511
|
});
|
|
369
512
|
}
|
|
370
513
|
|
|
514
|
+
/**
|
|
515
|
+
* GATE-MEM-ADMIT (0.2.20) — the gate's shard Jobs ask the same pool admission harness goal Pods ask
|
|
516
|
+
* (`PodPoolScheduler.acquireAdmission` · memory headroom by observed usage · `limitedBy`). 0.2.19: 8 shards sat Pending
|
|
517
|
+
* for CPU (requests 31.2/32) because harness Pods held the node and the gate counted only its own Jobs (#24698).
|
|
518
|
+
* The gate never deadlocks on it: after {@link GATE_ADMISSION_WAIT_SECONDS_DEFAULT} it launches anyway and records
|
|
519
|
+
* `admission-timeout`; an admission error launches at once (`admission-error`). Harness launches wait without a bound,
|
|
520
|
+
* so the gate keeps priority.
|
|
521
|
+
*/
|
|
522
|
+
export interface GateAdmission {
|
|
523
|
+
acquire(signal: AbortSignal): Promise<() => void>;
|
|
524
|
+
/** The pool-wide reading admission waits on (recommended · limitedBy · memorySlots); null before any reading. */
|
|
525
|
+
waitReason(): string | null;
|
|
526
|
+
}
|
|
527
|
+
export const GATE_ADMISSION_WAIT_SECONDS_DEFAULT = 300;
|
|
528
|
+
export type GateAdmissionOutcome = 'granted' | 'admission-timeout' | 'admission-error' | 'off';
|
|
529
|
+
export interface GateAdmissionResult { outcome: GateAdmissionOutcome; waitedMs: number; reason?: string; release: () => void }
|
|
530
|
+
export const gateAdmissionWaitText = (reading: string | null) => `메모리 여유 대기(admission) · ${reading ?? '측정 전'}`.slice(0, 160);
|
|
531
|
+
export function poolGateAdmission(pool: PodPoolScheduler): GateAdmission {
|
|
532
|
+
return { acquire: (signal) => pool.acquireAdmission(signal, undefined, { gate: true }), waitReason: () => pool.waitReason() };
|
|
533
|
+
}
|
|
534
|
+
export async function admitGateShard(admission: GateAdmission | undefined, o: { waitMs: number; onWait: (reason: string) => void; reasonPollMs?: number; firstReasonMs?: number }): Promise<GateAdmissionResult> {
|
|
535
|
+
const noop = () => {};
|
|
536
|
+
if (!admission || !(o.waitMs > 0)) return { outcome: 'off', waitedMs: 0, release: noop };
|
|
537
|
+
const started = Date.now();
|
|
538
|
+
const controller = new AbortController();
|
|
539
|
+
let timedOut = false;
|
|
540
|
+
let lastReason: string | undefined;
|
|
541
|
+
const tell = () => { lastReason = gateAdmissionWaitText(admission.waitReason()); o.onWait(lastReason); };
|
|
542
|
+
const deadline = setTimeout(() => { timedOut = true; controller.abort(); }, o.waitMs);
|
|
543
|
+
// A grant inside the first moment never touches the shard row (no flapping in shards.json).
|
|
544
|
+
let ticker: ReturnType<typeof setInterval> | undefined;
|
|
545
|
+
const first = setTimeout(() => { tell(); ticker = setInterval(tell, o.reasonPollMs ?? 15_000); }, o.firstReasonMs ?? 1_000);
|
|
546
|
+
try {
|
|
547
|
+
const granted = await admission.acquire(controller.signal);
|
|
548
|
+
// Released at placement and again after the Job (the launch may fail before placement): only the first counts.
|
|
549
|
+
let released = false;
|
|
550
|
+
const release = () => { if (!released) { released = true; granted(); } };
|
|
551
|
+
return { outcome: 'granted', waitedMs: Date.now() - started, ...(lastReason ? { reason: lastReason } : {}), release };
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (timedOut) return { outcome: 'admission-timeout', waitedMs: Date.now() - started, reason: lastReason ?? gateAdmissionWaitText(admission.waitReason()), release: noop };
|
|
554
|
+
return { outcome: 'admission-error', waitedMs: Date.now() - started, reason: (error instanceof Error ? error.message : String(error)).slice(0, 160), release: noop };
|
|
555
|
+
} finally {
|
|
556
|
+
clearTimeout(deadline);
|
|
557
|
+
clearTimeout(first);
|
|
558
|
+
if (ticker) clearInterval(ticker);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
371
562
|
export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
|
|
372
563
|
let remoteMirror: string | undefined;
|
|
373
564
|
const localCommand: GateRunner['localCommand'] = async (cmd, args, cwd, limitMs) => {
|
|
@@ -391,7 +582,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
391
582
|
: localCommand);
|
|
392
583
|
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>, only?: readonly string[]): Promise<CommandResult> => {
|
|
393
584
|
const shardCount = pod.shards ?? 24;
|
|
394
|
-
|
|
585
|
+
// GATE-TIMEOUT-HEAL ①: the configured value is the floor; each shard's own deadline is shardDeadlineSeconds(planned).
|
|
586
|
+
const deadlineSeconds = pod.shardTimeoutSeconds ?? POD_SHARD_TIMEOUT_FLOOR_SECONDS;
|
|
395
587
|
if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
|
|
396
588
|
|| !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds < 1) throw new Error('invalid pod sweep options');
|
|
397
589
|
const head = await command('git', ['rev-parse', 'HEAD'], tree);
|
|
@@ -449,7 +641,17 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
449
641
|
const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
|
|
450
642
|
const bunCache = configCache || envCache || undefined;
|
|
451
643
|
debug.log('release-loop.gate', 'pod-bun-cache', { source: configCache ? 'config' : envCache ? 'env' : 'none' });
|
|
452
|
-
const cachePrefix = bunCache ?
|
|
644
|
+
const cachePrefix = bunCache ? podBunCacheVolume(bunCache).shellPrefix : '';
|
|
645
|
+
// GATE-INSTALL-CACHE: node-wide install slots (see pod-install-slots.ts for the measured cause and the safety argument).
|
|
646
|
+
const installSlots = pod.installSlots ?? POD_INSTALL_SLOTS_DEFAULT;
|
|
647
|
+
if (!Number.isSafeInteger(installSlots) || installSlots < 0) throw new Error('invalid pod sweep options');
|
|
648
|
+
debug.log('release-loop.gate', 'pod-install-slots', { slots: installSlots, source: pod.installSlots !== undefined ? 'config' : 'default' });
|
|
649
|
+
// GATE-MEM-ADMIT: every shard Job (retries and splits too) asks pool admission before it is placed.
|
|
650
|
+
const admissionWaitSeconds = pod.admissionWaitSeconds ?? GATE_ADMISSION_WAIT_SECONDS_DEFAULT;
|
|
651
|
+
if (!Number.isSafeInteger(admissionWaitSeconds) || admissionWaitSeconds < 0) throw new Error('invalid pod sweep options');
|
|
652
|
+
// An injected pool (tests) runs without admission unless `pod.admission` is passed; the gate's own pool asks it.
|
|
653
|
+
const gateAdmission = pod.admission ?? (poolOverride ? undefined : poolGateAdmission(poolScheduler));
|
|
654
|
+
debug.log('release-loop.gate', 'pod-admission', { waitSeconds: admissionWaitSeconds, source: pod.admissionWaitSeconds !== undefined ? 'config' : 'default', enabled: !!gateAdmission && admissionWaitSeconds > 0 });
|
|
453
655
|
const jobLogTail = (job: PodCommandResult): string => {
|
|
454
656
|
if (podLogTail) return podLogTail(job);
|
|
455
657
|
for (const member of poolScheduler.members) {
|
|
@@ -469,22 +671,57 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
469
671
|
}, 0);
|
|
470
672
|
const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
|
|
471
673
|
const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
|
|
674
|
+
// GATE-LIVE-OBS — one row per root shard in the release ledger's shards.json (UX contract · src/release-loop/gate-shards.ts).
|
|
675
|
+
// Retries and splits of a shard update its row. A write failure is logged and never stops the gate.
|
|
676
|
+
const board: GateShard[] = shards.map((item, index) => ({ id: `pod-${index}`, state: 'pending', plannedMin: Math.round(item.plannedSeconds / 6) / 10 }));
|
|
677
|
+
const flushBoard = () => {
|
|
678
|
+
if (!pod.shardsFile) return;
|
|
679
|
+
try { writeGateShards(pod.shardsFile.path, { v: 1, version: pod.shardsFile.version, updatedAt: new Date().toISOString(), shards: board }); }
|
|
680
|
+
catch (error) { debug.log('release-loop.gate', 'shards-write-failed', { path: pod.shardsFile.path, error: String(error) }); }
|
|
681
|
+
};
|
|
682
|
+
const markShard = (shard: number, patch: Partial<GateShard>, clear: Array<keyof GateShard> = []) => {
|
|
683
|
+
const row = board[shard];
|
|
684
|
+
if (!row) return;
|
|
685
|
+
const next: GateShard = { ...row, ...patch };
|
|
686
|
+
for (const key of clear) delete next[key];
|
|
687
|
+
if (JSON.stringify(next) === JSON.stringify(row)) return;
|
|
688
|
+
board[shard] = next;
|
|
689
|
+
flushBoard();
|
|
690
|
+
};
|
|
691
|
+
flushBoard();
|
|
692
|
+
/** The gate's scheduler, seen through one shard: a slot wait and the Pod start become row states. */
|
|
693
|
+
// GATE-MEM-ADMIT: `placed` releases the shard's admission once a member is chosen — from then on the Job is
|
|
694
|
+
// Pending/Running in the cluster and the next admission reading counts it, so holding the lease would count it twice.
|
|
695
|
+
const trackedPool = (shard: number, retry: boolean, placed: () => void = () => {}): PodPoolScheduler => ({
|
|
696
|
+
members: poolScheduler.members,
|
|
697
|
+
tryAcquire: async (...args: Parameters<PodPoolScheduler['tryAcquire']>) => {
|
|
698
|
+
const member = await poolScheduler.tryAcquire(...args);
|
|
699
|
+
if (member) placed();
|
|
700
|
+
if (member) markShard(shard, { state: retry ? 'retry' : 'running', startedAt: board[shard]?.startedAt ?? new Date().toISOString() }, ['waitReason']);
|
|
701
|
+
else markShard(shard, { waitReason: 'Pod 자리 대기' });
|
|
702
|
+
return member;
|
|
703
|
+
},
|
|
704
|
+
release: (member: Parameters<PodPoolScheduler['release']>[0]) => poolScheduler.release(member),
|
|
705
|
+
}) as unknown as PodPoolScheduler;
|
|
472
706
|
const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false, fresh = pod.freshShards === true, parentAttempt?: string): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
473
|
-
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
474
|
-
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
475
707
|
// 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
|
|
476
|
-
|
|
477
|
-
|
|
708
|
+
// GATE-TIMEOUT-HEAL: parts (each with its own junit) under a soft deadline inside this shard's own Job deadline.
|
|
709
|
+
const shardDeadline = shardDeadlineSeconds(estimated(paths), deadlineSeconds);
|
|
710
|
+
const parts = shardParts(paths, (file) => estimated([file]));
|
|
478
711
|
const memoryLimit = paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? POD_ISOLATION_MEMORY_LIMIT : undefined;
|
|
479
|
-
const podShell =
|
|
480
|
-
const runKey = createHash('sha256').update(JSON.stringify({ pool: pod.pool, podShell, memoryLimit: memoryLimit ?? null, deadlineSeconds, bunCache: bunCache ?? null })).digest('hex');
|
|
481
|
-
|
|
712
|
+
const podShell = gateShardShell({ parts, cdpPatterns, softSeconds: softDeadlineSeconds(shardDeadline), cachePrefix, installSlots });
|
|
713
|
+
const runKey = createHash('sha256').update(JSON.stringify({ pool: pod.pool, podShell, memoryLimit: memoryLimit ?? null, deadlineSeconds: shardDeadline, bunCache: bunCache ?? null })).digest('hex');
|
|
714
|
+
let start = Date.now();
|
|
715
|
+
let admitted: GateAdmissionResult | undefined;
|
|
482
716
|
let output: string | undefined;
|
|
483
717
|
let junit: string | undefined;
|
|
484
718
|
let rc: number | undefined;
|
|
485
719
|
let jobExitCode: number | undefined;
|
|
486
720
|
let lastFile: string | undefined;
|
|
487
721
|
let jobFailed = false;
|
|
722
|
+
let timedOut = false;
|
|
723
|
+
let finished: string[] = [];
|
|
724
|
+
let unfinishedLog: string | undefined;
|
|
488
725
|
// The way down a previous attempt took wins over its own log: that log was not a verdict, or it would have no children.
|
|
489
726
|
// The split this shard would take if it failed here — the same rule as the split code below (isolation at depth 2 ·
|
|
490
727
|
// cap chunks for a big root shard · halves otherwise · none for one file).
|
|
@@ -512,9 +749,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
512
749
|
jobExitCode = 0;
|
|
513
750
|
lastFile = lastStartedTestFile(output, paths);
|
|
514
751
|
} else try {
|
|
752
|
+
admitted = await admitGateShard(gateAdmission, { waitMs: admissionWaitSeconds * 1000, onWait: (reason) => markShard(shard, { waitReason: reason }) });
|
|
753
|
+
if (admitted.outcome !== 'off') debug.log('release-loop.gate', 'shard-admission', { shard, branch, outcome: admitted.outcome, waitedMs: admitted.waitedMs, ...(admitted.reason ? { reason: admitted.reason } : {}) },
|
|
754
|
+
admitted.outcome === 'granted' ? undefined : { level: 'warn' });
|
|
755
|
+
// The admission wait is not the shard's run time: deadline/timeout judgment and the measured duration start here.
|
|
756
|
+
start = Date.now();
|
|
515
757
|
const job = await podCommand({
|
|
516
|
-
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
758
|
+
pool: pod.pool, poolScheduler: trackedPool(shard, depth > 0 || branch !== '', admitted.release), clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds: shardDeadline,
|
|
517
759
|
...(bunCache ? { bunCache } : {}),
|
|
760
|
+
...(installSlots > 0 ? { installSlots: POD_INSTALL_SLOTS_HOST_PATH } : {}),
|
|
518
761
|
...(pod.cpu ? { cpu: pod.cpu } : {}),
|
|
519
762
|
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
520
763
|
...(memoryLimit ? { memoryLimit } : {}),
|
|
@@ -522,14 +765,11 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
522
765
|
command: ['bash', '-lc', podShell],
|
|
523
766
|
});
|
|
524
767
|
jobExitCode = job.exitCode;
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const rcText = existsSync(rcPath) ? readFileSync(rcPath, 'utf8').trim() : '';
|
|
531
|
-
rc = /^\d+$/.test(rcText) && Number.isSafeInteger(Number(rcText)) ? Number(rcText) : undefined;
|
|
532
|
-
lastFile = output === undefined ? undefined : lastStartedTestFile(output, paths);
|
|
768
|
+
const read = readShardArtifacts(job.artifactsDir, parts);
|
|
769
|
+
({ output, junit, rc, timedOut, finished, unfinishedLog } = read);
|
|
770
|
+
// A Job the cluster cut at its own deadline leaves no outbox at all: past the deadline with nothing is a timeout too.
|
|
771
|
+
if (!timedOut && output === undefined && job.exitCode !== 0 && Date.now() - start >= shardDeadline * 1000) timedOut = true;
|
|
772
|
+
lastFile = output === undefined && unfinishedLog === undefined ? undefined : lastStartedTestFile(`${output ?? ''}\n${unfinishedLog ?? ''}`, paths);
|
|
533
773
|
if (!lastFile && (job.exitCode !== 0 || rc === undefined || (rc !== 0 && rc !== 1))) {
|
|
534
774
|
try { lastFile = lastStartedTestFile(jobLogTail(job).split(/\r?\n/).slice(-200).join('\n'), paths); }
|
|
535
775
|
catch { /* Log retrieval is diagnostic; do not hide the stalled shard. */ }
|
|
@@ -537,6 +777,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
537
777
|
} catch {
|
|
538
778
|
jobFailed = true;
|
|
539
779
|
}
|
|
780
|
+
// A launch that failed before placement still gives its admission back (release is idempotent).
|
|
781
|
+
admitted?.release();
|
|
540
782
|
const durationMs = Date.now() - start;
|
|
541
783
|
// A reused shard keeps its files as they are — rewriting would replace the measured durationMs the next release plans by.
|
|
542
784
|
if (logDir && !reused && !descent) {
|
|
@@ -550,9 +792,17 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
550
792
|
const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
|
|
551
793
|
if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
|
|
552
794
|
else rmSync(junitDestination, { force: true });
|
|
553
|
-
|
|
795
|
+
const unfinishedDestination = join(logDir, `pod-${shard}${branch}.unfinished.log`);
|
|
796
|
+
if (unfinishedLog !== undefined) writeFileSync(unfinishedDestination, unfinishedLog, { mode: 0o600 });
|
|
797
|
+
else rmSync(unfinishedDestination, { force: true });
|
|
798
|
+
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, ...(timedOut ? { reason: 'timeout', finishedFiles: finished.length } : {}), files: paths, plannedSeconds: estimated(paths), deadlineSeconds: shardDeadline, shardCount: shards.length, commit, runKey, attemptId, ...(parentAttempt ? { parentAttempt } : {}), jobExitCode: jobExitCode ?? null, jobFailed,
|
|
799
|
+
...(admitted && admitted.outcome !== 'off' ? { admission: admitted.outcome, admissionWaitMs: admitted.waitedMs } : {}) }) + '\n', { mode: 0o600 });
|
|
554
800
|
}
|
|
555
|
-
|
|
801
|
+
const installSeconds = parseInstallSeconds(output ?? '');
|
|
802
|
+
// GATE-TIMEOUT-HEAL ③: a cut shard is «reason: timeout» (with the files that still finished), not a bare rc=null.
|
|
803
|
+
if (!reused && !descent) debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds, deadlineSeconds: shardDeadline, parts: parts.length,
|
|
804
|
+
...(timedOut ? { reason: 'timeout', finishedFiles: finished.length } : {}) });
|
|
805
|
+
if (!reused && !descent) markShard(shard, { ...(installSeconds !== null ? { installSec: installSeconds } : {}), ...(rc !== undefined ? { rc } : {}), ...(timedOut ? { state: 'timeout' as const } : {}) });
|
|
556
806
|
let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
557
807
|
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
558
808
|
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
@@ -562,6 +812,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
562
812
|
let namedFailures: number | undefined;
|
|
563
813
|
let unattributedDetail: string | undefined;
|
|
564
814
|
if (descent === 'split') reason = 'incomplete';
|
|
815
|
+
else if (timedOut) reason = 'timeout';
|
|
565
816
|
else if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
|
|
566
817
|
else if (!clean || rc === undefined) reason = 'no-output';
|
|
567
818
|
else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
|
|
@@ -598,7 +849,12 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
598
849
|
debug.log('release-loop.gate', 'pod-shard-resume-miss', { shard, branch, reason });
|
|
599
850
|
return runShard(paths, shard, depth, branch, retriedNoOutput, true, parentAttempt);
|
|
600
851
|
}
|
|
601
|
-
|
|
852
|
+
// GATE-TIMEOUT-HEAL ④: a Pod that died before any test ran (no outbox · jobExitCode≠0 — 0.2.19: isolated retries
|
|
853
|
+
// ending with exit 5, the source checkout) gets the same one fresh retry as «no output» instead of stalling the sweep.
|
|
854
|
+
// 137 (killed · OOM) keeps the isolation path: re-running an OOM file only re-measures the same kill.
|
|
855
|
+
const diedBeforeTests = reason === 'job-failed' && output === undefined && !jobFailed && jobExitCode !== undefined && jobExitCode !== 0
|
|
856
|
+
&& jobExitCode !== 137 && (paths.length === 1 || jobExitCode === 5);
|
|
857
|
+
if ((reason === 'no-output' || diedBeforeTests) && !retriedNoOutput) {
|
|
602
858
|
const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId);
|
|
603
859
|
debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
|
|
604
860
|
return retry;
|
|
@@ -675,7 +931,17 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
675
931
|
ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
676
932
|
};
|
|
677
933
|
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
678
|
-
const settled = await Promise.allSettled(shards.map((item, shard) =>
|
|
934
|
+
const settled = await Promise.allSettled(shards.map(async (item, shard) => {
|
|
935
|
+
try {
|
|
936
|
+
const done = await runShard(item.files, shard);
|
|
937
|
+
const state = !done.stalled.length ? 'done' as const : done.stalled.some((stall) => stall.reason === 'timeout') ? 'timeout' as const : 'failed' as const;
|
|
938
|
+
markShard(shard, { state, endedAt: new Date().toISOString() }, ['waitReason']);
|
|
939
|
+
return done;
|
|
940
|
+
} catch (error) {
|
|
941
|
+
markShard(shard, { state: 'failed', endedAt: new Date().toISOString() }, ['waitReason']);
|
|
942
|
+
throw error;
|
|
943
|
+
}
|
|
944
|
+
}));
|
|
679
945
|
const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
680
946
|
if (rejected) throw rejected.reason;
|
|
681
947
|
const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
@@ -949,7 +1215,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
949
1215
|
} catch (error) { debug.log('release-loop.gate', 'known-failures-unavailable', { error: String(error) }); }
|
|
950
1216
|
}
|
|
951
1217
|
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', partial ? 'cut-partial' : 'cut'),
|
|
952
|
-
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), ...(knownFailures.length ? { knownFailures } : {})
|
|
1218
|
+
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), ...(knownFailures.length ? { knownFailures } : {}),
|
|
1219
|
+
// GATE-LIVE-OBS: the release ledger's shards.json (UX #24782) — never the run tree's universe; an explicit
|
|
1220
|
+
// --ledger-root (tests) keeps it in that ledger.
|
|
1221
|
+
shardsFile: { path: opts.ledgerRoot ? gateShardsPath(opts.version, opts.ledgerRoot) : gateShardsPath(opts.version), version: opts.version } } : undefined,
|
|
953
1222
|
partial?.files);
|
|
954
1223
|
const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
|
|
955
1224
|
const stalled = run.stalledEnv ?? [];
|
|
@@ -1219,6 +1488,8 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
|
|
|
1219
1488
|
...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
|
|
1220
1489
|
...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
|
|
1221
1490
|
...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
|
|
1491
|
+
...(fromGraph.gatePodInstallSlots !== undefined ? { installSlots: Number(fromGraph.gatePodInstallSlots) } : {}),
|
|
1492
|
+
...(fromGraph.gatePodAdmissionWaitSeconds !== undefined ? { admissionWaitSeconds: Number(fromGraph.gatePodAdmissionWaitSeconds) } : {}),
|
|
1222
1493
|
...(fromGraph.gatePodFreshShards === true ? { freshShards: true } : {}),
|
|
1223
1494
|
...((cpu) => cpu ? { cpu } : {})(parsePodCpu(fromGraph.gatePodCpu, 'release.loop.gatePodCpu')),
|
|
1224
1495
|
} : undefined,
|