elanous 0.2.4 → 0.2.6
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 +7 -6
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_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 +11 -1
- package/graphs/release/release-loop.yaml +9 -2
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/card-followup/README.md +33 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +16 -0
- package/plugins/card-followup/graphs/recipes.yaml +12 -0
- package/plugins/card-followup/graphs/run-step.ts +172 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/geo-check/README.md +13 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +177 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +42 -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/daemon-port-baseline.txt +1 -2
- 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/android-test-device.ts +32 -0
- 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/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +366 -45
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/shard-plan.ts +47 -0
- 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 +53 -0
- package/skills/explainer-video/SKILL.md +117 -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/reel.mjs +128 -0
- package/skills/explainer-video/engine/reel.sh +8 -0
- package/skills/explainer-video/engine/vo.ts +79 -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/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/autopilot/track-agent.ts +3 -3
- package/src/autopilot/track-courier.ts +2 -2
- 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/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +136 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +97 -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 +13 -7
- 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/field/field-media.ts +245 -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 +50 -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/field-uploads.ts +143 -0
- package/src/nexus/api/graph-approvals.ts +174 -0
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +69 -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/notify/llm-budget-alert.ts +119 -0
- 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/plugins/maker/plugin-maker.ts +208 -0
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +217 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/seat-address/seat-address.ts +34 -0
- 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/run-ledger-compact.ts +1 -1
- package/src/self-implement/seams.ts +4 -2
- package/src/session/index.ts +23 -10
- 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-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -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/telegram.ts +108 -1
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +32 -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/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- 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/2415.ed6428ff334c7ce2.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/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/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/botlab/page-9fa879ebf5959096.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/control/page-4472ddf1e2458e69.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/morning/page-112f7e477bda77fd.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 → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
|
@@ -5,19 +5,28 @@ 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';
|
|
9
|
-
import {
|
|
8
|
+
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
9
|
+
import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
10
|
+
import { diffFailures, junitFailures, parseFailures } from './gate-diff';
|
|
11
|
+
import { planShards, readFileDurations } from './shard-plan';
|
|
10
12
|
import { deriveCdpTestPatterns } from '../test-deterministic';
|
|
11
13
|
import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
14
|
+
import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
15
|
+
import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
|
|
16
|
+
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
17
|
+
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
12
18
|
|
|
13
19
|
interface CommandResult { rc: number; output: string }
|
|
14
20
|
export interface GateRunner {
|
|
15
21
|
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
16
|
-
|
|
22
|
+
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
23
|
+
sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
|
|
17
24
|
add(tree: string, commit: string): Promise<void>;
|
|
18
25
|
remove(tree: string): Promise<void>;
|
|
19
26
|
snapshot(tree: string, commit: string): Promise<void>;
|
|
20
27
|
removeSnapshot(tree: string): Promise<void>;
|
|
28
|
+
/** Absolute remote bare mirror path, set before preparing the remote trees. */
|
|
29
|
+
remoteMirror?: string;
|
|
21
30
|
}
|
|
22
31
|
export interface GateOptions {
|
|
23
32
|
commit: string;
|
|
@@ -25,8 +34,11 @@ export interface GateOptions {
|
|
|
25
34
|
baselineVersion?: string;
|
|
26
35
|
baselineCommit?: string;
|
|
27
36
|
remote?: string;
|
|
37
|
+
remoteMirror?: string;
|
|
28
38
|
instanceRoot?: string;
|
|
39
|
+
ledgerRoot?: string;
|
|
29
40
|
repo?: string;
|
|
41
|
+
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string };
|
|
30
42
|
}
|
|
31
43
|
export interface GateResult {
|
|
32
44
|
outcome: 'ok' | 'regression' | 'error';
|
|
@@ -34,26 +46,49 @@ export interface GateResult {
|
|
|
34
46
|
introduced: string[];
|
|
35
47
|
preexisting: number;
|
|
36
48
|
fixed: number;
|
|
49
|
+
baselineSource?: 'ledger' | 'instance' | 'swept';
|
|
37
50
|
durationMs: number;
|
|
38
51
|
error?: string;
|
|
52
|
+
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
53
|
+
partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
const sha = /^[0-9a-f]{7,40}$/i;
|
|
42
57
|
const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
|
|
43
58
|
const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
|
|
44
59
|
const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
60
|
+
const defaultRemoteMirror = '~/mirror/elanous-agent.git';
|
|
45
61
|
|
|
46
62
|
function check(result: CommandResult, label: string): void {
|
|
47
63
|
if (result.rc !== 0) throw new Error(`${label} failed (rc=${result.rc}): ${result.output.slice(-500)}`);
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
type SweepFailures = { failures: string[]; errors: string[] };
|
|
51
|
-
|
|
67
|
+
// A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
|
|
68
|
+
// (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
|
|
69
|
+
// the sweep's own summary is always the «last» one, so read the last match, never the first.
|
|
70
|
+
const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
|
|
71
|
+
let last: RegExpExecArray | null = null;
|
|
72
|
+
for (const m of output.matchAll(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`))) last = m as RegExpExecArray;
|
|
73
|
+
return last;
|
|
74
|
+
};
|
|
75
|
+
const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
|
|
76
|
+
/** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
|
|
77
|
+
const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
|
|
78
|
+
export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
|
|
52
79
|
const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
|
|
80
|
+
const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
|
|
81
|
+
let last: string | undefined;
|
|
82
|
+
for (const line of output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
|
|
83
|
+
const header = /^(?:\.\/)?((?:[\w.-]+\/)*[\w.-]+\.test\.tsx?):/.exec(line.trim());
|
|
84
|
+
if (header && paths.includes(header[1]!)) last = header[1];
|
|
85
|
+
}
|
|
86
|
+
return last;
|
|
87
|
+
};
|
|
53
88
|
|
|
54
89
|
function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
55
90
|
const output = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
56
|
-
const ran = /Ran (\d+) tests? across ([1-9]\d*) files
|
|
91
|
+
const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
|
|
57
92
|
const reportedErrors = summaryCount(output, 'error');
|
|
58
93
|
const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
|
|
59
94
|
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 +116,222 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
|
81
116
|
return { failures, errors: [...new Set(errors)].sort() };
|
|
82
117
|
}
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
class StalledPodShards extends Error {
|
|
120
|
+
constructor(readonly stalledShards: NonNullable<GateResult['stalledShards']>, readonly partialSummary: NonNullable<GateResult['partialSummary']>) {
|
|
121
|
+
super(`pod sweep incomplete: stalled shards ${stalledShards.map(({ shard }) => shard).join(', ')}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string): GateRunner {
|
|
126
|
+
let remoteMirror: string | undefined;
|
|
127
|
+
const localCommand: GateRunner['command'] = async (cmd, args, cwd) => {
|
|
128
|
+
const run = spawnSync(cmd, args, { cwd, encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
|
|
90
129
|
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
|
|
91
|
-
}
|
|
130
|
+
};
|
|
131
|
+
const command: GateRunner['command'] = commandOverride ?? (remote
|
|
132
|
+
? async (cmd, args, cwd) => {
|
|
133
|
+
const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${[cmd, ...args].map(quote).join(' ')}`],
|
|
134
|
+
{ encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
|
|
135
|
+
return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}` };
|
|
136
|
+
}
|
|
137
|
+
: localCommand);
|
|
138
|
+
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
|
|
139
|
+
const shardCount = pod.shards ?? 24;
|
|
140
|
+
const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
|
|
141
|
+
if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
|
|
142
|
+
|| !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds < 1) throw new Error('invalid pod sweep options');
|
|
143
|
+
const head = await command('git', ['rev-parse', 'HEAD'], tree);
|
|
144
|
+
check(head, 'cut tree HEAD');
|
|
145
|
+
const commit = head.output.trim();
|
|
146
|
+
if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('invalid cut tree HEAD');
|
|
147
|
+
const listed = await command('git', ['ls-files', '*.test.*'], tree);
|
|
148
|
+
check(listed, 'git ls-files tests');
|
|
149
|
+
const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
|
|
150
|
+
if (files.some((file) => file.startsWith('-') || file.startsWith('/') || file.startsWith('./')
|
|
151
|
+
|| file.split('/').some((part) => !part || part === '.' || part === '..') || /[\r\n]/.test(file))) throw new Error('unsafe pod test path');
|
|
152
|
+
if (new Set(files).size !== files.length) throw new Error('duplicate pod test path');
|
|
153
|
+
if (!files.length) throw new Error('sweep incomplete: no tests ran');
|
|
154
|
+
const cdp = await command('rg', ['-l', '--glob', '*.test.ts', '-e', 'requireCdpBase', '-e', '9333', 'test', 'scripts'], tree);
|
|
155
|
+
if (cdp.rc !== 0 && cdp.rc !== 1) throw new Error(`CDP pattern discovery failed (rc=${cdp.rc}): ${tail40(cdp.output)}`);
|
|
156
|
+
const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
|
|
157
|
+
// CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
|
|
158
|
+
// bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
|
|
159
|
+
const assignable = files.filter((file) => !cdpPatterns.includes(file) && !file.split('/').some((part) => part.startsWith('.')));
|
|
160
|
+
if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
|
|
161
|
+
const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
|
|
162
|
+
const shards = planShards(assignable, durations, shardCount);
|
|
163
|
+
const known = assignable.filter((file) => durations.has(file)).length;
|
|
164
|
+
debug.log('release-loop.gate', 'pod-shard-plan', {
|
|
165
|
+
shards: shards.length, known, unknown: assignable.length - known,
|
|
166
|
+
maxPlannedSeconds: Math.max(...shards.map((shard) => shard.plannedSeconds)),
|
|
167
|
+
source: durations.size ? pod.durationSource : 'none',
|
|
168
|
+
});
|
|
169
|
+
const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
|
|
170
|
+
if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
|
|
171
|
+
const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
|
|
172
|
+
const bunCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim() || undefined;
|
|
173
|
+
const cachePrefix = bunCache ? `${podBunCacheVolume(bunCache).shellPrefix} ` : '';
|
|
174
|
+
const jobLogTail = (job: PodCommandResult): string => {
|
|
175
|
+
if (podLogTail) return podLogTail(job);
|
|
176
|
+
for (const member of poolScheduler.members) {
|
|
177
|
+
const logs = defaultKubectl(['--context', member.context, '-n', 'elanous-test', 'logs', `job/${job.job}`, '-c', 'child', '--tail=200']);
|
|
178
|
+
if (logs.status === 0 && logs.stdout) return logs.stdout;
|
|
179
|
+
}
|
|
180
|
+
return '';
|
|
181
|
+
};
|
|
182
|
+
type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
|
|
183
|
+
type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
|
|
184
|
+
// 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
|
|
185
|
+
const isolationRemaining = shards.map((item) => item.files.length);
|
|
186
|
+
const estimated = (paths: string[]) => paths.reduce((sum, file) => {
|
|
187
|
+
const value = durations.get(file);
|
|
188
|
+
return sum + (value !== undefined && Number.isFinite(value) && value >= 0 ? value : unknownSeconds);
|
|
189
|
+
}, 0);
|
|
190
|
+
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);
|
|
191
|
+
const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
|
|
192
|
+
const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
193
|
+
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
194
|
+
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
195
|
+
// 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
|
|
196
|
+
const args = ['bun', 'run', 'test:deterministic', ...ignores, ...paths.map(asPath)].map(quote).join(' ')
|
|
197
|
+
+ ' --reporter=junit --reporter-outfile="$HOME/outbox/junit.xml"';
|
|
198
|
+
const start = Date.now();
|
|
199
|
+
let output: string | undefined;
|
|
200
|
+
let junit: string | undefined;
|
|
201
|
+
let rc: number | undefined;
|
|
202
|
+
let jobExitCode: number | undefined;
|
|
203
|
+
let lastFile: string | undefined;
|
|
204
|
+
let jobFailed = false;
|
|
205
|
+
try {
|
|
206
|
+
const job = await podCommand({
|
|
207
|
+
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
208
|
+
...(bunCache ? { bunCache } : {}),
|
|
209
|
+
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
210
|
+
...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
211
|
+
name: `gate-${randomUUID()}`,
|
|
212
|
+
command: ['bash', '-lc', `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"`],
|
|
213
|
+
});
|
|
214
|
+
jobExitCode = job.exitCode;
|
|
215
|
+
const logPath = join(job.artifactsDir, 'shard.log');
|
|
216
|
+
const rcPath = join(job.artifactsDir, 'shard.rc');
|
|
217
|
+
output = existsSync(logPath) ? readFileSync(logPath, 'utf8') : undefined;
|
|
218
|
+
const junitPath = join(job.artifactsDir, 'junit.xml');
|
|
219
|
+
junit = existsSync(junitPath) ? readFileSync(junitPath, 'utf8') : undefined;
|
|
220
|
+
const rcText = existsSync(rcPath) ? readFileSync(rcPath, 'utf8').trim() : '';
|
|
221
|
+
rc = /^\d+$/.test(rcText) && Number.isSafeInteger(Number(rcText)) ? Number(rcText) : undefined;
|
|
222
|
+
lastFile = output === undefined ? undefined : lastStartedTestFile(output, paths);
|
|
223
|
+
if (!lastFile && (job.exitCode !== 0 || rc === undefined || (rc !== 0 && rc !== 1))) {
|
|
224
|
+
try { lastFile = lastStartedTestFile(jobLogTail(job).split(/\r?\n/).slice(-200).join('\n'), paths); }
|
|
225
|
+
catch { /* Log retrieval is diagnostic; do not hide the stalled shard. */ }
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
jobFailed = true;
|
|
229
|
+
}
|
|
230
|
+
const durationMs = Date.now() - start;
|
|
231
|
+
if (logDir) {
|
|
232
|
+
mkdirSync(logDir, { recursive: true });
|
|
233
|
+
const destination = join(logDir, `pod-${shard}${branch}.log`);
|
|
234
|
+
if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
|
|
235
|
+
else rmSync(destination, { force: true });
|
|
236
|
+
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths) }) + '\n', { mode: 0o600 });
|
|
237
|
+
const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
|
|
238
|
+
if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
|
|
239
|
+
else rmSync(junitDestination, { force: true });
|
|
240
|
+
}
|
|
241
|
+
debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
|
|
242
|
+
let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
243
|
+
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
244
|
+
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
245
|
+
const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
|
|
246
|
+
let reason: ShardReason | undefined;
|
|
247
|
+
let namedFailures: number | undefined;
|
|
248
|
+
let unattributedDetail: string | undefined;
|
|
249
|
+
if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
|
|
250
|
+
else if (!clean || rc === undefined) reason = 'no-output';
|
|
251
|
+
else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
|
|
252
|
+
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length || Number(ran[1]) === 0) reason = 'incomplete';
|
|
253
|
+
else {
|
|
254
|
+
try {
|
|
255
|
+
const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
|
|
256
|
+
if (attributed.failures.length !== fails) reason = 'unattributed';
|
|
257
|
+
} catch (error) {
|
|
258
|
+
reason = 'unattributed';
|
|
259
|
+
// 어떤 수가 어긋났는지(요약·이름·오류 귀속) 첫 줄을 싣는다 — 수만으론 원인 파일을 못 찾았다(09-30 ① 1번 조각).
|
|
260
|
+
unattributedDetail = (error instanceof Error ? error.message : String(error)).split('\n', 1)[0]!.slice(0, 300);
|
|
261
|
+
}
|
|
262
|
+
if (reason === 'unattributed') namedFailures = [...clean.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
|
|
263
|
+
// 콘솔 요약은 실패를 세는데 `(fail)` 이름 줄이 하나도 없으면 같은 조각의 junit 에서 이름을 가져온다
|
|
264
|
+
// (10-01 0.2.6 게이트: `--dots` 로 도는 한 파일 조각이 이름 없이 끝나 게이트 전체가 error 로 멈췄다).
|
|
265
|
+
if (reason === 'unattributed' && namedFailures === 0 && junit) {
|
|
266
|
+
const named = junitFailures(junit);
|
|
267
|
+
if (named.length === fails) {
|
|
268
|
+
const patched = `${clean}\n${named.map((id) => { const cut = id.indexOf(' > '); return `${id.slice(0, cut)}:\n(fail) ${id.slice(cut + 3)}`; }).join('\n')}\n`;
|
|
269
|
+
try {
|
|
270
|
+
if (failuresOf({ rc, output: patched }, `pod-${shard} shard`).failures.length === fails) {
|
|
271
|
+
clean = patched;
|
|
272
|
+
reason = undefined;
|
|
273
|
+
debug.log('release-loop.gate', 'pod-shard-junit-attribution', { shard, files: paths, failures: named });
|
|
274
|
+
}
|
|
275
|
+
} catch { /* 그래도 못 맞추면 원래대로 멈춘다 */ }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (reason) {
|
|
280
|
+
// 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
|
|
281
|
+
// 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
|
|
282
|
+
if (depth === 2 && paths.length > 1) {
|
|
283
|
+
const count = Math.min(paths.length, isolationRemaining[shard]!);
|
|
284
|
+
isolationRemaining[shard]! -= count;
|
|
285
|
+
const children = await Promise.all(paths.slice(0, count).map(async (file, index) => {
|
|
286
|
+
const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`);
|
|
287
|
+
debug.log('release-loop.gate', 'pod-shard-isolate', { shard, file, outcome: child.stalled[0]?.reason ?? 'ok' });
|
|
288
|
+
return child;
|
|
289
|
+
}));
|
|
290
|
+
return { runs: children.flatMap((child) => child.runs), stalled: [
|
|
291
|
+
...children.flatMap((child) => child.stalled),
|
|
292
|
+
...(paths.length > count ? [{ shard, files: paths.slice(count), reason,
|
|
293
|
+
...(lastFile ? { lastFile } : {}),
|
|
294
|
+
}] : []),
|
|
295
|
+
] };
|
|
296
|
+
}
|
|
297
|
+
if (depth === 2 || paths.length === 1) return { runs: [], stalled: [{ shard, files: paths, reason,
|
|
298
|
+
...(lastFile ? { lastFile } : {}),
|
|
299
|
+
...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
|
|
300
|
+
}] };
|
|
301
|
+
debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
|
|
302
|
+
const middle = Math.ceil(paths.length / 2);
|
|
303
|
+
const children = await Promise.all([
|
|
304
|
+
runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`),
|
|
305
|
+
runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`),
|
|
306
|
+
]);
|
|
307
|
+
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
308
|
+
}
|
|
309
|
+
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
310
|
+
errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
|
|
311
|
+
ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
|
|
312
|
+
};
|
|
313
|
+
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
314
|
+
const settled = await Promise.allSettled(shards.map((item, shard) => runShard(item.files, shard)));
|
|
315
|
+
const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
316
|
+
if (rejected) throw rejected.reason;
|
|
317
|
+
const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
318
|
+
const stalledShards = results.flatMap((result) => result.stalled);
|
|
319
|
+
const measured = results.flatMap((result) => result.runs);
|
|
320
|
+
const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
|
|
321
|
+
errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
|
|
322
|
+
{ pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
|
|
323
|
+
if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
|
|
324
|
+
if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
|
|
325
|
+
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, ''));
|
|
326
|
+
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` };
|
|
327
|
+
};
|
|
92
328
|
return {
|
|
93
329
|
command,
|
|
94
|
-
|
|
330
|
+
localCommand: commandOverride && !remote ? commandOverride : localCommand,
|
|
331
|
+
get remoteMirror() { return remoteMirror; },
|
|
332
|
+
set remoteMirror(path) { remoteMirror = path; },
|
|
333
|
+
async sweep(tree, logDir, pod) {
|
|
334
|
+
if (pod) return podSweep(tree, logDir, pod);
|
|
95
335
|
const listed = await command('git', ['ls-files', '*.test.*'], tree);
|
|
96
336
|
check(listed, 'git ls-files tests');
|
|
97
337
|
const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
|
|
@@ -114,7 +354,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
114
354
|
const start = Date.now();
|
|
115
355
|
const groupIgnores = cdpPatterns.filter((pattern) => group.paths.some((p) => pattern === p || pattern.startsWith(`${p}/`)))
|
|
116
356
|
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
117
|
-
const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths], tree);
|
|
357
|
+
const run = await command('bun', ['run', 'test:deterministic', ...groupIgnores, ...group.paths.map(asPath)], tree);
|
|
118
358
|
if (logDir) {
|
|
119
359
|
mkdirSync(logDir, { recursive: true });
|
|
120
360
|
writeFileSync(join(logDir, `${group.name}.log`), run.output, { mode: 0o600 });
|
|
@@ -124,7 +364,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
124
364
|
try { failuresOf(run, `${group.name} shard`); }
|
|
125
365
|
catch (error) { throw new Error(`${group.name} shard: ${error instanceof Error ? error.message : String(error)}`); }
|
|
126
366
|
const clean = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
127
|
-
const ran = /Ran (\d+) tests? across (\d+) files
|
|
367
|
+
const ran = lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/)!;
|
|
128
368
|
const passes = summaryCount(clean, 'pass');
|
|
129
369
|
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
370
|
total = { pass: total.pass + (Number.isNaN(passes) ? 0 : passes), fail: total.fail + summaryCount(clean, 'fail'),
|
|
@@ -135,14 +375,31 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
135
375
|
if (!outputs.length || (total.ran === 0 && total.errors === 0)) throw new Error('sweep incomplete: no tests ran');
|
|
136
376
|
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
377
|
},
|
|
138
|
-
async add(tree, commit) {
|
|
139
|
-
|
|
378
|
+
async add(tree, commit) {
|
|
379
|
+
if (remote) {
|
|
380
|
+
if (!remoteMirror) throw new Error('remote mirror not prepared');
|
|
381
|
+
check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'remote tree clone');
|
|
382
|
+
check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'remote tree fetch');
|
|
383
|
+
check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'remote tree checkout');
|
|
384
|
+
} else check(await command('git', ['worktree', 'add', '--detach', tree, commit], repo), 'git worktree add');
|
|
385
|
+
},
|
|
386
|
+
async remove(tree) {
|
|
387
|
+
if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'remote tree cleanup');
|
|
388
|
+
else check(await command('git', ['worktree', 'remove', '--force', tree], repo), 'git worktree remove');
|
|
389
|
+
},
|
|
140
390
|
async snapshot(tree, commit) {
|
|
141
|
-
|
|
142
|
-
|
|
391
|
+
if (remote) {
|
|
392
|
+
if (!remoteMirror) throw new Error('remote mirror not prepared');
|
|
393
|
+
check(await command('git', ['clone', '--no-checkout', remoteMirror, tree], dirname(tree)), 'baseline snapshot clone');
|
|
394
|
+
check(await command('git', ['-C', tree, 'fetch', 'origin', `refs/elanous/gate/${commit}`], dirname(tree)), 'baseline snapshot fetch');
|
|
395
|
+
check(await command('git', ['-C', tree, 'checkout', '--detach', commit], dirname(tree)), 'baseline snapshot checkout');
|
|
396
|
+
} else {
|
|
397
|
+
check(await command('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, tree], repo), 'baseline snapshot clone');
|
|
398
|
+
check(await command('git', ['checkout', '--quiet', '--detach', commit], tree), 'baseline snapshot checkout');
|
|
399
|
+
}
|
|
143
400
|
},
|
|
144
401
|
async removeSnapshot(tree) {
|
|
145
|
-
if (remote) check(await command('rm', ['-
|
|
402
|
+
if (remote) check(await command('rm', ['-rf', '--', tree], dirname(tree)), 'baseline snapshot cleanup');
|
|
146
403
|
else rmSync(tree, { recursive: true, force: true });
|
|
147
404
|
},
|
|
148
405
|
};
|
|
@@ -179,7 +436,24 @@ function fileOf(id: string): string {
|
|
|
179
436
|
|
|
180
437
|
export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
|
|
181
438
|
const start = Date.now();
|
|
182
|
-
const
|
|
439
|
+
const explicitConfig = getElanousConfigDirOverride();
|
|
440
|
+
const root = explicitConfig ? effectiveInstanceRoot() : (opts.instanceRoot ?? effectiveInstanceRoot());
|
|
441
|
+
const ledger = explicitConfig ? root : (opts.ledgerRoot ?? releaseLedgerRoot());
|
|
442
|
+
const roots = [...new Set([ledger, root])];
|
|
443
|
+
const cachedBaseline = (version: string, commit: string) => {
|
|
444
|
+
for (const [index, location] of roots.entries()) {
|
|
445
|
+
const saved = readBaseline(location, version);
|
|
446
|
+
if (saved?.commit === commit) return { saved, source: index === 0 && location !== root ? 'ledger' as const : 'instance' as const };
|
|
447
|
+
}
|
|
448
|
+
return undefined;
|
|
449
|
+
};
|
|
450
|
+
const baselineReleaseCommit = (version: string) => {
|
|
451
|
+
for (const location of roots) {
|
|
452
|
+
const path = join(location, 'release', version, 'release.json');
|
|
453
|
+
if (existsSync(path)) return baselineCommit(location, version);
|
|
454
|
+
}
|
|
455
|
+
return baselineCommit(root, version);
|
|
456
|
+
};
|
|
183
457
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
184
458
|
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
|
|
185
459
|
let work: string | undefined;
|
|
@@ -191,24 +465,54 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
191
465
|
|| (opts.baselineVersion && !versionPattern.test(opts.baselineVersion))
|
|
192
466
|
|| (opts.baselineCommit && !sha.test(opts.baselineCommit))) throw new Error('invalid commit or version');
|
|
193
467
|
if (!opts.baselineVersion) throw new Error('previous release version required (--baseline-version)');
|
|
468
|
+
if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ?? 24) || (opts.pod.shards ?? 24) < 1
|
|
469
|
+
|| !Number.isSafeInteger(opts.pod.shardTimeoutSeconds ?? 1200) || (opts.pod.shardTimeoutSeconds ?? 1200) < 1)) throw new Error('invalid pod sweep options');
|
|
194
470
|
if (opts.remote && !/^(?:[\w.-]+@)?[\w.-]+$/.test(opts.remote)) throw new Error('invalid ssh host');
|
|
195
471
|
if (opts.remote) {
|
|
196
|
-
|
|
472
|
+
runner.remoteMirror = undefined;
|
|
473
|
+
const configured = opts.remoteMirror ?? defaultRemoteMirror;
|
|
474
|
+
if (!/^(?:\/|~\/)[\w./-]+$/.test(configured) || configured.split('/').includes('..')) throw new Error('invalid remote mirror path');
|
|
475
|
+
const home = configured.startsWith('~/') ? await runner.command('sh', ['-c', 'printf "%s\\n" "$HOME"'], '/tmp') : undefined;
|
|
476
|
+
if (home) check(home, 'remote home');
|
|
477
|
+
const homePath = home?.output.split(/\r?\n/).find((line) => line.startsWith('/'));
|
|
478
|
+
if (home && (!homePath || !/^\/[^\r\n]*$/.test(homePath))) throw new Error('invalid remote home');
|
|
479
|
+
const mirror = configured.startsWith('~/') ? join(homePath!, configured.slice(2)) : configured;
|
|
480
|
+
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
481
|
+
cachedBaseline(opts.baselineVersion, baseSha);
|
|
482
|
+
const present = await runner.command('test', ['-d', mirror], '/tmp');
|
|
483
|
+
if (present.rc === 1) {
|
|
484
|
+
check(await runner.command('mkdir', ['-p', dirname(mirror)], '/tmp'), 'remote mirror parent');
|
|
485
|
+
check(await runner.command('git', ['init', '--bare', mirror], '/tmp'), 'remote mirror init');
|
|
486
|
+
} else check(present, 'remote mirror lookup');
|
|
487
|
+
const bare = await runner.command('git', ['--git-dir', mirror, 'rev-parse', '--is-bare-repository'], '/tmp');
|
|
488
|
+
check(bare, 'remote mirror inspection');
|
|
489
|
+
if (!bare.output.split(/\r?\n/).includes('true')) throw new Error('remote mirror is not bare');
|
|
490
|
+
for (const commit of new Set([opts.commit, baseSha])) {
|
|
491
|
+
const ref = `refs/elanous/gate/${commit}`;
|
|
492
|
+
const found = await runner.command('git', ['--git-dir', mirror, 'show-ref', '--verify', '--quiet', ref], '/tmp');
|
|
493
|
+
if (found.rc === 1) check(await runner.localCommand('git', ['push', `${opts.remote}:${mirror}`, `${commit}:${ref}`], repo), `remote push ${commit}`);
|
|
494
|
+
else check(found, `remote mirror ref ${commit}`);
|
|
495
|
+
}
|
|
496
|
+
runner.remoteMirror = mirror;
|
|
497
|
+
const temporary = await runner.command('mktemp', ['-d', '/tmp/release-gate-XXXXXXXX'], '/tmp');
|
|
197
498
|
check(temporary, 'remote mktemp');
|
|
198
|
-
|
|
499
|
+
const paths = temporary.output.split(/\r?\n/).filter((line) => /^\/tmp\/release-gate-[\w-]+$/.test(line));
|
|
500
|
+
if (paths.length !== 1) throw new Error('could not create temporary work directory');
|
|
501
|
+
work = paths[0]!;
|
|
199
502
|
} 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
503
|
const cutTree = join(work, 'cut');
|
|
202
504
|
trees.push(cutTree);
|
|
203
505
|
await runner.add(cutTree, opts.commit);
|
|
204
506
|
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')
|
|
507
|
+
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
|
|
508
|
+
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
|
|
206
509
|
const cut = failuresOf(cutRun, 'cut sweep');
|
|
207
510
|
cutFailures = cut;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
511
|
+
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
512
|
+
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
513
|
+
const trusted = cached?.saved;
|
|
514
|
+
result.baselineSource = trusted ? cached!.source : 'swept';
|
|
515
|
+
debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
|
|
212
516
|
let baseTree: string | undefined;
|
|
213
517
|
const getBaseTree = async () => {
|
|
214
518
|
if (!baseTree) {
|
|
@@ -234,16 +538,16 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
234
538
|
result.fixed = diff.fixed.length;
|
|
235
539
|
result.preexisting = diff.common.length;
|
|
236
540
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
237
|
-
const isolated = await runner.command('bun', ['run', 'test:deterministic', file], cutTree);
|
|
541
|
+
const isolated = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree);
|
|
238
542
|
const isolatedCut = failuresOf(isolated, `cut isolated ${file}`);
|
|
239
543
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
240
544
|
const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
241
545
|
if (candidates.length === 0) continue;
|
|
242
546
|
const baselineTree = await getBaseTree();
|
|
243
|
-
const previous = await runner.command('bun', ['run', 'test:deterministic', file], baselineTree);
|
|
547
|
+
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree);
|
|
244
548
|
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);
|
|
549
|
+
if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
|
|
550
|
+
const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
|
|
247
551
|
if (lookup.rc !== 0 || lookup.output.trim()) throw new Error(`baseline isolated run incomplete: ${file}`);
|
|
248
552
|
oldFailures = new Set();
|
|
249
553
|
} else {
|
|
@@ -258,6 +562,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
258
562
|
result.outcome = result.introduced.length ? 'regression' : 'ok';
|
|
259
563
|
} catch (error) {
|
|
260
564
|
result.error = error instanceof Error ? error.message : String(error);
|
|
565
|
+
if (error instanceof StalledPodShards) {
|
|
566
|
+
result.stalledShards = error.stalledShards;
|
|
567
|
+
result.partialSummary = error.partialSummary;
|
|
568
|
+
}
|
|
261
569
|
} finally {
|
|
262
570
|
if (baseSnapshot && work) {
|
|
263
571
|
try { await runner.removeSnapshot(join(work, 'baseline')); }
|
|
@@ -269,22 +577,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
269
577
|
}
|
|
270
578
|
if (work) {
|
|
271
579
|
try {
|
|
272
|
-
if (opts.remote) check(await runner.command('
|
|
580
|
+
if (opts.remote) check(await runner.command('rm', ['-rf', '--', work], '/tmp'), 'remote temporary directory cleanup');
|
|
273
581
|
else rmSync(work, { recursive: true, force: true });
|
|
274
582
|
} catch (error) { result.outcome = 'error'; result.error = `cleanup: ${String(error)}`; }
|
|
275
583
|
}
|
|
584
|
+
if (opts.remote) runner.remoteMirror = undefined;
|
|
276
585
|
result.durationMs = Date.now() - start;
|
|
277
586
|
}
|
|
278
587
|
if (result.outcome !== 'error' && cutFailures) {
|
|
279
588
|
try {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
589
|
+
for (const location of roots) {
|
|
590
|
+
const path = failureFile(location, opts.version);
|
|
591
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
592
|
+
const temp = join(dirname(path), `.gate-failures-${process.pid}-${randomUUID()}.tmp`);
|
|
593
|
+
try {
|
|
594
|
+
writeFileSync(temp, JSON.stringify({ commit: opts.commit, failures: cutFailures.failures, ...(cutFailures.errors.length ? { errors: cutFailures.errors } : {}) }, null, 2) + '\n', { mode: 0o600 });
|
|
595
|
+
chmodSync(temp, 0o600);
|
|
596
|
+
renameSync(temp, path);
|
|
597
|
+
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
598
|
+
}
|
|
288
599
|
} catch (error) {
|
|
289
600
|
result.outcome = 'error';
|
|
290
601
|
result.error = `baseline persistence: ${String(error)}`;
|
|
@@ -313,20 +624,30 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
|
|
|
313
624
|
baselineVersion: typeof fromGraph.previousVersion === 'string' ? fromGraph.previousVersion : undefined,
|
|
314
625
|
baselineCommit: typeof fromGraph.previousCommit === 'string' ? fromGraph.previousCommit : undefined,
|
|
315
626
|
remote: typeof fromGraph.gateRemote === 'string' ? fromGraph.gateRemote : undefined,
|
|
627
|
+
remoteMirror: typeof fromGraph.gateRemoteMirror === 'string' ? fromGraph.gateRemoteMirror : undefined,
|
|
628
|
+
pod: typeof fromGraph.gatePodPool === 'string' ? {
|
|
629
|
+
pool: fromGraph.gatePodPool,
|
|
630
|
+
...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
|
|
631
|
+
...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
|
|
632
|
+
} : undefined,
|
|
316
633
|
};
|
|
317
634
|
for (let i = 0; i < args.length; i++) {
|
|
318
635
|
const arg = args[i];
|
|
319
636
|
if (arg === '--help') return 'help';
|
|
320
637
|
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!];
|
|
638
|
+
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
639
|
if (!key) throw new Error(`unknown option: ${arg}`);
|
|
323
640
|
const value = args[++i];
|
|
324
641
|
if (!value || value.startsWith('--')) throw new Error(`value required for ${arg}`);
|
|
325
|
-
if (
|
|
642
|
+
if (arg === '--pod-pool') opts.pod = { ...opts.pod, pool: value };
|
|
643
|
+
else if (arg === '--pod-shards') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shards: Number(value) };
|
|
644
|
+
else if (arg === '--pod-shard-timeout-seconds') opts.pod = { pool: opts.pod?.pool ?? '', ...opts.pod, shardTimeoutSeconds: Number(value) };
|
|
645
|
+
else if (key === 'commit') opts.commit = value;
|
|
326
646
|
else if (key === 'version') opts.version = value;
|
|
327
647
|
else if (key === 'baselineVersion') opts.baselineVersion = value;
|
|
328
648
|
else if (key === 'baselineCommit') opts.baselineCommit = value;
|
|
329
649
|
else if (key === 'remote') opts.remote = value;
|
|
650
|
+
else if (key === 'remoteMirror') opts.remoteMirror = value;
|
|
330
651
|
}
|
|
331
652
|
return opts;
|
|
332
653
|
}
|
|
@@ -338,7 +659,7 @@ if (import.meta.main) {
|
|
|
338
659
|
try {
|
|
339
660
|
const opts = parseOptions(process.argv.slice(2), process.env);
|
|
340
661
|
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.');
|
|
662
|
+
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
663
|
process.exit(0);
|
|
343
664
|
}
|
|
344
665
|
version = opts.version;
|
|
@@ -20,9 +20,13 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
|
|
|
20
20
|
export function runPublish(run: CommandRunner = runCommand) {
|
|
21
21
|
const context = readGraphContext();
|
|
22
22
|
const version = context.input.version;
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// A measured automatic decision or the existing human approval must authorize publication.
|
|
24
|
+
const auto = context.outputs['auto-approve'];
|
|
25
|
+
if (context.outputs['approve-publish']?.outcome !== 'approved' &&
|
|
26
|
+
!(auto?.outcome === 'ok' && auto.decidedBy === 'release-loop metrics'
|
|
27
|
+
&& Array.isArray(auto.metrics) && auto.metrics.length === 8
|
|
28
|
+
&& auto.metrics.every((metric: unknown) => metric && typeof metric === 'object' && 'verdict' in metric && metric.verdict === 'pass'))) {
|
|
29
|
+
return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: neither auto-approve nor approve-publish approved' };
|
|
26
30
|
}
|
|
27
31
|
for (const node of ['gate', 'pwa', 'upgrade', 'tui', 'prepare', 'docs']) {
|
|
28
32
|
if (context.outputs[node]?.outcome !== 'ok') return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `publish blocked: ${node} outcome is not ok` };
|