elanous 0.2.4 → 0.2.5
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/README.md +3 -3
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
- package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +29 -27
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +29 -27
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +29 -27
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +26 -24
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +26 -24
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +26 -24
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +29 -27
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +29 -27
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +29 -27
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +26 -24
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +29 -27
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +29 -27
- package/apps/pwa/out/manifest.webmanifest +5 -2
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +26 -24
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +26 -24
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +29 -27
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +29 -27
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +27 -25
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +29 -27
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +26 -24
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +29 -27
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +29 -27
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +29 -27
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +29 -27
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +29 -27
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +26 -24
- package/apps/pwa/out/sw-strategies.js +44 -0
- package/apps/pwa/out/sw.js +6 -44
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +29 -27
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +29 -27
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +29 -27
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +29 -27
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +29 -27
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +29 -27
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +29 -27
- package/catalog/resources.yaml +6 -6
- package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
- package/graphs/landing/landing-heal.yaml +1 -1
- package/graphs/release/recipes.yaml +8 -1
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +12 -0
- package/plugins/job-coach/connectors/ncs/server.ts +4 -3
- package/plugins/job-coach/graphs/gap-judge.ts +44 -0
- package/plugins/job-coach/graphs/recipes.yaml +27 -0
- package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
- package/plugins/job-coach/graphs/run-step.ts +211 -17
- package/plugins/job-coach/plugin.json +16 -3
- package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
- package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
- package/scripts/backup/elanous-backup.sh +4 -1
- package/scripts/build-docs-site.ts +4 -3
- package/scripts/coord-channel-watch.sh +23 -4
- package/scripts/coord-post.sh +49 -12
- package/scripts/coord-tracks.json +5 -5
- package/scripts/eval-bot-vs-grok.ts +79 -0
- package/scripts/eval-grok-live-baseline.ts +36 -0
- package/scripts/fixtures/front-route-corpus.sample.json +17 -0
- package/scripts/fleet/node-setup.sh +46 -7
- package/scripts/landing-heal/must-fix-match.ts +26 -0
- package/scripts/landing-heal/run-mission.ts +114 -20
- package/scripts/lib/front-route-measurement.ts +131 -0
- package/scripts/lib/tui-sim-keys.ts +42 -0
- package/scripts/measure-front-route-classifier.ts +56 -0
- package/scripts/media/ocr-leak-scan.sh +21 -0
- package/scripts/public-export.ts +2 -3
- package/scripts/public-import-pr.ts +80 -6
- package/scripts/publish-internal-dist.ts +2 -3
- package/scripts/red-sweep-diff.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +72 -0
- package/scripts/release-loop/gate-node.ts +332 -44
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/test-durations.ts +63 -0
- package/scripts/release-loop/upgrade-node.ts +3 -2
- package/scripts/test-audit-coverage-per-file.sh +16 -0
- package/scripts/test-audit-meaningless.ts +88 -0
- package/scripts/test-audit-overlap-and-weak.ts +73 -0
- package/scripts/test-deterministic.ts +27 -5
- package/scripts/tui-sim.ts +59 -14
- package/scripts/ux/bump-installed.sh +20 -0
- package/scripts/ux/lib/cdp.ts +72 -0
- package/scripts/ux/record-page.ts +50 -0
- package/skills/explainer-video/SKILL.md +99 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
- package/skills/explainer-video/engine/build.mjs +431 -0
- package/skills/explainer-video/engine/vo.ts +37 -0
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
- package/src/agent/nested-agent-env.ts +1 -0
- package/src/agent-mission/driver.ts +95 -16
- package/src/agent-mission/mission-cli.ts +6 -0
- package/src/agent-mission/resource-ladder.ts +73 -25
- package/src/auth/acp-token.ts +16 -0
- package/src/auth/github-app-token.ts +112 -0
- package/src/autopilot/gh-rest-fallback.ts +177 -0
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/boot/acp-server.ts +2 -16
- package/src/boot/daemon-tools/web-search.ts +2 -1
- package/src/boot/ws-bridge.ts +11 -0
- package/src/chat/index.ts +37 -8
- package/src/chat/log-scroll-key.ts +12 -0
- package/src/chat/submit-trace.ts +30 -0
- package/src/cli/agent-cli.ts +7 -2
- package/src/cli/decisions-cli.ts +94 -0
- package/src/cli/doctor-cli.ts +40 -54
- package/src/cli/doctor-readiness.ts +16 -24
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/mcp-call.ts +9 -0
- package/src/cli/mcp-gateway-cli.ts +7 -3
- package/src/cli/plugin-cli.ts +109 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +94 -7
- package/src/cli/self-update.ts +7 -4
- package/src/cli/start-cli.ts +33 -14
- package/src/dashboard/acp-boot.ts +3 -0
- package/src/dashboard/index.ts +62 -13
- package/src/dashboard/input/no-provider-reply.ts +13 -0
- package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
- package/src/dashboard/stale-install-check.ts +77 -0
- package/src/decisions/decision-ledger.ts +404 -0
- package/src/domains/outbound-alert.ts +8 -2
- package/src/domains/telegram-kind-route.ts +21 -0
- package/src/execution-loop/dispatch-task.ts +139 -0
- package/src/execution-loop/gate-decision-summary.ts +29 -0
- package/src/execution-loop/gates.ts +197 -0
- package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
- package/src/graph-kinds/plugin-nodes.ts +10 -2
- package/src/graph-runner/runner.ts +35 -5
- package/src/grounding/token.ts +12 -6
- package/src/harness/harness-cli-command.ts +68 -9
- package/src/harness/harness-pod-dispatch.ts +8 -2
- package/src/harness/harness-stop.ts +147 -18
- package/src/harness/self-send-decide.ts +26 -17
- package/src/harness/self-send-target.ts +22 -1
- package/src/harness/worktree-branch-prefix.ts +4 -3
- package/src/index.ts +56 -13
- package/src/instance/resolve.ts +6 -0
- package/src/intake-plane/collect-telegram-saved.ts +5 -0
- package/src/intake-plane/digest.ts +11 -2
- package/src/live/detail-switch.ts +8 -3
- package/src/llm/usable-llm.ts +32 -0
- package/src/loops/registry.ts +27 -5
- package/src/market/publish.ts +59 -3
- package/src/native-tool-catalog.ts +2 -2
- package/src/nexus/api/graph-approvals.ts +125 -0
- package/src/nexus/api/http-server.ts +60 -6
- package/src/nexus/api/log-fabric.ts +10 -1
- package/src/nexus/api/mcp-oauth-hint.ts +10 -9
- package/src/nexus/api/merge-approvals.ts +2 -1
- package/src/nexus/api/meta-api.ts +14 -0
- package/src/nexus/api/plugins-credentials-api.ts +47 -0
- package/src/nexus/api/plugins-market.ts +87 -3
- package/src/nexus/api/pod-credential-api.ts +68 -14
- package/src/nexus/api/public-routes.ts +7 -0
- package/src/nexus/api/trace.ts +57 -6
- package/src/nexus/boot/register-mcp-clients.ts +99 -14
- package/src/nexus/index.ts +33 -32
- package/src/nexus/outbound/router.ts +1 -1
- package/src/onboarding.ts +25 -2
- package/src/plugins/core/host.ts +14 -0
- package/src/plugins/install/market-fetch.ts +4 -3
- package/src/plugins/install/plugin-credentials.ts +106 -0
- package/src/plugins/install/plugin-install.ts +163 -32
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/release-loop/checklist.ts +191 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/self-dev/dev-cli.ts +40 -20
- package/src/self-dev/dev-pipeline.ts +77 -1
- package/src/self-implement/gate-baseline.ts +3 -1
- package/src/self-implement/goal-author.ts +18 -1
- package/src/self-implement/host-regate.ts +6 -2
- package/src/self-implement/orchestrator.ts +7 -2
- package/src/self-implement/seams.ts +4 -2
- package/src/skills/tools/dev-harness.ts +42 -0
- package/src/steward/steward-cards.ts +45 -0
- package/src/steward/triage.ts +50 -14
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
- package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
- package/src/telegram-report.ts +12 -6
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +28 -2
- package/src/util/spawn-sync-output.ts +15 -0
- package/src/version/code-revision.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-search/free.ts +77 -0
- package/src/web-search/index.ts +2 -0
- package/src/workflow-runtime/deps-bridge.ts +4 -3
- package/src/workflow-runtime/plugin-kind-node.ts +32 -4
- package/src/workflow-runtime/types.ts +2 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
- /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { debug } from '../../src/debug/log.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
6
|
+
import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
|
|
7
|
+
|
|
8
|
+
export type MetricVerdict = 'pass' | 'fail' | 'unmeasured';
|
|
9
|
+
export interface ApprovalMetric { name: string; value: string; verdict: MetricVerdict }
|
|
10
|
+
|
|
11
|
+
const outcomeMetric = (name: string, node: Record<string, unknown> | undefined): ApprovalMetric => ({
|
|
12
|
+
name, value: node?.outcome === undefined ? 'missing' : String(node.outcome),
|
|
13
|
+
verdict: node?.outcome === undefined ? 'unmeasured' : node.outcome === 'ok' ? 'pass' : 'fail',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function runAutoApprove(context: GraphContext = readGraphContext(), deps: { instanceRoot?: string; repo?: string; bunVersion?: string } = {}) {
|
|
17
|
+
const { version } = context.input;
|
|
18
|
+
const root = deps.instanceRoot ?? effectiveInstanceRoot();
|
|
19
|
+
const repo = deps.repo ?? resolve(import.meta.dir, '../..');
|
|
20
|
+
const gate = context.outputs.gate;
|
|
21
|
+
const prepare = context.outputs.prepare;
|
|
22
|
+
let pinned: string | undefined;
|
|
23
|
+
try { pinned = readFileSync(join(repo, '.bun-version'), 'utf8').trim(); } catch { pinned = undefined; }
|
|
24
|
+
const bunVersion = deps.bunVersion ?? Bun.version;
|
|
25
|
+
const out = prepare?.out;
|
|
26
|
+
let distCount: number | undefined;
|
|
27
|
+
if (prepare?.outcome === 'ok' && typeof out === 'string' && isAbsolute(out)
|
|
28
|
+
&& resolve(out) === join(root, 'release', version, 'prepared')) {
|
|
29
|
+
try { distCount = readdirSync(join(out, 'dist'), { withFileTypes: true }).filter((entry) => entry.isFile()).length; }
|
|
30
|
+
catch { distCount = undefined; }
|
|
31
|
+
}
|
|
32
|
+
const gateErrorAbsent = gate && !Object.hasOwn(gate, 'error');
|
|
33
|
+
const metrics: ApprovalMetric[] = [
|
|
34
|
+
{ name: 'gate regressions and measurement', value: JSON.stringify({ outcome: gate?.outcome ?? null, introduced: gate?.introduced ?? null, error: gate?.error ?? null }),
|
|
35
|
+
verdict: !gate || gate.outcome === undefined || !Array.isArray(gate.introduced) ? 'unmeasured'
|
|
36
|
+
: gate.outcome === 'ok' && gate.introduced.length === 0 && gateErrorAbsent ? 'pass' : 'fail' },
|
|
37
|
+
{ name: 'bun version', value: `running=${bunVersion}; pinned=${pinned ?? 'missing'}`,
|
|
38
|
+
verdict: !pinned ? 'unmeasured' : bunVersion === pinned ? 'pass' : 'fail' },
|
|
39
|
+
outcomeMetric('upgrade', context.outputs.upgrade),
|
|
40
|
+
outcomeMetric('tui', context.outputs.tui),
|
|
41
|
+
outcomeMetric('notes-check', context.outputs['notes-check']),
|
|
42
|
+
{ name: 'public leak and prepare completeness', value: `prepare=${prepare?.outcome ?? 'missing'}`,
|
|
43
|
+
verdict: prepare?.outcome === undefined ? 'unmeasured' : prepare.outcome === 'ok' ? 'pass' : 'fail' },
|
|
44
|
+
{ name: 'docs notes page', value: `docs=${context.outputs.docs?.outcome ?? 'missing'}; notes=${String(context.outputs.docs?.notes ?? 'missing')}`,
|
|
45
|
+
verdict: context.outputs.docs?.outcome === undefined || context.outputs.docs.notes === undefined ? 'unmeasured'
|
|
46
|
+
: context.outputs.docs.outcome === 'ok' && typeof context.outputs.docs.notes === 'string' && context.outputs.docs.notes.trim() ? 'pass' : 'fail' },
|
|
47
|
+
{ name: 'installation files', value: `dist files=${distCount ?? 'missing'}; prepare.out=${String(out ?? 'missing')}`,
|
|
48
|
+
verdict: prepare?.outcome !== 'ok' ? prepare?.outcome === undefined ? 'unmeasured' : 'fail'
|
|
49
|
+
: distCount === undefined ? 'unmeasured' : distCount === 5 ? 'pass' : 'fail' },
|
|
50
|
+
];
|
|
51
|
+
const outcome = metrics.every((metric) => metric.verdict === 'pass') ? 'ok' as const : 'fail' as const;
|
|
52
|
+
const report = join(root, 'release', version, 'auto-approval.md');
|
|
53
|
+
const lines = [`# v${version} auto-approval`, '', `Outcome: ${outcome}`, '', '| # | Metric | Value | Verdict |', '|---|---|---|---|',
|
|
54
|
+
...metrics.map((metric, i) => `| ${i + 1} | ${metric.name} | ${metric.value.replaceAll('|', '\\|').replace(/[\r\n]/g, ' ')} | ${metric.verdict} |`),
|
|
55
|
+
'', '## 되돌리는 법', `gh release edit v${version} --draft`, `git tag -d v${version} && git push origin :refs/tags/v${version}`, '해당 docs PR revert', ''];
|
|
56
|
+
mkdirSync(dirname(report), { recursive: true });
|
|
57
|
+
writeFileSync(report, lines.join('\n'));
|
|
58
|
+
debug.log('release-loop.approve', 'auto-decision', { version, outcome, metrics });
|
|
59
|
+
return { outcome, verdict: outcome === 'ok' ? 'pass' as const : 'fail' as const, summary: `auto approval ${outcome}: ${metrics.filter((metric) => metric.verdict === 'pass').length}/8`,
|
|
60
|
+
...(outcome === 'ok' ? { decidedBy: 'release-loop metrics' as const } : {}), metrics, report };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (import.meta.main) {
|
|
64
|
+
try {
|
|
65
|
+
const result = runAutoApprove();
|
|
66
|
+
emitNodeResult(result);
|
|
67
|
+
process.exitCode = result.outcome === 'ok' ? 0 : 1;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: `auto approval unmeasured: ${error instanceof Error ? error.message : String(error)}` });
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -5,19 +5,26 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
|
-
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
9
|
+
import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
9
10
|
import { diffFailures, parseFailures } from './gate-diff';
|
|
10
11
|
import { deriveCdpTestPatterns } from '../test-deterministic';
|
|
11
12
|
import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
13
|
+
import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
14
|
+
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
15
|
+
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
12
16
|
|
|
13
17
|
interface CommandResult { rc: number; output: string }
|
|
14
18
|
export interface GateRunner {
|
|
15
19
|
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
16
|
-
|
|
20
|
+
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
21
|
+
sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
|
|
17
22
|
add(tree: string, commit: string): Promise<void>;
|
|
18
23
|
remove(tree: string): Promise<void>;
|
|
19
24
|
snapshot(tree: string, commit: string): Promise<void>;
|
|
20
25
|
removeSnapshot(tree: string): Promise<void>;
|
|
26
|
+
/** Absolute remote bare mirror path, set before preparing the remote trees. */
|
|
27
|
+
remoteMirror?: string;
|
|
21
28
|
}
|
|
22
29
|
export interface GateOptions {
|
|
23
30
|
commit: string;
|
|
@@ -25,8 +32,11 @@ export interface GateOptions {
|
|
|
25
32
|
baselineVersion?: string;
|
|
26
33
|
baselineCommit?: string;
|
|
27
34
|
remote?: string;
|
|
35
|
+
remoteMirror?: string;
|
|
28
36
|
instanceRoot?: string;
|
|
37
|
+
ledgerRoot?: string;
|
|
29
38
|
repo?: string;
|
|
39
|
+
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number };
|
|
30
40
|
}
|
|
31
41
|
export interface GateResult {
|
|
32
42
|
outcome: 'ok' | 'regression' | 'error';
|
|
@@ -34,26 +44,49 @@ export interface GateResult {
|
|
|
34
44
|
introduced: string[];
|
|
35
45
|
preexisting: number;
|
|
36
46
|
fixed: number;
|
|
47
|
+
baselineSource?: 'ledger' | 'instance' | 'swept';
|
|
37
48
|
durationMs: number;
|
|
38
49
|
error?: string;
|
|
50
|
+
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
51
|
+
partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
const sha = /^[0-9a-f]{7,40}$/i;
|
|
42
55
|
const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
|
|
43
56
|
const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
|
|
44
57
|
const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
58
|
+
const defaultRemoteMirror = '~/mirror/elanous-agent.git';
|
|
45
59
|
|
|
46
60
|
function check(result: CommandResult, label: string): void {
|
|
47
61
|
if (result.rc !== 0) throw new Error(`${label} failed (rc=${result.rc}): ${result.output.slice(-500)}`);
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
type SweepFailures = { failures: string[]; errors: string[] };
|
|
51
|
-
|
|
65
|
+
// A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
|
|
66
|
+
// (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
|
|
67
|
+
// the sweep's own summary is always the «last» one, so read the last match, never the first.
|
|
68
|
+
const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
|
|
69
|
+
let last: RegExpExecArray | null = null;
|
|
70
|
+
for (const m of output.matchAll(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`))) last = m as RegExpExecArray;
|
|
71
|
+
return last;
|
|
72
|
+
};
|
|
73
|
+
const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
|
|
74
|
+
/** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
|
|
75
|
+
const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
|
|
76
|
+
export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
|
|
52
77
|
const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
|
|
78
|
+
const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
|
|
79
|
+
let last: string | undefined;
|
|
80
|
+
for (const line of output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
|
|
81
|
+
const header = /^(?:\.\/)?((?:[\w.-]+\/)*[\w.-]+\.test\.tsx?):/.exec(line.trim());
|
|
82
|
+
if (header && paths.includes(header[1]!)) last = header[1];
|
|
83
|
+
}
|
|
84
|
+
return last;
|
|
85
|
+
};
|
|
53
86
|
|
|
54
87
|
function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
55
88
|
const output = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
56
|
-
const ran = /Ran (\d+) tests? across ([1-9]\d*) files
|
|
89
|
+
const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
|
|
57
90
|
const reportedErrors = summaryCount(output, 'error');
|
|
58
91
|
const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
|
|
59
92
|
const incomplete = (reason: string) => new Error(`${label} incomplete (rc=${run.rc}; Ran=${ran ? ran[0] : 'missing'}; errors=${Number.isNaN(reportedErrors) ? 'missing' : errorCount}; ${reason})\n${tail40(output)}`);
|
|
@@ -81,17 +114,192 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
|
81
114
|
return { failures, errors: [...new Set(errors)].sort() };
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
class StalledPodShards extends Error {
|
|
118
|
+
constructor(readonly stalledShards: NonNullable<GateResult['stalledShards']>, readonly partialSummary: NonNullable<GateResult['partialSummary']>) {
|
|
119
|
+
super(`pod sweep incomplete: stalled shards ${stalledShards.map(({ shard }) => shard).join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
|
|
124
|
+
let remoteMirror: string | undefined;
|
|
125
|
+
const localCommand: GateRunner['command'] = async (cmd, args, cwd) => {
|
|
126
|
+
const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
|
|
90
127
|
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
|
|
91
|
-
}
|
|
128
|
+
};
|
|
129
|
+
const command: GateRunner['command'] = commandOverride ?? (remote
|
|
130
|
+
? async (cmd, args, cwd) => {
|
|
131
|
+
const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${[cmd, ...args].map(quote).join(' ')}`],
|
|
132
|
+
{ encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
|
|
133
|
+
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
|
|
134
|
+
}
|
|
135
|
+
: localCommand);
|
|
136
|
+
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
|
|
137
|
+
const shardCount = pod.shards ?? 8;
|
|
138
|
+
const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
|
|
139
|
+
if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
|
|
140
|
+
|| !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds < 1) throw new Error('invalid pod sweep options');
|
|
141
|
+
const head = await command('git', ['rev-parse', 'HEAD'], tree);
|
|
142
|
+
check(head, 'cut tree HEAD');
|
|
143
|
+
const commit = head.output.trim();
|
|
144
|
+
if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('invalid cut tree HEAD');
|
|
145
|
+
const listed = await command('git', ['ls-files', '*.test.*'], tree);
|
|
146
|
+
check(listed, 'git ls-files tests');
|
|
147
|
+
const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
|
|
148
|
+
if (files.some((file) => file.startsWith('-') || file.startsWith('/') || file.startsWith('./')
|
|
149
|
+
|| file.split('/').some((part) => !part || part === '.' || part === '..') || /[\r\n]/.test(file))) throw new Error('unsafe pod test path');
|
|
150
|
+
if (new Set(files).size !== files.length) throw new Error('duplicate pod test path');
|
|
151
|
+
if (!files.length) throw new Error('sweep incomplete: no tests ran');
|
|
152
|
+
const cdp = await command('rg', ['-l', '--glob', '*.test.ts', '-e', 'requireCdpBase', '-e', '9333', 'test', 'scripts'], tree);
|
|
153
|
+
if (cdp.rc !== 0 && cdp.rc !== 1) throw new Error(`CDP pattern discovery failed (rc=${cdp.rc}): ${tail40(cdp.output)}`);
|
|
154
|
+
const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
|
|
155
|
+
// CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
|
|
156
|
+
// bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
|
|
157
|
+
const assignable = files.filter((file) => !cdpPatterns.includes(file) && !file.split('/').some((part) => part.startsWith('.')));
|
|
158
|
+
if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
|
|
159
|
+
const shards = Array.from({ length: Math.min(shardCount, assignable.length) }, () => [] as string[]);
|
|
160
|
+
assignable.forEach((file, index) => shards[index % shards.length]!.push(file));
|
|
161
|
+
const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
|
|
162
|
+
if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
|
|
163
|
+
const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
|
|
164
|
+
const jobLogTail = (job: PodCommandResult): string => {
|
|
165
|
+
if (podLogTail) return podLogTail(job);
|
|
166
|
+
for (const member of poolScheduler.members) {
|
|
167
|
+
const logs = defaultKubectl(['--context', member.context, '-n', 'elanous-test', 'logs', `job/${job.job}`, '-c', 'child', '--tail=200']);
|
|
168
|
+
if (logs.status === 0 && logs.stdout) return logs.stdout;
|
|
169
|
+
}
|
|
170
|
+
return '';
|
|
171
|
+
};
|
|
172
|
+
type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
|
|
173
|
+
type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
|
|
174
|
+
// 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
|
|
175
|
+
const isolationRemaining = shards.map((paths) => paths.length);
|
|
176
|
+
const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
177
|
+
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
178
|
+
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
179
|
+
// 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
|
|
180
|
+
const args = ['bun', 'run', 'test:deterministic', ...ignores, ...paths.map(asPath)].map(quote).join(' ')
|
|
181
|
+
+ ' --reporter=junit --reporter-outfile="$HOME/outbox/junit.xml"';
|
|
182
|
+
const start = Date.now();
|
|
183
|
+
let output: string | undefined;
|
|
184
|
+
let junit: string | undefined;
|
|
185
|
+
let rc: number | undefined;
|
|
186
|
+
let jobExitCode: number | undefined;
|
|
187
|
+
let lastFile: string | undefined;
|
|
188
|
+
let jobFailed = false;
|
|
189
|
+
try {
|
|
190
|
+
const job = await podCommand({
|
|
191
|
+
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
192
|
+
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
193
|
+
...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
194
|
+
name: `gate-${randomUUID()}`,
|
|
195
|
+
command: ['bash', '-lc', `mkdir -p "$HOME/outbox"; (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"`],
|
|
196
|
+
});
|
|
197
|
+
jobExitCode = job.exitCode;
|
|
198
|
+
const logPath = join(job.artifactsDir, 'shard.log');
|
|
199
|
+
const rcPath = join(job.artifactsDir, 'shard.rc');
|
|
200
|
+
output = existsSync(logPath) ? readFileSync(logPath, 'utf8') : undefined;
|
|
201
|
+
const junitPath = join(job.artifactsDir, 'junit.xml');
|
|
202
|
+
junit = existsSync(junitPath) ? readFileSync(junitPath, 'utf8') : undefined;
|
|
203
|
+
const rcText = existsSync(rcPath) ? readFileSync(rcPath, 'utf8').trim() : '';
|
|
204
|
+
rc = /^\d+$/.test(rcText) && Number.isSafeInteger(Number(rcText)) ? Number(rcText) : undefined;
|
|
205
|
+
lastFile = output === undefined ? undefined : lastStartedTestFile(output, paths);
|
|
206
|
+
if (!lastFile && (job.exitCode !== 0 || rc === undefined || (rc !== 0 && rc !== 1))) {
|
|
207
|
+
try { lastFile = lastStartedTestFile(jobLogTail(job).split(/\r?\n/).slice(-200).join('\n'), paths); }
|
|
208
|
+
catch { /* Log retrieval is diagnostic; do not hide the stalled shard. */ }
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
jobFailed = true;
|
|
212
|
+
}
|
|
213
|
+
const durationMs = Date.now() - start;
|
|
214
|
+
if (logDir) {
|
|
215
|
+
mkdirSync(logDir, { recursive: true });
|
|
216
|
+
const destination = join(logDir, `pod-${shard}${branch}.log`);
|
|
217
|
+
if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
|
|
218
|
+
else rmSync(destination, { force: true });
|
|
219
|
+
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths }) + '\n', { mode: 0o600 });
|
|
220
|
+
const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
|
|
221
|
+
if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
|
|
222
|
+
else rmSync(junitDestination, { force: true });
|
|
223
|
+
}
|
|
224
|
+
debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1 });
|
|
225
|
+
const clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
226
|
+
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
227
|
+
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
228
|
+
const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
|
|
229
|
+
let reason: ShardReason | undefined;
|
|
230
|
+
let namedFailures: number | undefined;
|
|
231
|
+
let unattributedDetail: string | undefined;
|
|
232
|
+
if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
|
|
233
|
+
else if (!clean || rc === undefined) reason = 'no-output';
|
|
234
|
+
else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
|
|
235
|
+
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length || Number(ran[1]) === 0) reason = 'incomplete';
|
|
236
|
+
else {
|
|
237
|
+
try {
|
|
238
|
+
const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
|
|
239
|
+
if (attributed.failures.length !== fails) reason = 'unattributed';
|
|
240
|
+
} catch (error) {
|
|
241
|
+
reason = 'unattributed';
|
|
242
|
+
// 어떤 수가 어긋났는지(요약·이름·오류 귀속) 첫 줄을 싣는다 — 수만으론 원인 파일을 못 찾았다(09-30 ① 1번 조각).
|
|
243
|
+
unattributedDetail = (error instanceof Error ? error.message : String(error)).split('\n', 1)[0]!.slice(0, 300);
|
|
244
|
+
}
|
|
245
|
+
if (reason === 'unattributed') namedFailures = [...clean.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
|
|
246
|
+
}
|
|
247
|
+
if (reason) {
|
|
248
|
+
// 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
|
|
249
|
+
// 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
|
|
250
|
+
if (depth === 2 && paths.length > 1) {
|
|
251
|
+
const count = Math.min(paths.length, isolationRemaining[shard]!);
|
|
252
|
+
isolationRemaining[shard]! -= count;
|
|
253
|
+
const children = await Promise.all(paths.slice(0, count).map(async (file, index) => {
|
|
254
|
+
const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`);
|
|
255
|
+
debug.log('release-loop.gate', 'pod-shard-isolate', { shard, file, outcome: child.stalled[0]?.reason ?? 'ok' });
|
|
256
|
+
return child;
|
|
257
|
+
}));
|
|
258
|
+
return { runs: children.flatMap((child) => child.runs), stalled: [
|
|
259
|
+
...children.flatMap((child) => child.stalled),
|
|
260
|
+
...(paths.length > count ? [{ shard, files: paths.slice(count), reason,
|
|
261
|
+
...(lastFile ? { lastFile } : {}),
|
|
262
|
+
}] : []),
|
|
263
|
+
] };
|
|
264
|
+
}
|
|
265
|
+
if (depth === 2 || paths.length === 1) return { runs: [], stalled: [{ shard, files: paths, reason,
|
|
266
|
+
...(lastFile ? { lastFile } : {}),
|
|
267
|
+
...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
|
|
268
|
+
}] };
|
|
269
|
+
debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
|
|
270
|
+
const middle = Math.ceil(paths.length / 2);
|
|
271
|
+
const children = await Promise.all([
|
|
272
|
+
runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`),
|
|
273
|
+
runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`),
|
|
274
|
+
]);
|
|
275
|
+
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
276
|
+
}
|
|
277
|
+
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
278
|
+
errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
|
|
279
|
+
ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
|
|
280
|
+
};
|
|
281
|
+
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
282
|
+
const settled = await Promise.allSettled(shards.map((paths, shard) => runShard(paths, shard)));
|
|
283
|
+
const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
284
|
+
if (rejected) throw rejected.reason;
|
|
285
|
+
const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
286
|
+
const stalledShards = results.flatMap((result) => result.stalled);
|
|
287
|
+
const measured = results.flatMap((result) => result.runs);
|
|
288
|
+
const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
|
|
289
|
+
errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
|
|
290
|
+
{ pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
|
|
291
|
+
if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
|
|
292
|
+
if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
|
|
293
|
+
const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
|
|
294
|
+
return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n` };
|
|
295
|
+
};
|
|
92
296
|
return {
|
|
93
297
|
command,
|
|
94
|
-
|
|
298
|
+
localCommand: commandOverride && !remote ? commandOverride : localCommand,
|
|
299
|
+
get remoteMirror() { return remoteMirror; },
|
|
300
|
+
set remoteMirror(path) { remoteMirror = path; },
|
|
301
|
+
async sweep(tree, logDir, pod) {
|
|
302
|
+
if (pod) return podSweep(tree, logDir, pod);
|
|
95
303
|
const listed = await command('git', ['ls-files', '*.test.*'], tree);
|
|
96
304
|
check(listed, 'git ls-files tests');
|
|
97
305
|
const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
|
|
@@ -114,7 +322,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
114
322
|
const start = Date.now();
|
|
115
323
|
const groupIgnores = cdpPatterns.filter((pattern) => group.paths.some((p) => pattern === p || pattern.startsWith(`${p}/`)))
|
|
116
324
|
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
117
|
-
const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths], tree);
|
|
325
|
+
const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths.map(asPath)], tree);
|
|
118
326
|
if (logDir) {
|
|
119
327
|
mkdirSync(logDir, { recursive: true });
|
|
120
328
|
writeFileSync(join(logDir, `${group.name}.log`), run.output, { mode: 0o600 });
|
|
@@ -124,7 +332,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
124
332
|
try { failuresOf(run, `${group.name} shard`); }
|
|
125
333
|
catch (error) { throw new Error(`${group.name} shard: ${error instanceof Error ? error.message : String(error)}`); }
|
|
126
334
|
const clean = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
127
|
-
const ran = /Ran (\d+) tests? across (\d+) files
|
|
335
|
+
const ran = lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/)!;
|
|
128
336
|
const passes = summaryCount(clean, 'pass');
|
|
129
337
|
if (!Number.isFinite(passes) && Number(ran[1]) > 0) throw new Error(`${group.name} shard incomplete (rc=${run.rc}; pass summary missing)\n${tail40(clean)}`);
|
|
130
338
|
total = { pass: total.pass + (Number.isNaN(passes) ? 0 : passes), fail: total.fail + summaryCount(clean, 'fail'),
|
|
@@ -135,14 +343,31 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
135
343
|
if (!outputs.length || (total.ran === 0 && total.errors === 0)) throw new Error('sweep incomplete: no tests ran');
|
|
136
344
|
return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n` };
|
|
137
345
|
},
|
|
138
|
-
async add(tree, commit) {
|
|
139
|
-
|
|
346
|
+
async add(tree, commit) {
|
|
347
|
+
if (remote) {
|
|
348
|
+
if (!remoteMirror) throw new Error('remote mirror not prepared');
|
|
349
|
+
check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'remote tree clone');
|
|
350
|
+
check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'remote tree fetch');
|
|
351
|
+
check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'remote tree checkout');
|
|
352
|
+
} else check(await command('git', ['worktree', 'add', '--detach', tree, commit], repo), 'git worktree add');
|
|
353
|
+
},
|
|
354
|
+
async remove(tree) {
|
|
355
|
+
if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'remote tree cleanup');
|
|
356
|
+
else check(await command('git', ['worktree', 'remove', '--force', tree], repo), 'git worktree remove');
|
|
357
|
+
},
|
|
140
358
|
async snapshot(tree, commit) {
|
|
141
|
-
|
|
142
|
-
|
|
359
|
+
if (remote) {
|
|
360
|
+
if (!remoteMirror) throw new Error('remote mirror not prepared');
|
|
361
|
+
check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'baseline snapshot clone');
|
|
362
|
+
check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'baseline snapshot fetch');
|
|
363
|
+
check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'baseline snapshot checkout');
|
|
364
|
+
} else {
|
|
365
|
+
check(await command('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, tree], repo), 'baseline snapshot clone');
|
|
366
|
+
check(await command('git', ['checkout', '--quiet', '--detach', commit], tree), 'baseline snapshot checkout');
|
|
367
|
+
}
|
|
143
368
|
},
|
|
144
369
|
async removeSnapshot(tree) {
|
|
145
|
-
if (remote) check(await command('rm', ['-
|
|
370
|
+
if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'baseline snapshot cleanup');
|
|
146
371
|
else rmSync(tree, { recursive: true, force: true });
|
|
147
372
|
},
|
|
148
373
|
};
|
|
@@ -179,7 +404,24 @@ function fileOf(id: string): string {
|
|
|
179
404
|
|
|
180
405
|
export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
|
|
181
406
|
const start = Date.now();
|
|
182
|
-
const
|
|
407
|
+
const explicitConfig = getElanousConfigDirOverride();
|
|
408
|
+
const root = explicitConfig ? effectiveInstanceRoot() : (opts.instanceRoot ?? effectiveInstanceRoot());
|
|
409
|
+
const ledger = explicitConfig ? root : (opts.ledgerRoot ?? releaseLedgerRoot());
|
|
410
|
+
const roots = [...new Set([ledger, root])];
|
|
411
|
+
const cachedBaseline = (version: string, commit: string) => {
|
|
412
|
+
for (const [index, location] of roots.entries()) {
|
|
413
|
+
const saved = readBaseline(location, version);
|
|
414
|
+
if (saved?.commit === commit) return { saved, source: index === 0 && location !== root ? 'ledger' as const : 'instance' as const };
|
|
415
|
+
}
|
|
416
|
+
return undefined;
|
|
417
|
+
};
|
|
418
|
+
const baselineReleaseCommit = (version: string) => {
|
|
419
|
+
for (const location of roots) {
|
|
420
|
+
const path = join(location, 'release', version, 'release.json');
|
|
421
|
+
if (existsSync(path)) return baselineCommit(location, version);
|
|
422
|
+
}
|
|
423
|
+
return baselineCommit(root, version);
|
|
424
|
+
};
|
|
183
425
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
184
426
|
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
|
|
185
427
|
let work: string | undefined;
|
|
@@ -191,24 +433,53 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
191
433
|
|| (opts.baselineVersion && !versionPattern.test(opts.baselineVersion))
|
|
192
434
|
|| (opts.baselineCommit && !sha.test(opts.baselineCommit))) throw new Error('invalid commit or version');
|
|
193
435
|
if (!opts.baselineVersion) throw new Error('previous release version required (--baseline-version)');
|
|
436
|
+
if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ?? 8) || (opts.pod.shards ?? 8) < 1
|
|
437
|
+
|| !Number.isSafeInteger(opts.pod.shardTimeoutSeconds ?? 1200) || (opts.pod.shardTimeoutSeconds ?? 1200) < 1)) throw new Error('invalid pod sweep options');
|
|
194
438
|
if (opts.remote && !/^(?:[\w.-]+@)?[\w.-]+$/.test(opts.remote)) throw new Error('invalid ssh host');
|
|
195
439
|
if (opts.remote) {
|
|
196
|
-
|
|
440
|
+
runner.remoteMirror = undefined;
|
|
441
|
+
const configured = opts.remoteMirror ?? defaultRemoteMirror;
|
|
442
|
+
if (!/^(?:\/|~\/)[\w./-]+$/.test(configured) || configured.split('/').includes('..')) throw new Error('invalid remote mirror path');
|
|
443
|
+
const home = configured.startsWith('~/') ? await runner.command('sh', ['-c', 'printf "%s\\n" "$HOME"'], '/tmp') : undefined;
|
|
444
|
+
if (home) check(home, 'remote home');
|
|
445
|
+
const homePath = home?.output.split(/\r?\n/).find((line) => line.startsWith('/'));
|
|
446
|
+
if (home && (!homePath || !/^\/[^\r\n]*$/.test(homePath))) throw new Error('invalid remote home');
|
|
447
|
+
const mirror = configured.startsWith('~/') ? join(homePath!, configured.slice(2)) : configured;
|
|
448
|
+
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
449
|
+
cachedBaseline(opts.baselineVersion, baseSha);
|
|
450
|
+
const present = await runner.command('test', ['-d', mirror], '/tmp');
|
|
451
|
+
if (present.rc === 1) {
|
|
452
|
+
check(await runner.command('mkdir', ['-p', dirname(mirror)], '/tmp'), 'remote mirror parent');
|
|
453
|
+
check(await runner.command('git', ['init', '--bare', mirror], '/tmp'), 'remote mirror init');
|
|
454
|
+
} else check(present, 'remote mirror lookup');
|
|
455
|
+
const bare = await runner.command('git', ['--git-dir', mirror, 'rev-parse', '--is-bare-repository'], '/tmp');
|
|
456
|
+
check(bare, 'remote mirror inspection');
|
|
457
|
+
if (!bare.output.split(/\r?\n/).includes('true')) throw new Error('remote mirror is not bare');
|
|
458
|
+
for (const commit of new Set([opts.commit, baseSha])) {
|
|
459
|
+
const ref = `refs/elanous/gate/${commit}`;
|
|
460
|
+
const found = await runner.command('git', ['--git-dir', mirror, 'show-ref', '--verify', '--quiet', ref], '/tmp');
|
|
461
|
+
if (found.rc === 1) check(await runner.localCommand('git', ['push', `${opts.remote}:${mirror}`, `${commit}:${ref}`], repo), `remote push ${commit}`);
|
|
462
|
+
else check(found, `remote mirror ref ${commit}`);
|
|
463
|
+
}
|
|
464
|
+
runner.remoteMirror = mirror;
|
|
465
|
+
const temporary = await runner.command('mktemp', ['-d', '/tmp/release-gate-XXXXXXXX'], '/tmp');
|
|
197
466
|
check(temporary, 'remote mktemp');
|
|
198
|
-
|
|
467
|
+
const paths = temporary.output.split(/\r?\n/).filter((line) => /^\/tmp\/release-gate-[\w-]+$/.test(line));
|
|
468
|
+
if (paths.length !== 1) throw new Error('could not create temporary work directory');
|
|
469
|
+
work = paths[0]!;
|
|
199
470
|
} else work = mkdtempSync(join(tmpdir(), 'release-gate-'));
|
|
200
|
-
if (!work || (opts.remote && !/^\/tmp\/release-gate-[\w-]+$/.test(work))) throw new Error('could not create temporary work directory');
|
|
201
471
|
const cutTree = join(work, 'cut');
|
|
202
472
|
trees.push(cutTree);
|
|
203
473
|
await runner.add(cutTree, opts.commit);
|
|
204
474
|
for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
|
|
205
|
-
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'));
|
|
475
|
+
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'), opts.pod);
|
|
206
476
|
const cut = failuresOf(cutRun, 'cut sweep');
|
|
207
477
|
cutFailures = cut;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
478
|
+
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
479
|
+
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
480
|
+
const trusted = cached?.saved;
|
|
481
|
+
result.baselineSource = trusted ? cached!.source : 'swept';
|
|
482
|
+
debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
|
|
212
483
|
let baseTree: string | undefined;
|
|
213
484
|
const getBaseTree = async () => {
|
|
214
485
|
if (!baseTree) {
|
|
@@ -234,16 +505,16 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
234
505
|
result.fixed = diff.fixed.length;
|
|
235
506
|
result.preexisting = diff.common.length;
|
|
236
507
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
237
|
-
const isolated = await runner.command('bun', ['run', 'test:deterministic', file], cutTree);
|
|
508
|
+
const isolated = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree);
|
|
238
509
|
const isolatedCut = failuresOf(isolated, `cut isolated ${file}`);
|
|
239
510
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
240
511
|
const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
241
512
|
if (candidates.length === 0) continue;
|
|
242
513
|
const baselineTree = await getBaseTree();
|
|
243
|
-
const previous = await runner.command('bun', ['run', 'test:deterministic', file], baselineTree);
|
|
514
|
+
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree);
|
|
244
515
|
let oldFailures: Set<string>;
|
|
245
|
-
if (/No tests found/i.test(previous.output) && previous.rc === 1) {
|
|
246
|
-
const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], repo);
|
|
516
|
+
if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
|
|
517
|
+
const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
|
|
247
518
|
if (lookup.rc !== 0 || lookup.output.trim()) throw new Error(`baseline isolated run incomplete: ${file}`);
|
|
248
519
|
oldFailures = new Set();
|
|
249
520
|
} else {
|
|
@@ -258,6 +529,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
258
529
|
result.outcome = result.introduced.length ? 'regression' : 'ok';
|
|
259
530
|
} catch (error) {
|
|
260
531
|
result.error = error instanceof Error ? error.message : String(error);
|
|
532
|
+
if (error instanceof StalledPodShards) {
|
|
533
|
+
result.stalledShards = error.stalledShards;
|
|
534
|
+
result.partialSummary = error.partialSummary;
|
|
535
|
+
}
|
|
261
536
|
} finally {
|
|
262
537
|
if (baseSnapshot && work) {
|
|
263
538
|
try { await runner.removeSnapshot(join(work, 'baseline')); }
|
|
@@ -269,22 +544,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
269
544
|
}
|
|
270
545
|
if (work) {
|
|
271
546
|
try {
|
|
272
|
-
if (opts.remote) check(await runner.command('
|
|
547
|
+
if (opts.remote) check(await runner.command('rm', ['-rf', '--', work], '/tmp'), 'remote temporary directory cleanup');
|
|
273
548
|
else rmSync(work, { recursive: true, force: true });
|
|
274
549
|
} catch (error) { result.outcome = 'error'; result.error = `cleanup: ${String(error)}`; }
|
|
275
550
|
}
|
|
551
|
+
if (opts.remote) runner.remoteMirror = undefined;
|
|
276
552
|
result.durationMs = Date.now() - start;
|
|
277
553
|
}
|
|
278
554
|
if (result.outcome !== 'error' && cutFailures) {
|
|
279
555
|
try {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
556
|
+
for (const location of roots) {
|
|
557
|
+
const path = failureFile(location, opts.version);
|
|
558
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
559
|
+
const temp = join(dirname(path), `.gate-failures-${process.pid}-${randomUUID()}.tmp`);
|
|
560
|
+
try {
|
|
561
|
+
writeFileSync(temp, JSON.stringify({ commit: opts.commit, failures: cutFailures.failures, ...(cutFailures.errors.length ? { errors: cutFailures.errors } : {}) }, null, 2) + '\n', { mode: 0o600 });
|
|
562
|
+
chmodSync(temp, 0o600);
|
|
563
|
+
renameSync(temp, path);
|
|
564
|
+
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
565
|
+
}
|
|
288
566
|
} catch (error) {
|
|
289
567
|
result.outcome = 'error';
|
|
290
568
|
result.error = `baseline persistence: ${String(error)}`;
|
|
@@ -313,20 +591,30 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
|
|
|
313
591
|
baselineVersion: typeof fromGraph.previousVersion === 'string' ? fromGraph.previousVersion : undefined,
|
|
314
592
|
baselineCommit: typeof fromGraph.previousCommit === 'string' ? fromGraph.previousCommit : undefined,
|
|
315
593
|
remote: typeof fromGraph.gateRemote === 'string' ? fromGraph.gateRemote : undefined,
|
|
594
|
+
remoteMirror: typeof fromGraph.gateRemoteMirror === 'string' ? fromGraph.gateRemoteMirror : undefined,
|
|
595
|
+
pod: typeof fromGraph.gatePodPool === 'string' ? {
|
|
596
|
+
pool: fromGraph.gatePodPool,
|
|
597
|
+
...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
|
|
598
|
+
...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
|
|
599
|
+
} : undefined,
|
|
316
600
|
};
|
|
317
601
|
for (let i = 0; i < args.length; i++) {
|
|
318
602
|
const arg = args[i];
|
|
319
603
|
if (arg === '--help') return 'help';
|
|
320
604
|
if (arg === '--json') continue;
|
|
321
|
-
const key = ({ '--commit': 'commit', '--version': 'version', '--baseline-version': 'baselineVersion', '--baseline-commit': 'baselineCommit', '--remote': 'remote' } as Record<string, keyof GateOptions>)[arg!];
|
|
605
|
+
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!];
|
|
322
606
|
if (!key) throw new Error(`unknown option: ${arg}`);
|
|
323
607
|
const value = args[++i];
|
|
324
608
|
if (!value || value.startsWith('--')) throw new Error(`value required for ${arg}`);
|
|
325
|
-
if (
|
|
609
|
+
if (arg === '--pod-pool') opts.pod = { ...opts.pod, pool: value };
|
|
610
|
+
else if (arg === '--pod-shards') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shards: Number(value) };
|
|
611
|
+
else if (arg === '--pod-shard-timeout-seconds') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shardTimeoutSeconds: Number(value) };
|
|
612
|
+
else if (key === 'commit') opts.commit = value;
|
|
326
613
|
else if (key === 'version') opts.version = value;
|
|
327
614
|
else if (key === 'baselineVersion') opts.baselineVersion = value;
|
|
328
615
|
else if (key === 'baselineCommit') opts.baselineCommit = value;
|
|
329
616
|
else if (key === 'remote') opts.remote = value;
|
|
617
|
+
else if (key === 'remoteMirror') opts.remoteMirror = value;
|
|
330
618
|
}
|
|
331
619
|
return opts;
|
|
332
620
|
}
|
|
@@ -338,7 +626,7 @@ if (import.meta.main) {
|
|
|
338
626
|
try {
|
|
339
627
|
const opts = parseOptions(process.argv.slice(2), process.env);
|
|
340
628
|
if (opts === 'help') {
|
|
341
|
-
console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
|
|
629
|
+
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.');
|
|
342
630
|
process.exit(0);
|
|
343
631
|
}
|
|
344
632
|
version = opts.version;
|