elanous 0.2.17 → 0.2.19
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/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.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 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- 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 +25 -24
- 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 +24 -23
- 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 +28 -27
- 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/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +6 -3
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/lib/pch-regress-map.ts +23 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +436 -59
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +120 -0
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +174 -0
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +45 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +255 -0
- package/src/briefing/brief-items.ts +86 -17
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +85 -7
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +128 -19
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +403 -4
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +244 -40
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/graph-runner/runner.ts +25 -5
- package/src/harness/harness-cli-command.ts +12 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/harness-salvage-cli.ts +78 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/hq.ts +6 -1
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +185 -18
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +106 -4
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +450 -54
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/http-server.ts +16 -5
- package/src/nexus/api/loop-edges.ts +352 -23
- package/src/nexus/api/ops-api.ts +96 -28
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/nexus/index.ts +7 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +106 -6
- package/src/release-loop/feature-store.ts +64 -16
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +64 -20
- package/src/release-loop/rubric.ts +136 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +179 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +61 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +101 -7
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +203 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +49 -5
- package/src/task-agent/live-moves.ts +176 -0
- package/src/task-agent/mission.ts +449 -0
- package/src/task-agent/shadow.ts +258 -0
- package/src/task-agent/task-hand.ts +337 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
- package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +88 -20
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +11 -0
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/telegram.ts +5 -20
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +196 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import { registerStandaloneLogSink } from '../../src/domains/standalone-log-sink.js';
|
|
8
9
|
import { landingFreezeMessage, readLandingFreeze } from '../../src/release-loop/landing-freeze.js';
|
|
9
10
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
10
11
|
import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
11
|
-
import { diffFailures, junitFailures, parseFailures } from './gate-diff';
|
|
12
|
+
import { diffFailures, junitFailures, parseFailures, parseTimedOutFailures } from './gate-diff';
|
|
12
13
|
import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
|
|
13
14
|
import { baselineFromCutLogs } from './baseline-from-logs';
|
|
14
|
-
import {
|
|
15
|
+
import { mergePartialFailures, readPartialPlan, testFileOfId } from './gate-partial';
|
|
16
|
+
import { logPlanWalltime, planDurations, planShards, readFileMemory } from './shard-plan';
|
|
15
17
|
import { deriveCdpTestPatterns } from '../test-deterministic';
|
|
16
18
|
import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
17
|
-
import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
19
|
+
import { runPodCommand, parsePodCpu, resolvePodCpu, type RunPodCommandOptions, type PodCommandResult, type PodCpu } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
18
20
|
import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
|
|
19
21
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
20
22
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
@@ -23,7 +25,8 @@ interface CommandResult { rc: number; output: string; passedIds?: string[]; stal
|
|
|
23
25
|
export interface GateRunner {
|
|
24
26
|
command(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
|
|
25
27
|
localCommand(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
|
|
26
|
-
|
|
28
|
+
/** `only` (GATE-PARTIAL) restricts the sweep to these test files; absent = every test file. */
|
|
29
|
+
sweep(tree: string, logDir?: string, pod?: GateOptions['pod'], only?: readonly string[]): Promise<CommandResult>;
|
|
27
30
|
add(tree: string, commit: string): Promise<void>;
|
|
28
31
|
remove(tree: string): Promise<void>;
|
|
29
32
|
snapshot(tree: string, commit: string): Promise<void>;
|
|
@@ -42,22 +45,38 @@ export interface GateOptions {
|
|
|
42
45
|
instanceRoot?: string;
|
|
43
46
|
ledgerRoot?: string;
|
|
44
47
|
repo?: string;
|
|
45
|
-
|
|
48
|
+
/** freshShards — GATE-SHARD-RESUME off: every shard starts a Pod even when this logDir already holds its result. */
|
|
49
|
+
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string; freshShards?: boolean;
|
|
50
|
+
/** GATE-SKIP-KNOWN-RETRY — failure ids the previous release already had (and env-known ones): a shard that fails only
|
|
51
|
+
* with these is not re-run in isolation; the verdict still classifies them against the baseline as before. */
|
|
52
|
+
knownFailures?: readonly string[];
|
|
53
|
+
/** GATE-SPEED A3① — shard Pod CPU request/limit (`release.loop.gatePodCpu`); omitted = request 1 / limit 4. */
|
|
54
|
+
cpu?: PodCpu };
|
|
46
55
|
}
|
|
47
56
|
export interface GateResult {
|
|
48
57
|
outcome: 'ok' | 'regression' | 'error';
|
|
49
58
|
commit: string;
|
|
50
59
|
introduced: string[];
|
|
51
60
|
preexisting: number;
|
|
52
|
-
|
|
61
|
+
/** null = not counted (GATE-SPEED A4: the swept baseline ran only the cut's failing files). */
|
|
62
|
+
fixed: number | null;
|
|
53
63
|
knownEnv: number;
|
|
54
64
|
knownEnvCleared: string[];
|
|
55
65
|
stalledEnv: Array<{ file: string; reason: 'no-output' | 'isolated-timeout'; local: 'passed' | 'failed' | 'timeout' }>;
|
|
56
66
|
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
67
|
+
/** GATE-BASELINE-CACHE — isolated baseline files answered from the ledger (hits) or run now (misses). */
|
|
68
|
+
baselineCache?: { hits: number; misses: number };
|
|
69
|
+
/** GATE-SPEED A4 — a swept baseline ran only the cut's failing files that exist at the previous release. */
|
|
70
|
+
baselineDeferred?: { cutFiles: number; swept: number };
|
|
57
71
|
durationMs: number;
|
|
58
72
|
error?: string;
|
|
59
73
|
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
60
74
|
partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
|
|
75
|
+
/** GATE-PARTIAL — this verdict re-ran only these files and carried the prior gate's results for the rest. */
|
|
76
|
+
partial?: { priorCommit: string; files: number };
|
|
77
|
+
/** GATE-INTRO-RECHECK — new failures that were only the «timed out» shape and passed one isolated re-run with a
|
|
78
|
+
* {@link GATE_TIMEOUT_RECHECK_MS} per-test limit: a warning (flaky under load), not an introduced regression. */
|
|
79
|
+
loadFlaky?: string[];
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
const sha = /^[0-9a-f]{7,40}$/i;
|
|
@@ -114,6 +133,65 @@ const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
|
|
|
114
133
|
const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
|
|
115
134
|
/** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
|
|
116
135
|
const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
|
|
136
|
+
/** A Pod path (`~/repo/src/…` · `../..~/repo/src/…`) as a repository-relative one. */
|
|
137
|
+
const podRelative = (path: string) => path.replace(/^(?:\.\.\/)*\/?home\/ubuntu\/repo\//, '');
|
|
138
|
+
/** `file > [error]` ids of bun's «Unhandled error between tests» blocks, or undefined when one cannot be attributed. */
|
|
139
|
+
function unhandledErrorIds(output: string): string[] | undefined {
|
|
140
|
+
const errors: string[] = [];
|
|
141
|
+
let file: string | undefined;
|
|
142
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
143
|
+
const header = /^(?:\.\/)?((?:[\w.-]+\/)+[\w.-]+\.test\.tsx?):/.exec(raw.trim());
|
|
144
|
+
if (header) file = header[1];
|
|
145
|
+
if (/^# Unhandled error between tests/.test(raw.trim())) {
|
|
146
|
+
if (!file) return undefined;
|
|
147
|
+
errors.push(`${file} > [error]`);
|
|
148
|
+
file = undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return errors;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* GATE-SKIP-KNOWN-RETRY / GATE-ISOLATE-FAILED-FILES — the failing ids of one Pod shard, but only when every failure the
|
|
155
|
+
* summary counts is named (junit first, else the console `(fail)` lines) and every unhandled error is attributed.
|
|
156
|
+
* Anything less returns undefined, and the shard keeps the old retry path.
|
|
157
|
+
*/
|
|
158
|
+
export function shardFailureIds(clean: string, junit: string | undefined, fails: number, errors: number): { failures: string[]; errors: string[] } | undefined {
|
|
159
|
+
if (!Number.isFinite(fails)) return undefined;
|
|
160
|
+
const stripped = stripPodRepoRoot(clean);
|
|
161
|
+
let failures: string[];
|
|
162
|
+
try { failures = junit !== undefined ? [...new Set(junitFailures(junit).map(podRelative))].sort() : parseFailures(stripped); }
|
|
163
|
+
catch { return undefined; }
|
|
164
|
+
if (failures.length !== fails) return undefined;
|
|
165
|
+
const errorIds = unhandledErrorIds(stripped);
|
|
166
|
+
if (!errorIds || errorIds.length !== (Number.isNaN(errors) ? 0 : errors)) return undefined;
|
|
167
|
+
return { failures, errors: [...new Set(errorIds)].sort() };
|
|
168
|
+
}
|
|
169
|
+
/** The console form `failuresOf` reads back, for failures named from junit (same shape as `pod-shard-junit-attribution`). */
|
|
170
|
+
const failureOutput = (failures: readonly string[], errors: readonly string[]) => [
|
|
171
|
+
...failures.map((id) => { const cut = id.indexOf(' > '); return `${id.slice(0, cut)}:\n(fail) ${id.slice(cut + 3)}`; }),
|
|
172
|
+
...errors.map((id) => `${testFileOfId(id)}:\n# Unhandled error between tests`),
|
|
173
|
+
].join('\n');
|
|
174
|
+
/** Repository-relative test files with at least one passed or failed junit testcase (an empty suite is no result). */
|
|
175
|
+
function junitFiles(xml: string): Set<string> {
|
|
176
|
+
const files = new Set<string>();
|
|
177
|
+
for (const id of [...junitPassedIds(xml), ...junitFailures(xml)]) {
|
|
178
|
+
try { files.add(testFileOfId(podRelative(id))); } catch { /* an unreadable id proves nothing about its file */ }
|
|
179
|
+
}
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* GATE-ISOLATE-FAILED-FILES — the files of a failed shard worth re-running: those with a failing test or an unhandled
|
|
184
|
+
* error, and those with no junit result at all (crashed or never reached). undefined = cannot tell (fallback: whole shard).
|
|
185
|
+
*/
|
|
186
|
+
export function retryFiles(paths: readonly string[], clean: string | undefined, junit: string | undefined, fails: number, errors: number): string[] | undefined {
|
|
187
|
+
if (junit === undefined || clean === undefined) return undefined;
|
|
188
|
+
const ids = shardFailureIds(clean, junit, fails, errors);
|
|
189
|
+
if (!ids) return undefined;
|
|
190
|
+
const failing = new Set([...ids.failures, ...ids.errors].map((id) => { try { return testFileOfId(id); } catch { return id; } }));
|
|
191
|
+
if ([...failing].some((file) => !paths.includes(file))) return undefined;
|
|
192
|
+
const reported = junitFiles(junit);
|
|
193
|
+
return paths.filter((file) => failing.has(file) || !reported.has(file));
|
|
194
|
+
}
|
|
117
195
|
export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
|
|
118
196
|
/** GT1b — a file whose measured peak is at least this runs alone at the isolation limit from the start: one such file
|
|
119
197
|
* sank a whole 203-file root shard at 16Gi (0.2.6 shard 12 · `unwired-exports` 9.9 GB) and cost two halving rounds. */
|
|
@@ -148,8 +226,64 @@ const lastStartedTestFile = (output: string, paths: string[]): string | undefine
|
|
|
148
226
|
return last;
|
|
149
227
|
};
|
|
150
228
|
|
|
229
|
+
/**
|
|
230
|
+
* GATE-SHARD-RESUME (0.2.19) — the result a previous attempt left in this log directory for exactly this shard: same name,
|
|
231
|
+
* same commit, the same files in the same order and a test exit (rc 0|1) with its log. 0.2.18: the second gate stopped
|
|
232
|
+
* after 33 minutes on a tool error (unsafe test path) and the third ran all 80 minutes again although the cut commit and
|
|
233
|
+
* every finished shard were unchanged. The caller re-reads the log with the current tool code; anything else is a miss.
|
|
234
|
+
*/
|
|
235
|
+
/** GATE-SHARD-RESUME — how a previous attempt went past this shard without a verdict of its own: it re-ran in a fresh Job
|
|
236
|
+
* after no output (`-retry`), or it failed here and was split (`-c0` cap chunks · `-0` halves · `-file-0` isolation). The
|
|
237
|
+
* split shape follows from depth and size alone, so the same plan reaches the same child names (TC 09:30 review ②).
|
|
238
|
+
* `runKey` hashes where and how the Pod ran the shard (pool · command · ignores · memory · deadline · Bun cache): a fix on the Pod side
|
|
239
|
+
* makes every old result a miss instead of re-reading an old failure as introduced (TC 09:48 review). */
|
|
240
|
+
export function previousShardDescent(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string, splitChild?: string): { way: 'retry' | 'split'; attemptId: string } | undefined {
|
|
241
|
+
const meta = (file: string): { commit?: unknown; files?: unknown; rc?: unknown; runKey?: unknown; attemptId?: unknown; parentAttempt?: unknown } | undefined => {
|
|
242
|
+
try { return JSON.parse(readFileSync(join(logDir, `${file}.json`), 'utf8')); } catch { return undefined; }
|
|
243
|
+
};
|
|
244
|
+
const own = meta(name);
|
|
245
|
+
if (own?.commit !== commit || own.runKey !== runKey || !Array.isArray(own.files) || own.files.length !== files.length || own.files.some((file, index) => file !== files[index])) return undefined;
|
|
246
|
+
if (typeof own.attemptId !== 'string' || (parentAttempt !== undefined && own.parentAttempt !== parentAttempt)) return undefined;
|
|
247
|
+
const attemptId = own.attemptId;
|
|
248
|
+
// Children are checked whatever the parent's rc was: a parent can exit 0|1 and still have gone down (no output · an
|
|
249
|
+
// unreadable summary). A child counts only for this commit and only with files taken from this shard.
|
|
250
|
+
const parent = new Set(files);
|
|
251
|
+
const child = (file: string, whole: boolean) => {
|
|
252
|
+
const row = meta(file);
|
|
253
|
+
// A child counts only when the parent's latest attempt wrote it — a child left by an older attempt is not this way down.
|
|
254
|
+
return row?.commit === commit && row.parentAttempt === attemptId && Array.isArray(row.files) && row.files.length > 0 && row.files.every((path) => typeof path === 'string' && parent.has(path))
|
|
255
|
+
&& (!whole || row.files.length === files.length);
|
|
256
|
+
};
|
|
257
|
+
if (child(`${name}-retry`, true)) return { way: 'retry', attemptId };
|
|
258
|
+
// Only the first child of the split the caller takes now (`-file-0` · `-c0` · `-0` · review r3): a record of another
|
|
259
|
+
// split shape is not followed.
|
|
260
|
+
return splitChild !== undefined && child(`${name}${splitChild}`, false) ? { way: 'split', attemptId } : undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function previousShardResult(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string): { output: string; junit?: string; rc: 0 | 1; durationMs?: number } | undefined {
|
|
264
|
+
try {
|
|
265
|
+
const meta = JSON.parse(readFileSync(join(logDir, `${name}.json`), 'utf8')) as { commit?: unknown; files?: unknown; rc?: unknown; durationMs?: unknown; runKey?: unknown; parentAttempt?: unknown; jobExitCode?: unknown; jobFailed?: unknown };
|
|
266
|
+
// Only a Job that itself exited 0 — a crashed Job can still leave rc 0 and a clean-looking log (review r6).
|
|
267
|
+
if (meta.jobExitCode !== 0 || meta.jobFailed !== false) return undefined;
|
|
268
|
+
if (meta.commit !== commit || meta.runKey !== runKey || (parentAttempt !== undefined && meta.parentAttempt !== parentAttempt) || (meta.rc !== 0 && meta.rc !== 1) || !Array.isArray(meta.files)
|
|
269
|
+
|| meta.files.length !== files.length || meta.files.some((file, index) => file !== files[index])) return undefined;
|
|
270
|
+
const logPath = join(logDir, `${name}.log`);
|
|
271
|
+
if (!existsSync(logPath)) return undefined;
|
|
272
|
+
const junitPath = join(logDir, `${name}.junit.xml`);
|
|
273
|
+
return { output: readFileSync(logPath, 'utf8'), rc: meta.rc, ...(existsSync(junitPath) ? { junit: readFileSync(junitPath, 'utf8') } : {}),
|
|
274
|
+
...(typeof meta.durationMs === 'number' ? { durationMs: meta.durationMs } : {}) };
|
|
275
|
+
} catch { return undefined; }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 0.2.18 gate: a test that changed process.cwd made bun print later test files relative to that cwd
|
|
279
|
+
* (`../..~/repo/src/…`), and the safe-path check stopped the whole gate. The Pod clone root is
|
|
280
|
+
* `~/repo`; strip it back to a repository-relative path before any id is read. */
|
|
281
|
+
export function stripPodRepoRoot(output: string): string {
|
|
282
|
+
return output.replace(/^(\s*(?:\(fail\)\s+)?)(?:\.\.\/)*(?:\/)?home\/ubuntu\/repo\//gm, '$1');
|
|
283
|
+
}
|
|
284
|
+
|
|
151
285
|
function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
152
|
-
const output = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
286
|
+
const output = stripPodRepoRoot(run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''));
|
|
153
287
|
const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
|
|
154
288
|
const reportedErrors = summaryCount(output, 'error');
|
|
155
289
|
const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
|
|
@@ -191,6 +325,11 @@ class StalledPodShards extends Error {
|
|
|
191
325
|
|
|
192
326
|
/** Isolated re-runs (host stall re-run · cut and baseline re-checks) are time-limited; sweeps and installs keep their full time. */
|
|
193
327
|
export const GATE_ISOLATED_TIMEOUT_MS = 300_000;
|
|
328
|
+
/** GATE-INTRO-RECHECK (0.2.19) — the per-test limit of the one re-run given to a «timed out after 5000ms» new failure.
|
|
329
|
+
* 0.2.18: both first-gate introductions were 5 s limits missed at 6.04/6.06 s, and they alone cost an 84-minute re-gate. */
|
|
330
|
+
export const GATE_TIMEOUT_RECHECK_MS = 60_000;
|
|
331
|
+
/** TEST-CENSUS intake: one row per load-flaky test, in the machine ledger (read by the test census, never by the gate). */
|
|
332
|
+
export const loadFlakyCensusPath = (ledger: string) => join(ledger, 'release', 'test-census', 'load-flaky.jsonl');
|
|
194
333
|
|
|
195
334
|
/**
|
|
196
335
|
* Runs a limited local command in its own process group. On timeout: SIGTERM the group (test-deterministic forwards it to its own
|
|
@@ -250,7 +389,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
250
389
|
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}`, ...(timedOut ? { timedOut } : {}) };
|
|
251
390
|
}
|
|
252
391
|
: localCommand);
|
|
253
|
-
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']
|
|
392
|
+
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>, only?: readonly string[]): Promise<CommandResult> => {
|
|
254
393
|
const shardCount = pod.shards ?? 24;
|
|
255
394
|
const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
|
|
256
395
|
if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
|
|
@@ -261,7 +400,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
261
400
|
if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('invalid cut tree HEAD');
|
|
262
401
|
const listed = await command('git', ['ls-files', '*.test.*'], tree);
|
|
263
402
|
check(listed, 'git ls-files tests');
|
|
264
|
-
const
|
|
403
|
+
const allFiles = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
|
|
404
|
+
const onlySet = only ? new Set(only) : undefined;
|
|
405
|
+
// GATE-PARTIAL: every planned file must exist at this commit — a deleted/renamed failing file would otherwise «pass»
|
|
406
|
+
// by not running. Fail closed (gate error), never a smaller sweep than planned.
|
|
407
|
+
const missing = only?.filter((file) => !allFiles.includes(file)) ?? [];
|
|
408
|
+
if (missing.length) throw new Error(`partial plan file missing at this commit: ${missing.slice(0, 5).join(', ')}`);
|
|
409
|
+
const files = onlySet ? allFiles.filter((file) => onlySet.has(file)) : allFiles;
|
|
265
410
|
if (files.some((file) => file.startsWith('-') || file.startsWith('/') || file.startsWith('./')
|
|
266
411
|
|| file.split('/').some((part) => !part || part === '.' || part === '..') || /[\r\n]/.test(file))) throw new Error('unsafe pod test path');
|
|
267
412
|
if (new Set(files).size !== files.length) throw new Error('duplicate pod test path');
|
|
@@ -276,7 +421,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
276
421
|
const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
|
|
277
422
|
if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
|
|
278
423
|
if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
|
|
279
|
-
const durations
|
|
424
|
+
const { durations, wall: planWall } = planDurations(pod.durationSource);
|
|
280
425
|
const memory = readFileMemory(join(tree, POD_MEMORY_SOURCE));
|
|
281
426
|
// The TD1 measurements predate the fixture-only gate; do not isolate those five files at their old 2–13 GB peaks.
|
|
282
427
|
const fixtureOnly = new Set([...GATE_NIGHTLY_AUDITS, 'test/guardian/dispatch-surface-contract.test.ts', 'test/user-config-mcp.test.ts']);
|
|
@@ -286,6 +431,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
286
431
|
...(light.length ? planShards(light, durations, shardCount) : []),
|
|
287
432
|
...[...heavy].map((file) => ({ files: [file], plannedSeconds: durations.get(file) ?? 0 })),
|
|
288
433
|
];
|
|
434
|
+
logPlanWalltime(planWall, shards);
|
|
289
435
|
if (heavy.size) debug.log('release-loop.gate', 'pod-heavy-alone', { files: [...heavy], thresholdMb: POD_HEAVY_FILE_MB, memoryLimit: POD_ISOLATION_MEMORY_LIMIT });
|
|
290
436
|
const known = assignable.filter((file) => durations.has(file)).length;
|
|
291
437
|
debug.log('release-loop.gate', 'pod-shard-plan', {
|
|
@@ -293,6 +439,9 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
293
439
|
maxPlannedSeconds: Math.max(...shards.map((shard) => shard.plannedSeconds)),
|
|
294
440
|
source: durations.size ? pod.durationSource : 'none',
|
|
295
441
|
});
|
|
442
|
+
// GATE-SPEED A3①: shard Pod CPU (`release.loop.gatePodCpu` · default request 1 / limit 4) — shards × request must fit the pool.
|
|
443
|
+
const cpu = resolvePodCpu(pod.cpu);
|
|
444
|
+
debug.log('release-loop.gate', 'shard-resources', { shards: shards.length, cpuRequest: cpu.request, cpuLimit: cpu.limit, cpuSource: pod.cpu ? 'config' : 'default' });
|
|
296
445
|
const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
|
|
297
446
|
if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
|
|
298
447
|
const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
|
|
@@ -313,18 +462,22 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
313
462
|
type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
|
|
314
463
|
// 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
|
|
315
464
|
const isolationRemaining = shards.map((item) => item.files.length);
|
|
465
|
+
const knownFailures = new Set(pod.knownFailures ?? []);
|
|
316
466
|
const estimated = (paths: string[]) => paths.reduce((sum, file) => {
|
|
317
467
|
const value = durations.get(file);
|
|
318
468
|
return sum + (value !== undefined && Number.isFinite(value) && value >= 0 ? value : unknownSeconds);
|
|
319
469
|
}, 0);
|
|
320
470
|
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);
|
|
321
471
|
const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
|
|
322
|
-
const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
472
|
+
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']> }> => {
|
|
323
473
|
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
324
474
|
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
325
475
|
// 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
|
|
326
476
|
const args = ['bun', 'run', 'test:deterministic', ...ignores, ...paths.map(asPath)].map(quote).join(' ')
|
|
327
477
|
+ ' --reporter=junit --reporter-outfile="$HOME/outbox/junit.xml"';
|
|
478
|
+
const memoryLimit = paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? POD_ISOLATION_MEMORY_LIMIT : undefined;
|
|
479
|
+
const podShell = `mkdir -p "$HOME/outbox"; ${cachePrefix}(cd .. && cd repo && bun install && (cd apps/pwa && bun install) && ${args}) 2>&1 | tee "$HOME/outbox/shard.log"; echo \${PIPESTATUS[0]} > "$HOME/outbox/shard.rc"`;
|
|
480
|
+
const runKey = createHash('sha256').update(JSON.stringify({ pool: pod.pool, podShell, memoryLimit: memoryLimit ?? null, deadlineSeconds, bunCache: bunCache ?? null })).digest('hex');
|
|
328
481
|
const start = Date.now();
|
|
329
482
|
let output: string | undefined;
|
|
330
483
|
let junit: string | undefined;
|
|
@@ -332,14 +485,41 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
332
485
|
let jobExitCode: number | undefined;
|
|
333
486
|
let lastFile: string | undefined;
|
|
334
487
|
let jobFailed = false;
|
|
335
|
-
|
|
488
|
+
// 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
|
+
// The split this shard would take if it failed here — the same rule as the split code below (isolation at depth 2 ·
|
|
490
|
+
// cap chunks for a big root shard · halves otherwise · none for one file).
|
|
491
|
+
// Isolation follows only while this shard's isolation budget still covers every file (review r7) — the budget is the
|
|
492
|
+
// shard's file count and isolations take disjoint files, so this holds unless the plan changed; if not, run the Pod.
|
|
493
|
+
const splitChild = depth === 2 && paths.length > 1 ? (isolationRemaining[shard]! >= paths.length ? '-file-0' : undefined)
|
|
494
|
+
: depth < 2 && paths.length > 1 ? (depth === 0 && paths.length > POD_SHARD_FILE_CAP ? '-c0' : '-0') : undefined;
|
|
495
|
+
const found = !fresh && logDir ? previousShardDescent(logDir, `pod-${shard}${branch}`, commit, paths, runKey, parentAttempt, splitChild) : undefined;
|
|
496
|
+
const descent = found?.way;
|
|
497
|
+
const reused = !fresh && !descent && logDir ? previousShardResult(logDir, `pod-${shard}${branch}`, commit, paths, runKey, parentAttempt) : undefined;
|
|
498
|
+
// This shard's attempt id: the recorded one when its way down is followed, a new one when a Pod runs. Children carry it.
|
|
499
|
+
const attemptId = found?.attemptId ?? randomUUID();
|
|
500
|
+
if (descent === 'retry') {
|
|
501
|
+
// «descend» is only the way down; «resumed» comes from each leaf whose log reads cleanly (review r4).
|
|
502
|
+
debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'retry' });
|
|
503
|
+
return runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId);
|
|
504
|
+
}
|
|
505
|
+
if (descent === 'split') {
|
|
506
|
+
output = undefined;
|
|
507
|
+
debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'split' });
|
|
508
|
+
} else if (reused) {
|
|
509
|
+
output = reused.output;
|
|
510
|
+
junit = reused.junit;
|
|
511
|
+
rc = reused.rc;
|
|
512
|
+
jobExitCode = 0;
|
|
513
|
+
lastFile = lastStartedTestFile(output, paths);
|
|
514
|
+
} else try {
|
|
336
515
|
const job = await podCommand({
|
|
337
516
|
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
338
517
|
...(bunCache ? { bunCache } : {}),
|
|
518
|
+
...(pod.cpu ? { cpu: pod.cpu } : {}),
|
|
339
519
|
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
340
|
-
...(
|
|
520
|
+
...(memoryLimit ? { memoryLimit } : {}),
|
|
341
521
|
name: `gate-${randomUUID()}`,
|
|
342
|
-
command: ['bash', '-lc',
|
|
522
|
+
command: ['bash', '-lc', podShell],
|
|
343
523
|
});
|
|
344
524
|
jobExitCode = job.exitCode;
|
|
345
525
|
const logPath = join(job.artifactsDir, 'shard.log');
|
|
@@ -358,17 +538,21 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
358
538
|
jobFailed = true;
|
|
359
539
|
}
|
|
360
540
|
const durationMs = Date.now() - start;
|
|
361
|
-
|
|
541
|
+
// A reused shard keeps its files as they are — rewriting would replace the measured durationMs the next release plans by.
|
|
542
|
+
if (logDir && !reused && !descent) {
|
|
362
543
|
mkdirSync(logDir, { recursive: true });
|
|
544
|
+
// GATE-SHARD-RESUME: the json is what makes a log reusable, so it goes first and comes back last — a stop between
|
|
545
|
+
// the new log and the new json leaves no json, never the old Job's exit paired with the new log (review r8).
|
|
546
|
+
rmSync(join(logDir, `pod-${shard}${branch}.json`), { force: true });
|
|
363
547
|
const destination = join(logDir, `pod-${shard}${branch}.log`);
|
|
364
548
|
if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
|
|
365
549
|
else rmSync(destination, { force: true });
|
|
366
|
-
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths), shardCount: shards.length, commit }) + '\n', { mode: 0o600 });
|
|
367
550
|
const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
|
|
368
551
|
if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
|
|
369
552
|
else rmSync(junitDestination, { force: true });
|
|
553
|
+
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths), shardCount: shards.length, commit, runKey, attemptId, ...(parentAttempt ? { parentAttempt } : {}), jobExitCode: jobExitCode ?? null, jobFailed }) + '\n', { mode: 0o600 });
|
|
370
554
|
}
|
|
371
|
-
debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
|
|
555
|
+
if (!reused && !descent) debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
|
|
372
556
|
let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
373
557
|
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
374
558
|
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
@@ -377,7 +561,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
377
561
|
let reason: ShardReason | undefined;
|
|
378
562
|
let namedFailures: number | undefined;
|
|
379
563
|
let unattributedDetail: string | undefined;
|
|
380
|
-
if (
|
|
564
|
+
if (descent === 'split') reason = 'incomplete';
|
|
565
|
+
else if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
|
|
381
566
|
else if (!clean || rc === undefined) reason = 'no-output';
|
|
382
567
|
else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
|
|
383
568
|
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length
|
|
@@ -408,19 +593,51 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
408
593
|
}
|
|
409
594
|
}
|
|
410
595
|
}
|
|
596
|
+
// A reused result that the current tool cannot read cleanly is not a verdict: run that shard for real.
|
|
597
|
+
if (reused && reason) {
|
|
598
|
+
debug.log('release-loop.gate', 'pod-shard-resume-miss', { shard, branch, reason });
|
|
599
|
+
return runShard(paths, shard, depth, branch, retriedNoOutput, true, parentAttempt);
|
|
600
|
+
}
|
|
411
601
|
if (reason === 'no-output' && !retriedNoOutput) {
|
|
412
|
-
const retry = await runShard(paths, shard, depth, `${branch}-retry`, true);
|
|
602
|
+
const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId);
|
|
413
603
|
debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
|
|
414
604
|
return retry;
|
|
415
605
|
}
|
|
416
606
|
if (reason) {
|
|
607
|
+
// GATE-SKIP-KNOWN-RETRY — a shard whose summary is complete but whose failures could not be read back (unattributed)
|
|
608
|
+
// and whose every named failure is one the previous release already had is not re-run: re-running only re-measures
|
|
609
|
+
// known failures (0.2.18 cut: 109 retry Pods · 1,840 shard-minutes, 4x the first pass). Those failures go to the
|
|
610
|
+
// verdict as they are, so it still classifies them against the baseline (preexisting) — never silently dropped.
|
|
611
|
+
if (reason === 'unattributed' && knownFailures.size && clean && ran) {
|
|
612
|
+
const ids = shardFailureIds(clean, junit, fails, errors);
|
|
613
|
+
const all = ids ? [...ids.failures, ...ids.errors] : [];
|
|
614
|
+
if (ids && all.length && all.every((id) => knownFailures.has(id))) {
|
|
615
|
+
debug.log('release-loop.gate', 'retry-skipped-known', { shard, failures: all.length });
|
|
616
|
+
return { runs: [{ output: `${failureOutput(ids.failures, ids.errors)}\n`, rc: 1, pass: Number.isFinite(passes) ? passes : 0,
|
|
617
|
+
fail: ids.failures.length, errors: ids.errors.length, ran: Number(ran[1]), files: Number(ran[2]),
|
|
618
|
+
passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// GATE-ISOLATE-FAILED-FILES — with this shard's junit, only the files with a failing test or an unhandled error and
|
|
622
|
+
// the files with no junit result (crashed · never reached) are re-run; the rest passed here and keep that result.
|
|
623
|
+
// The narrowed set runs under its own branch (`-n`) at the same depth, so the usual split/isolation rules still apply
|
|
624
|
+
// to it, and a resumed gate finds this shard's own log (not a `-c0`/`-0` child) and narrows the same way again.
|
|
625
|
+
const narrowed = !descent ? retryFiles(paths, clean, junit, fails, errors) : undefined;
|
|
626
|
+
if (narrowed && narrowed.length && narrowed.length < paths.length) {
|
|
627
|
+
debug.log('release-loop.gate', 'retry-narrowed', { shard, before: paths.length, after: narrowed.length });
|
|
628
|
+
const kept = new Set(paths.filter((file) => !narrowed.includes(file)));
|
|
629
|
+
const passedIds = junitPassedIds(junit!).map(podRelative).filter((id) => { try { return kept.has(testFileOfId(id)); } catch { return false; } });
|
|
630
|
+
const child = await runShard(narrowed, shard, depth, `${branch}-n`, false, undefined, attemptId);
|
|
631
|
+
return { runs: [{ output: '', rc: 0, pass: passedIds.length, fail: 0, errors: 0, ran: passedIds.length, files: kept.size, passedIds },
|
|
632
|
+
...child.runs], stalled: child.stalled };
|
|
633
|
+
}
|
|
417
634
|
// 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
|
|
418
635
|
// 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
|
|
419
636
|
if (depth === 2 && paths.length > 1) {
|
|
420
637
|
const count = Math.min(paths.length, isolationRemaining[shard]!);
|
|
421
638
|
isolationRemaining[shard]! -= count;
|
|
422
639
|
const children = await Promise.all(paths.slice(0, count).map(async (file, index) => {
|
|
423
|
-
const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}
|
|
640
|
+
const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`, false, undefined, attemptId);
|
|
424
641
|
debug.log('release-loop.gate', 'pod-shard-isolate', { shard, file, outcome: child.stalled[0]?.reason ?? 'ok' });
|
|
425
642
|
return child;
|
|
426
643
|
}));
|
|
@@ -441,17 +658,18 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
441
658
|
if (depth === 0 && paths.length > POD_SHARD_FILE_CAP) {
|
|
442
659
|
const chunks = Array.from({ length: Math.ceil(paths.length / POD_SHARD_FILE_CAP) }, (_, i) => paths.slice(i * POD_SHARD_FILE_CAP, (i + 1) * POD_SHARD_FILE_CAP));
|
|
443
660
|
debug.log('release-loop.gate', 'pod-shard-cap-split', { shard, files: paths.length, chunks: chunks.length, cap: POD_SHARD_FILE_CAP, reason });
|
|
444
|
-
const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}
|
|
661
|
+
const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`, false, undefined, attemptId)));
|
|
445
662
|
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
446
663
|
}
|
|
447
664
|
debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
|
|
448
665
|
const middle = Math.ceil(paths.length / 2);
|
|
449
666
|
const children = await Promise.all([
|
|
450
|
-
runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0
|
|
451
|
-
runShard(paths.slice(middle), shard, depth + 1, `${branch}-1
|
|
667
|
+
runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`, false, undefined, attemptId),
|
|
668
|
+
runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`, false, undefined, attemptId),
|
|
452
669
|
]);
|
|
453
670
|
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
454
671
|
}
|
|
672
|
+
if (reused) debug.log('release-loop.gate', 'pod-shard-resumed', { shard, branch, files: paths.length, rc, durationMs: reused.durationMs ?? null });
|
|
455
673
|
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
456
674
|
errors: Number.isNaN(errors) ? 0 : errors,
|
|
457
675
|
ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
@@ -478,12 +696,16 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
478
696
|
localCommand: commandOverride && !remote ? commandOverride : localCommand,
|
|
479
697
|
get remoteMirror() { return remoteMirror; },
|
|
480
698
|
set remoteMirror(path) { remoteMirror = path; },
|
|
481
|
-
async sweep(tree, logDir, pod) {
|
|
482
|
-
if (pod) return podSweep(tree, logDir, pod);
|
|
699
|
+
async sweep(tree, logDir, pod, only) {
|
|
700
|
+
if (pod) return podSweep(tree, logDir, pod, only);
|
|
483
701
|
const listed = await command('git', ['ls-files', '*.test.*'], tree);
|
|
484
702
|
check(listed, 'git ls-files tests');
|
|
485
|
-
const
|
|
486
|
-
const
|
|
703
|
+
const allFiles = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
|
|
704
|
+
const onlySet = only ? new Set(only) : undefined;
|
|
705
|
+
const missing = only?.filter((file) => !allFiles.includes(file)) ?? [];
|
|
706
|
+
if (missing.length) throw new Error(`partial plan file missing at this commit: ${missing.slice(0, 5).join(', ')}`);
|
|
707
|
+
const files = onlySet ? allFiles.filter((file) => onlySet.has(file)) : allFiles;
|
|
708
|
+
const groups = onlySet ? [{ name: 'partial', paths: files }] : [
|
|
487
709
|
{ name: 'src-cli', paths: ['src/cli'] },
|
|
488
710
|
{ name: 'src-rest', paths: [...new Set(files.filter((f) => f.startsWith('src/') && !f.startsWith('src/cli/')).map((f) => f.split('/').length === 2 ? f : f.split('/').slice(0, 2).join('/')))] },
|
|
489
711
|
{ name: 'test', paths: ['test'] },
|
|
@@ -577,10 +799,61 @@ export function baselineCommit(root: string, version: string): string {
|
|
|
577
799
|
return commit;
|
|
578
800
|
}
|
|
579
801
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
802
|
+
const fileOf = testFileOfId;
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* GATE-BASELINE-CACHE (0.2.18) — the isolated `bun run test:deterministic <file>` result of one test file at one commit,
|
|
806
|
+
* kept in the machine ledger so a later gate does not run it again (0.2.17: the failing files were re-run on the previous
|
|
807
|
+
* release commit although that release had already measured them). Cache format harvested from draft #24500.
|
|
808
|
+
* `host` is where the result was measured (`local` or the ssh host) — a result from another host is a miss.
|
|
809
|
+
* Timeouts are never stored: an environment stall is measured again, not remembered.
|
|
810
|
+
*/
|
|
811
|
+
export interface BaselineFileResult { commit: string; file: string; host: string; failures: string[]; errors: string[]; missing: boolean }
|
|
812
|
+
export const baselineCachePath = (ledger: string, commit: string) => join(ledger, 'gate-baseline-cache', `${commit}.jsonl`);
|
|
813
|
+
function validFileResult(value: unknown, commit: string): value is BaselineFileResult {
|
|
814
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
815
|
+
const row = value as Partial<BaselineFileResult>;
|
|
816
|
+
if (row.commit !== commit || typeof row.file !== 'string' || typeof row.host !== 'string' || !row.host.trim()
|
|
817
|
+
|| typeof row.missing !== 'boolean' || !Array.isArray(row.failures) || !Array.isArray(row.errors)
|
|
818
|
+
|| (row.missing && (row.failures.length || row.errors.length))) return false;
|
|
819
|
+
try {
|
|
820
|
+
fileOf(`${row.file} > case`);
|
|
821
|
+
return [...row.failures, ...row.errors].every((id) => typeof id === 'string' && fileOf(id) === row.file);
|
|
822
|
+
} catch { return false; }
|
|
823
|
+
}
|
|
824
|
+
/** A missing cache file is empty; any unreadable or invalid row discards the whole file — it is never read as a pass. */
|
|
825
|
+
export function readBaselineFileCache(ledger: string, commit: string): Map<string, BaselineFileResult> {
|
|
826
|
+
const entries = new Map<string, BaselineFileResult>();
|
|
827
|
+
if (!sha.test(commit)) return entries;
|
|
828
|
+
const path = baselineCachePath(ledger, commit);
|
|
829
|
+
if (!existsSync(path)) return entries;
|
|
830
|
+
try {
|
|
831
|
+
// The writer always ends with one newline and never writes an empty line — anything else is a damaged file.
|
|
832
|
+
const text = readFileSync(path, 'utf8');
|
|
833
|
+
if (!text.endsWith('\n')) throw new Error('missing final newline');
|
|
834
|
+
for (const line of text.slice(0, -1).split('\n')) {
|
|
835
|
+
if (!line.trim()) throw new Error('empty line');
|
|
836
|
+
const row: unknown = JSON.parse(line);
|
|
837
|
+
if (!validFileResult(row, commit)) throw new Error('invalid row');
|
|
838
|
+
entries.set(`${row.file}\0${row.host}`, row);
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
debug.log('release.gate', 'baseline-cache-corrupt', { path, error: String(error) });
|
|
842
|
+
entries.clear();
|
|
843
|
+
}
|
|
844
|
+
return entries;
|
|
845
|
+
}
|
|
846
|
+
export function saveBaselineFileCache(ledger: string, commit: string, rows: BaselineFileResult[]): void {
|
|
847
|
+
if (!rows.length || !sha.test(commit)) return;
|
|
848
|
+
const entries = readBaselineFileCache(ledger, commit);
|
|
849
|
+
for (const row of rows) entries.set(`${row.file}\0${row.host}`, row);
|
|
850
|
+
const path = baselineCachePath(ledger, commit);
|
|
851
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
852
|
+
const temp = join(dirname(path), `.gate-baseline-${process.pid}-${randomUUID()}.tmp`);
|
|
853
|
+
try {
|
|
854
|
+
writeFileSync(temp, [...entries.values()].map((row) => JSON.stringify(row)).join('\n') + '\n', { mode: 0o600 });
|
|
855
|
+
renameSync(temp, path);
|
|
856
|
+
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
584
857
|
}
|
|
585
858
|
|
|
586
859
|
export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
|
|
@@ -659,8 +932,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
659
932
|
trees.push(cutTree);
|
|
660
933
|
await runner.add(cutTree, opts.commit);
|
|
661
934
|
for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
|
|
662
|
-
|
|
663
|
-
|
|
935
|
+
// GATE-PARTIAL: a plan made by `release resume --from gate --partial` for exactly this commit re-runs only its files.
|
|
936
|
+
// Its logs go to cut-partial/ — the full cut/ logs stay whole (the next release reads them as a baseline source).
|
|
937
|
+
const partial = readPartialPlan(roots, opts.version, opts.commit);
|
|
938
|
+
if (partial) debug.log('release-loop.gate', 'partial-regate', { version: opts.version, priorCommit: partial.priorCommit, commit: opts.commit, files: partial.files.length });
|
|
939
|
+
// GATE-SKIP-KNOWN-RETRY — the ids the verdict will not count as introduced (the recorded baseline ⊕ env-known), read
|
|
940
|
+
// before the sweep so a shard failing only with them skips its isolation retries. No recorded baseline = no skip.
|
|
941
|
+
let knownFailures: string[] = [];
|
|
942
|
+
if (opts.pod) {
|
|
943
|
+
try {
|
|
944
|
+
const knownSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
945
|
+
const saved = cachedBaseline(opts.baselineVersion, knownSha)?.saved;
|
|
946
|
+
const recorded = saved ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), knownSha);
|
|
947
|
+
const trustedKnown = saved ?? (recorded?.complete ? recorded : undefined);
|
|
948
|
+
if (trustedKnown) knownFailures = [...trustedKnown.failures, ...(trustedKnown.errors ?? []), ...loadEnvKnownFailures(repo)];
|
|
949
|
+
} catch (error) { debug.log('release-loop.gate', 'known-failures-unavailable', { error: String(error) }); }
|
|
950
|
+
}
|
|
951
|
+
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 } : {}) } : undefined,
|
|
953
|
+
partial?.files);
|
|
664
954
|
const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
|
|
665
955
|
const stalled = run.stalledEnv ?? [];
|
|
666
956
|
const noCompletedShard = stalled.length && run.rc === 0
|
|
@@ -700,7 +990,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
700
990
|
debug.log('release-loop.gate', 'pod-shard-stalled-env', { files: stalled.map(({ file, reason }) => ({ file, reason })), local });
|
|
701
991
|
return measured;
|
|
702
992
|
};
|
|
703
|
-
const
|
|
993
|
+
const measuredCut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
|
|
994
|
+
const cut = partial ? mergePartialFailures(partial, measuredCut) : measuredCut;
|
|
995
|
+
if (partial) result.partial = { priorCommit: partial.priorCommit, files: partial.files.length };
|
|
704
996
|
cutFailures = cut;
|
|
705
997
|
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
706
998
|
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
@@ -725,11 +1017,28 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
725
1017
|
};
|
|
726
1018
|
let baseline: SweepFailures | undefined = trusted ? { failures: trusted.failures, errors: trusted.errors ?? [] } : undefined;
|
|
727
1019
|
if (!baseline) {
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
1020
|
+
// GATE-SPEED A4 (RFC-release-gate-under-30min §A4) — with no recorded baseline only the files that failed at the cut
|
|
1021
|
+
// are swept at the previous release; a passing cut file needs no comparison. Files absent there stay new failures.
|
|
1022
|
+
const cutFiles = [...new Set([...cut.failures, ...cut.errors].map(fileOf))].sort();
|
|
1023
|
+
let present: string[] = [];
|
|
1024
|
+
if (cutFiles.length) {
|
|
1025
|
+
const tree = await getBaseTree();
|
|
1026
|
+
// -z: the default output quotes paths with special characters, which would read as absent.
|
|
1027
|
+
const listed = await runner.command('git', ['ls-files', '-z', '--', ...cutFiles], tree);
|
|
1028
|
+
check(listed, 'baseline ls-files');
|
|
1029
|
+
const known = new Set(listed.output.split('\0'));
|
|
1030
|
+
present = cutFiles.filter((file) => known.has(file));
|
|
1031
|
+
}
|
|
1032
|
+
result.baselineDeferred = { cutFiles: cutFiles.length, swept: present.length };
|
|
1033
|
+
debug.log('release-loop.gate', 'baseline-deferred', { version: opts.baselineVersion, commit: baseSha, cutFiles: cutFiles.length, swept: present.length });
|
|
1034
|
+
if (present.length) {
|
|
1035
|
+
// K9b — the baseline is swept on the same Pod pool as the cut, not on this host (0.2.6: an hour of local baseline
|
|
1036
|
+
// after a 23-minute Pod cut).
|
|
1037
|
+
const baseRun = await runner.sweep(baseTree!, join(root, 'release', opts.version, 'gate-logs', 'baseline'),
|
|
1038
|
+
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined,
|
|
1039
|
+
present);
|
|
1040
|
+
baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
|
|
1041
|
+
} else baseline = { failures: [], errors: [] };
|
|
733
1042
|
}
|
|
734
1043
|
for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
|
|
735
1044
|
const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
|
|
@@ -741,9 +1050,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
741
1050
|
result.knownEnv = countedCut.knownEnv.length;
|
|
742
1051
|
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
|
|
743
1052
|
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
744
|
-
result.fixed = diff.fixed.length;
|
|
1053
|
+
result.fixed = result.baselineDeferred ? null : diff.fixed.length;
|
|
745
1054
|
result.preexisting = diff.common.length;
|
|
746
|
-
|
|
1055
|
+
// GATE-BASELINE-CACHE — only the files whose new failures reproduce in isolation reach the baseline, and each
|
|
1056
|
+
// (commit, file, host) is run at most once across gates: this gate's cut results also seed the next gate's baseline.
|
|
1057
|
+
const cacheHost = opts.remote ?? 'local';
|
|
1058
|
+
const newFailureFiles = new Set(diff.newFailures.map(fileOf));
|
|
1059
|
+
const baseCache = newFailureFiles.size ? readBaselineFileCache(ledger, baseSha) : new Map<string, BaselineFileResult>();
|
|
1060
|
+
const cacheStats = { hits: 0, misses: 0 };
|
|
1061
|
+
const remember = (row: BaselineFileResult) => {
|
|
1062
|
+
// Only a row the reader would accept is stored; an unstorable result skips the cache and leaves the verdict alone.
|
|
1063
|
+
const { commit, file } = row;
|
|
1064
|
+
if (!validFileResult(row, commit)) {
|
|
1065
|
+
debug.log('release.gate', 'baseline-cache-skip', { commit, file });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
try { saveBaselineFileCache(ledger, row.commit, [row]); }
|
|
1069
|
+
catch (error) { debug.log('release.gate', 'baseline-cache-write-failed', { commit: row.commit, file: row.file, error: String(error) }); }
|
|
1070
|
+
};
|
|
1071
|
+
for (const file of newFailureFiles) {
|
|
747
1072
|
// 0.2.15: the cut re-check of a new-failure file (InsidePage.test.tsx · headless Chrome) hung for good — limit it like the host re-run.
|
|
748
1073
|
const cutCheck = cutHostFailed.has(file) ? undefined : await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
749
1074
|
if (cutCheck?.timedOut) {
|
|
@@ -754,30 +1079,71 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
754
1079
|
const isolatedCut = cutCheck === undefined
|
|
755
1080
|
? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
|
|
756
1081
|
: failuresOf(cutCheck, `cut isolated ${file}`);
|
|
1082
|
+
if (cutCheck !== undefined) remember({ commit: opts.commit, file, host: cacheHost, failures: isolatedCut.failures, errors: isolatedCut.errors, missing: false });
|
|
757
1083
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1084
|
+
let candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
1085
|
+
// GATE-INTRO-RECHECK — a new failure that is only bun's «timed out» shape gets one more isolated run with a generous
|
|
1086
|
+
// per-test limit, on the same runner (node-b when the gate is remote). Passing there = flaky under load (warning);
|
|
1087
|
+
// an assertion failure, a failure again, or a run that cannot be read stays introduced.
|
|
1088
|
+
const timedOut = cutCheck === undefined ? new Set<string>() : new Set(parseTimedOutFailures(stripPodRepoRoot(cutCheck.output)));
|
|
1089
|
+
const shaped = candidates.filter((id) => timedOut.has(id) && isolatedCut.failures.includes(id));
|
|
1090
|
+
if (shaped.length) {
|
|
1091
|
+
const relaxed = await runner.command('bun', ['run', 'test:deterministic', asPath(file), '--timeout', String(GATE_TIMEOUT_RECHECK_MS)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
1092
|
+
let flaky: string[] = [];
|
|
1093
|
+
let recheck: 'read' | 'timeout' | 'unreadable' = 'read';
|
|
1094
|
+
if (relaxed.timedOut) recheck = 'timeout';
|
|
1095
|
+
else {
|
|
1096
|
+
try {
|
|
1097
|
+
const again = failuresOf(relaxed, `cut timeout recheck ${file}`);
|
|
1098
|
+
const still = new Set([...again.failures, ...again.errors]);
|
|
1099
|
+
flaky = again.errors.length ? [] : shaped.filter((id) => !still.has(id));
|
|
1100
|
+
} catch { recheck = 'unreadable'; }
|
|
1101
|
+
}
|
|
1102
|
+
debug.log('release-loop.gate', 'timeout-recheck', { file, shaped: shaped.length, flaky: flaky.length, recheck, remote: opts.remote ?? 'local', limitMs: GATE_TIMEOUT_RECHECK_MS });
|
|
1103
|
+
if (flaky.length) {
|
|
1104
|
+
(result.loadFlaky ??= []).push(...flaky);
|
|
1105
|
+
candidates = candidates.filter((id) => !flaky.includes(id));
|
|
1106
|
+
try {
|
|
1107
|
+
const path = loadFlakyCensusPath(ledger);
|
|
1108
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1109
|
+
const at = new Date().toISOString();
|
|
1110
|
+
appendFileSync(path, flaky.map((id) => JSON.stringify({ at, version: opts.version, commit: opts.commit, id, host: cacheHost, limitMs: GATE_TIMEOUT_RECHECK_MS }) + '\n').join(''), { mode: 0o600 });
|
|
1111
|
+
} catch (error) { debug.log('release-loop.gate', 'load-flaky-census-write-failed', { file, error: String(error) }); }
|
|
1112
|
+
}
|
|
766
1113
|
}
|
|
1114
|
+
if (candidates.length === 0) continue;
|
|
767
1115
|
let oldFailures: Set<string>;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
oldFailures = new Set();
|
|
1116
|
+
const hit = baseCache.get(`${file}\0${cacheHost}`);
|
|
1117
|
+
if (hit) {
|
|
1118
|
+
cacheStats.hits++;
|
|
1119
|
+
oldFailures = new Set([...hit.failures, ...hit.errors]);
|
|
772
1120
|
} else {
|
|
773
|
-
|
|
774
|
-
|
|
1121
|
+
cacheStats.misses++;
|
|
1122
|
+
const baselineTree = await getBaseTree();
|
|
1123
|
+
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
1124
|
+
if (previous.timedOut) {
|
|
1125
|
+
debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'baseline isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
|
|
1126
|
+
result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
|
|
1130
|
+
const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
|
|
1131
|
+
if (lookup.rc !== 0 || lookup.output.trim()) throw new Error(`baseline isolated run incomplete: ${file}`);
|
|
1132
|
+
oldFailures = new Set();
|
|
1133
|
+
remember({ commit: baseSha, file, host: cacheHost, failures: [], errors: [], missing: true });
|
|
1134
|
+
} else {
|
|
1135
|
+
const prior = failuresOf(previous, `baseline isolated ${file}`);
|
|
1136
|
+
oldFailures = new Set([...prior.failures, ...prior.errors]);
|
|
1137
|
+
remember({ commit: baseSha, file, host: cacheHost, failures: prior.failures, errors: prior.errors, missing: false });
|
|
1138
|
+
}
|
|
775
1139
|
}
|
|
776
1140
|
for (const id of candidates) {
|
|
777
1141
|
if (oldFailures.has(id)) result.preexisting++;
|
|
778
1142
|
else result.introduced.push(id);
|
|
779
1143
|
}
|
|
780
1144
|
}
|
|
1145
|
+
result.baselineCache = cacheStats;
|
|
1146
|
+
debug.log('release.gate', 'baseline-cache', { hits: cacheStats.hits, misses: cacheStats.misses, commit: baseSha });
|
|
781
1147
|
result.outcome = result.introduced.length ? 'regression' : 'ok';
|
|
782
1148
|
} catch (error) {
|
|
783
1149
|
result.error = error instanceof Error ? error.message : String(error);
|
|
@@ -827,7 +1193,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
827
1193
|
export function graphGateResult(result: GateResult, graph: boolean) {
|
|
828
1194
|
return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
|
|
829
1195
|
verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
|
|
830
|
-
summary: (result.
|
|
1196
|
+
summary: (result.partial ? `부분 재검 ${result.partial.files}파일(앞 게이트 ${result.partial.priorCommit.slice(0, 9)} 결과 이음) · ` : '')
|
|
1197
|
+
+ (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed ?? '못 셈(기준선 미루기)'}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
|
|
1198
|
+
+ (result.loadFlaky?.length ? ` · ⚠ 부하 흔들림 ${result.loadFlaky.length}(시간 초과 꼴 · ${GATE_TIMEOUT_RECHECK_MS / 1000}s 재실행 통과 · 막지 않음)` : '')
|
|
831
1199
|
+ (result.stalledEnv.length ? ` · 환경 멈춤 ${result.stalledEnv.length} (Pod 밖 통과 ${result.stalledEnv.filter((item) => item.local === 'passed').length})` : '') };
|
|
832
1200
|
}
|
|
833
1201
|
|
|
@@ -851,12 +1219,17 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
|
|
|
851
1219
|
...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
|
|
852
1220
|
...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
|
|
853
1221
|
...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
|
|
1222
|
+
...(fromGraph.gatePodFreshShards === true ? { freshShards: true } : {}),
|
|
1223
|
+
...((cpu) => cpu ? { cpu } : {})(parsePodCpu(fromGraph.gatePodCpu, 'release.loop.gatePodCpu')),
|
|
854
1224
|
} : undefined,
|
|
855
1225
|
};
|
|
1226
|
+
let freshShards = false;
|
|
856
1227
|
for (let i = 0; i < args.length; i++) {
|
|
857
1228
|
const arg = args[i];
|
|
858
1229
|
if (arg === '--help') return 'help';
|
|
859
1230
|
if (arg === '--json') continue;
|
|
1231
|
+
// Applied after the loop and only to a Pod gate — on its own it must not turn a local gate into a Pod one (review r5).
|
|
1232
|
+
if (arg === '--fresh-shards') { freshShards = true; continue; }
|
|
860
1233
|
const key = ({ '--commit': 'commit', '--version': 'version', '--baseline-version': 'baselineVersion', '--baseline-commit': 'baselineCommit', '--remote': 'remote', '--remote-mirror': 'remoteMirror', '--pod-pool': 'pod', '--pod-shards': 'pod', '--pod-shard-timeout-seconds': 'pod' } as Record<string, keyof GateOptions>)[arg!];
|
|
861
1234
|
if (!key) throw new Error(`unknown option: ${arg}`);
|
|
862
1235
|
const value = args[++i];
|
|
@@ -871,17 +1244,21 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
|
|
|
871
1244
|
else if (key === 'remote') opts.remote = value;
|
|
872
1245
|
else if (key === 'remoteMirror') opts.remoteMirror = value;
|
|
873
1246
|
}
|
|
1247
|
+
if (freshShards && opts.pod) opts.pod = { ...opts.pod, freshShards: true };
|
|
874
1248
|
return opts;
|
|
875
1249
|
}
|
|
876
1250
|
|
|
877
1251
|
if (import.meta.main) {
|
|
878
1252
|
const start = Date.now();
|
|
1253
|
+
// The graph spawns this file as its own process, which inherits no daemon sink — without this every
|
|
1254
|
+
// `release-loop.gate`/`release.gate`/`pod.pool` event stayed in the file trail and `elanous logs` read 0 rows.
|
|
1255
|
+
await registerStandaloneLogSink('release-loop');
|
|
879
1256
|
let result: GateResult;
|
|
880
1257
|
let version = '';
|
|
881
1258
|
try {
|
|
882
1259
|
const opts = parseOptions(process.argv.slice(2), process.env);
|
|
883
1260
|
if (opts === 'help') {
|
|
884
|
-
console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--remote-mirror <path>] [--pod-pool <pool>] [--pod-shards <n>] [--pod-shard-timeout-seconds <n>] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
|
|
1261
|
+
console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--remote-mirror <path>] [--pod-pool <pool>] [--pod-shards <n>] [--pod-shard-timeout-seconds <n>] [--fresh-shards] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
|
|
885
1262
|
process.exit(0);
|
|
886
1263
|
}
|
|
887
1264
|
version = opts.version;
|