elanous 0.2.5 → 0.2.7
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/.env.example +10 -7
- package/README.md +9 -4
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 5080-1613752891fcadb4.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +22 -22
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +22 -22
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +22 -22
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +21 -21
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +21 -21
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +21 -21
- package/apps/pwa/out/consult/index.html +1 -0
- package/apps/pwa/out/consult/index.txt +32 -0
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +22 -22
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +22 -22
- package/apps/pwa/out/exec/index.html +1 -0
- package/apps/pwa/out/exec/index.txt +32 -0
- package/apps/pwa/out/field/index.html +1 -0
- package/apps/pwa/out/field/index.txt +32 -0
- 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 +21 -21
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +22 -22
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +21 -21
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +21 -21
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +22 -22
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +22 -22
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +22 -22
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +21 -21
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +22 -22
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +22 -22
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +22 -22
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +22 -22
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +22 -22
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +21 -21
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +22 -22
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +22 -22
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +22 -22
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +22 -22
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +22 -22
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +22 -22
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +22 -22
- package/bin/elanous.cjs +107 -0
- package/graphs/field/field-feed.yaml +24 -0
- package/graphs/field/recipes.yaml +12 -0
- package/graphs/landing/landing-heal.yaml +3 -1
- package/graphs/landing/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +13 -0
- package/graphs/release/release-loop.yaml +13 -4
- package/package.json +5 -5
- package/plugins/card-followup/README.md +42 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +18 -0
- package/plugins/card-followup/graphs/recipes.yaml +16 -0
- package/plugins/card-followup/graphs/run-step.ts +276 -0
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/doc-draft/README.md +13 -0
- package/plugins/doc-draft/examples/brief-sample.json +8 -0
- package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
- package/plugins/doc-draft/graphs/recipes.yaml +9 -0
- package/plugins/doc-draft/graphs/run-step.ts +113 -0
- package/plugins/doc-draft/plugin.json +24 -0
- package/plugins/geo-check/README.md +15 -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 +198 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +31 -1
- package/plugins/job-coach/graphs/run-step.ts +2 -2
- package/plugins/job-coach/plugin.json +1 -1
- package/plugins/job-coach/plugin.ts +1 -1
- package/scripts/affinity/endcard.sh +3 -2
- package/scripts/affinity/reframe.sh +3 -1
- package/scripts/ci-public-export-leak-gate.ts +48 -0
- package/scripts/codex-quota-alert.ts +122 -258
- package/scripts/coord-tracks.json +4 -4
- package/scripts/daemon-port-baseline.txt +1 -2
- package/scripts/field-feed/deliver-node.ts +52 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +162 -0
- package/scripts/field-feed/reel-node.ts +24 -0
- package/scripts/field-feed/start.ts +62 -0
- package/scripts/fixtures/vflow-sample.ndjson +6 -0
- package/scripts/install.sh +92 -13
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +124 -29
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +118 -0
- package/scripts/release-loop/ops-upgrade-node.ts +87 -0
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/ux/record-page.ts +4 -1
- package/scripts/vflow-collect.ts +20 -7
- package/scripts/vflow-search.ts +73 -0
- package/scripts/video-free-line-test-walker.ts +36 -0
- package/scripts/video-pipeline.ts +2 -2
- package/skills/explainer-video/SKILL.md +24 -3
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
- package/skills/explainer-video/engine/build.mjs +2 -2
- package/skills/explainer-video/engine/reel.mjs +235 -0
- package/skills/explainer-video/engine/reel.sh +9 -0
- package/skills/explainer-video/engine/vo.ts +58 -16
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
- package/src/a2a/server.ts +2 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +91 -15
- package/src/agent/monad-agent-turn.ts +25 -6
- package/src/agent/shared-app-tools.ts +5 -3
- package/src/agent-substrate/review-intent.ts +6 -2
- package/src/agent-substrate/shard-boundary-intent.ts +70 -0
- package/src/auth/github-app-token.ts +4 -1
- package/src/autopilot/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +26 -6
- package/src/autopilot/track-courier.ts +2 -2
- package/src/boot/acp-server-help.ts +16 -0
- package/src/boot/daemon-prompt-turn.ts +31 -10
- package/src/boot/daemon-tools/index.ts +7 -1
- package/src/budget/codex-credit-pace.ts +84 -0
- package/src/budget/codex-credit-plan.ts +154 -0
- package/src/budget/codex-reset-credit-state.ts +2 -0
- package/src/chat/chat-cli.ts +4 -1
- package/src/chat/short-question.ts +40 -0
- package/src/cli/agent-cli.ts +24 -15
- package/src/cli/ask-bare.ts +42 -0
- package/src/cli/cli-user-error.ts +25 -0
- package/src/cli/codex-credit-plan-view.ts +25 -0
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-cli.ts +5 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/intake-cli.ts +12 -2
- package/src/cli/intake-source-cli.ts +58 -0
- package/src/cli/machine-cli.ts +23 -0
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +52 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +56 -12
- package/src/cli/release-run-if-ready.ts +48 -0
- package/src/cli/setup-cli.ts +27 -6
- package/src/cli/setup-recommend.ts +8 -0
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +27 -8
- package/src/dashboard/index.ts +1 -1
- package/src/discord.ts +56 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +55 -16
- package/src/exec-requests/planner.ts +140 -0
- package/src/exec-requests/runner.ts +338 -0
- package/src/exec-requests/store.ts +76 -0
- package/src/field/field-media.ts +250 -0
- package/src/field/field-reel.ts +196 -0
- package/src/grok/credential.ts +7 -5
- package/src/harness/aside-run-status.ts +33 -0
- package/src/harness/browser-verify.ts +48 -17
- package/src/harness/harness-cli-command.ts +25 -3
- package/src/harness/image-load-state.ts +21 -0
- package/src/harness/process-garbage.ts +45 -0
- package/src/index.ts +104 -38
- package/src/instance/leader.ts +13 -1
- package/src/intake-plane/collect-github.ts +5 -3
- package/src/intake-plane/collect-telegram-saved.ts +15 -3
- package/src/intake-plane/discord-seat-work.ts +56 -0
- package/src/intake-plane/intake-sources.ts +202 -0
- package/src/intake-plane/items.ts +28 -12
- package/src/intake-plane/seat-doc-route.ts +207 -0
- package/src/intake-plane/submit-intake-work.ts +9 -1
- package/src/intake-plane/telegram-seat-work.ts +72 -0
- package/src/llm/model-defaults.ts +24 -1
- package/src/llm.ts +5 -4
- package/src/machines/machine-ledger.ts +127 -0
- package/src/market/pack-internal-refs.ts +28 -0
- package/src/market/publish.ts +31 -4
- package/src/maturity/feature-maturity.ts +65 -0
- package/src/model-tier/turn-tier.ts +24 -0
- package/src/msg/msg-store.ts +149 -0
- package/src/native-tool-catalog.ts +2 -1
- package/src/nexus/api/chat-fast-path-config.ts +28 -0
- package/src/nexus/api/consult-requests.ts +117 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +180 -0
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +120 -4
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +68 -5
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +3 -0
- package/src/nexus/setup-status.ts +4 -5
- package/src/notify/llm-budget-alert.ts +125 -0
- package/src/oauth/codex-account-rotation.ts +26 -4
- package/src/oauth/codex-account-store.ts +20 -2
- package/src/oauth/codex-browser-login.ts +173 -0
- package/src/oauth/codex.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +29 -0
- package/src/onboarding/entry-hints.ts +8 -0
- package/src/onboarding/full-screen-io.ts +35 -3
- package/src/onboarding/help/skills.md +1 -1
- package/src/onboarding/non-interactive.ts +1 -0
- package/src/onboarding/subscription-hint.ts +27 -0
- package/src/onboarding/web-first.ts +83 -0
- package/src/onboarding.ts +230 -23
- package/src/plugins/maker/node-install.ts +92 -0
- package/src/plugins/maker/node-maker.ts +139 -0
- package/src/plugins/maker/plugin-maker.ts +220 -0
- package/src/policy/llm-policy.ts +268 -0
- package/src/prompt-library/session-guidance.ts +7 -1
- package/src/prompt-library/universal-preamble.ts +2 -2
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +36 -9
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/run-record-mirror.ts +44 -0
- package/src/self-dev/run-store.ts +12 -3
- package/src/self-implement/harness-policy-gates.ts +4 -1
- package/src/self-implement/report-origin.ts +12 -1
- package/src/self-implement/run-ledger-compact.ts +1 -1
- package/src/session/chat.ts +17 -5
- package/src/session/index.ts +23 -10
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/steward/failure-streak.ts +78 -0
- package/src/steward/steward-cards.ts +24 -0
- package/src/steward/triage-budget.ts +127 -0
- package/src/steward/triage-plan.ts +75 -0
- package/src/steward/triage.ts +244 -42
- package/src/task-cards/card-store.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
- package/src/telegram.ts +145 -2
- package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
- package/src/user-config.ts +84 -4
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
- package/src/video-pipeline/recipes/free-line.ts +16 -3
- package/src/video-pipeline/recipes/remote.ts +7 -1
- package/src/video-pipeline/vflow-reference.ts +109 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
|
@@ -7,14 +7,18 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
8
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
9
9
|
import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
10
|
-
import { diffFailures, parseFailures } from './gate-diff';
|
|
10
|
+
import { diffFailures, junitFailures, parseFailures } from './gate-diff';
|
|
11
|
+
import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
|
|
12
|
+
import { baselineFromCutLogs } from './baseline-from-logs';
|
|
13
|
+
import { planShards, readFileDurations } from './shard-plan';
|
|
11
14
|
import { deriveCdpTestPatterns } from '../test-deterministic';
|
|
12
15
|
import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
13
16
|
import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
17
|
+
import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
|
|
14
18
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
15
19
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
16
20
|
|
|
17
|
-
interface CommandResult { rc: number; output: string }
|
|
21
|
+
interface CommandResult { rc: number; output: string; passedIds?: string[] }
|
|
18
22
|
export interface GateRunner {
|
|
19
23
|
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
20
24
|
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
@@ -36,7 +40,7 @@ export interface GateOptions {
|
|
|
36
40
|
instanceRoot?: string;
|
|
37
41
|
ledgerRoot?: string;
|
|
38
42
|
repo?: string;
|
|
39
|
-
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number };
|
|
43
|
+
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string };
|
|
40
44
|
}
|
|
41
45
|
export interface GateResult {
|
|
42
46
|
outcome: 'ok' | 'regression' | 'error';
|
|
@@ -44,7 +48,9 @@ export interface GateResult {
|
|
|
44
48
|
introduced: string[];
|
|
45
49
|
preexisting: number;
|
|
46
50
|
fixed: number;
|
|
47
|
-
|
|
51
|
+
knownEnv: number;
|
|
52
|
+
knownEnvCleared: string[];
|
|
53
|
+
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
48
54
|
durationMs: number;
|
|
49
55
|
error?: string;
|
|
50
56
|
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
@@ -62,6 +68,38 @@ function check(result: CommandResult, label: string): void {
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
type SweepFailures = { failures: string[]; errors: string[] };
|
|
71
|
+
|
|
72
|
+
/** Only named, completed JUnit cases can prove a listed failure passed in this cut. */
|
|
73
|
+
function junitPassedIds(xml: string): string[] {
|
|
74
|
+
const passed = new Set<string>();
|
|
75
|
+
const suites: string[] = [];
|
|
76
|
+
let file: string | undefined;
|
|
77
|
+
let open: { id: string; passed: boolean } | undefined;
|
|
78
|
+
const decode = (value: string) => value.replace(/&(?:quot|apos|lt|gt|amp|#\d+|#x[\da-fA-F]+);/g, (entity) => {
|
|
79
|
+
const named: Record<string, string> = { '"': '"', ''': "'", '<': '<', '>': '>', '&': '&' };
|
|
80
|
+
if (entity in named) return named[entity]!;
|
|
81
|
+
const code = entity.startsWith('&#x') ? Number.parseInt(entity.slice(3, -1), 16) : Number(entity.slice(2, -1));
|
|
82
|
+
return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity;
|
|
83
|
+
});
|
|
84
|
+
for (const match of xml.matchAll(/<(\/?)\s*(testsuite|testcase|failure|error|skipped)\b([^>]*?)(\/?)>/g)) {
|
|
85
|
+
const [, closing, tag, attrs, selfClosing] = match;
|
|
86
|
+
const attr = (name: string) => { const found = new RegExp(`\\b${name}="([^"]*)"`).exec(attrs!); return found ? decode(found[1]!) : undefined; };
|
|
87
|
+
if (tag === 'testsuite') {
|
|
88
|
+
if (closing) { suites.pop(); if (!suites.length) file = undefined; }
|
|
89
|
+
else if (!selfClosing) { suites.push(attr('name') ?? ''); if (suites.length === 1) file = attr('file') ?? attr('name'); }
|
|
90
|
+
} else if (tag === 'testcase') {
|
|
91
|
+
if (closing) { if (open?.passed) passed.add(open.id); open = undefined; }
|
|
92
|
+
else {
|
|
93
|
+
const name = [...suites.slice(1), attr('name') ?? ''].join(' > ');
|
|
94
|
+
const path = attr('file') ?? file;
|
|
95
|
+
const id = path && name ? `${path} > ${name}` : undefined;
|
|
96
|
+
if (selfClosing) { if (id) passed.add(id); }
|
|
97
|
+
else open = id ? { id, passed: true } : undefined;
|
|
98
|
+
}
|
|
99
|
+
} else if (!closing && open) open.passed = false;
|
|
100
|
+
}
|
|
101
|
+
return [...passed].sort();
|
|
102
|
+
}
|
|
65
103
|
// A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
|
|
66
104
|
// (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
|
|
67
105
|
// the sweep's own summary is always the «last» one, so read the last match, never the first.
|
|
@@ -74,6 +112,11 @@ const summaryCount = (output: string, label: string) => Number(lastMatch(output,
|
|
|
74
112
|
/** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
|
|
75
113
|
const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
|
|
76
114
|
export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
|
|
115
|
+
/** Integration tests that the Pod sweep does not run — each is judged by a real run elsewhere at the cut.
|
|
116
|
+
* 0.2.7 run 5 (10-01): `scripts/install.test.ts` (real `bun pm pack` → `bun add` installs, 301 s alone on node-b)
|
|
117
|
+
* sat in a shard of its own past the 1200 s deadline with no output and stopped the whole gate. Judged on the
|
|
118
|
+
* mbp at the cut instead (TD1 moves it to the integration line). Keep this list short and explained. */
|
|
119
|
+
export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
|
|
77
120
|
const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
|
|
78
121
|
const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
|
|
79
122
|
let last: string | undefined;
|
|
@@ -90,10 +133,15 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
|
90
133
|
const reportedErrors = summaryCount(output, 'error');
|
|
91
134
|
const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
|
|
92
135
|
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)}`);
|
|
93
|
-
if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (run.rc !== 0 && run.rc !== 1)) throw incomplete('summary/exit');
|
|
94
136
|
const count = summaryCount(output, 'fail');
|
|
95
137
|
const failures = parseFailures(output);
|
|
96
138
|
const occurrences = [...output.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
|
|
139
|
+
// A test that leaves process.exitCode set leaks a non-zero exit into a run whose own summary is complete and clean
|
|
140
|
+
// (0.2.7 gate: agent-cli at v0.2.6 · 12 pass · 0 fail · rc 2). The summary is the verdict; read that exit as clean.
|
|
141
|
+
const leakedExit = !!ran && run.rc !== 0 && run.rc !== 1 && count === 0 && errorCount === 0 && occurrences === 0
|
|
142
|
+
&& !/^# Unhandled error between tests/m.test(output);
|
|
143
|
+
const rc = leakedExit ? 0 : run.rc;
|
|
144
|
+
if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (rc !== 0 && rc !== 1)) throw incomplete('summary/exit');
|
|
97
145
|
// Bun reports file-level unhandled errors separately from test failures.
|
|
98
146
|
const errors: string[] = [];
|
|
99
147
|
let file: string | undefined;
|
|
@@ -108,7 +156,7 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
|
108
156
|
}
|
|
109
157
|
if (errors.length !== errorCount) throw incomplete(`error attribution identified=${errors.length}`);
|
|
110
158
|
if (!Number.isFinite(count) || count !== occurrences || (count > 0 && failures.length === 0)
|
|
111
|
-
|| (
|
|
159
|
+
|| (rc === 1 && count === 0 && errorCount === 0) || (rc === 0 && count + errorCount > 0)) {
|
|
112
160
|
throw incomplete(`failure attribution incomplete: summary=${count}, identified=${failures.length}`);
|
|
113
161
|
}
|
|
114
162
|
return { failures, errors: [...new Set(errors)].sort() };
|
|
@@ -134,7 +182,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
134
182
|
}
|
|
135
183
|
: localCommand);
|
|
136
184
|
const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
|
|
137
|
-
const shardCount = pod.shards ??
|
|
185
|
+
const shardCount = pod.shards ?? 24;
|
|
138
186
|
const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
|
|
139
187
|
if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
|
|
140
188
|
|| !Number.isSafeInteger(deadlineSeconds) || deadlineSeconds < 1) throw new Error('invalid pod sweep options');
|
|
@@ -154,13 +202,27 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
154
202
|
const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
|
|
155
203
|
// CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
|
|
156
204
|
// bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
|
|
157
|
-
const assignable = files.filter((file) => !cdpPatterns.includes(file) && !
|
|
205
|
+
const assignable = files.filter((file) => !cdpPatterns.includes(file) && !POD_SWEEP_INTEGRATION_ONLY.includes(file)
|
|
206
|
+
&& !file.split('/').some((part) => part.startsWith('.')));
|
|
207
|
+
const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
|
|
208
|
+
if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
|
|
158
209
|
if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
|
|
159
|
-
const
|
|
160
|
-
|
|
210
|
+
const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
|
|
211
|
+
const shards = planShards(assignable, durations, shardCount);
|
|
212
|
+
const known = assignable.filter((file) => durations.has(file)).length;
|
|
213
|
+
debug.log('release-loop.gate', 'pod-shard-plan', {
|
|
214
|
+
shards: shards.length, known, unknown: assignable.length - known,
|
|
215
|
+
maxPlannedSeconds: Math.max(...shards.map((shard) => shard.plannedSeconds)),
|
|
216
|
+
source: durations.size ? pod.durationSource : 'none',
|
|
217
|
+
});
|
|
161
218
|
const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
|
|
162
219
|
if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
|
|
163
220
|
const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
|
|
221
|
+
const configCache = typeof pod.bunCache === 'string' ? pod.bunCache.trim() : '';
|
|
222
|
+
const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
|
|
223
|
+
const bunCache = configCache || envCache || undefined;
|
|
224
|
+
debug.log('release-loop.gate', 'pod-bun-cache', { source: configCache ? 'config' : envCache ? 'env' : 'none' });
|
|
225
|
+
const cachePrefix = bunCache ? `${podBunCacheVolume(bunCache).shellPrefix} ` : '';
|
|
164
226
|
const jobLogTail = (job: PodCommandResult): string => {
|
|
165
227
|
if (podLogTail) return podLogTail(job);
|
|
166
228
|
for (const member of poolScheduler.members) {
|
|
@@ -169,10 +231,16 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
169
231
|
}
|
|
170
232
|
return '';
|
|
171
233
|
};
|
|
172
|
-
type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
|
|
234
|
+
type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number; passedIds: string[] };
|
|
173
235
|
type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
|
|
174
236
|
// 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
|
|
175
|
-
const isolationRemaining = shards.map((
|
|
237
|
+
const isolationRemaining = shards.map((item) => item.files.length);
|
|
238
|
+
const estimated = (paths: string[]) => paths.reduce((sum, file) => {
|
|
239
|
+
const value = durations.get(file);
|
|
240
|
+
return sum + (value !== undefined && Number.isFinite(value) && value >= 0 ? value : unknownSeconds);
|
|
241
|
+
}, 0);
|
|
242
|
+
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);
|
|
243
|
+
const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
|
|
176
244
|
const runShard = async (paths: string[], shard: number, depth = 0, branch = ''): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
|
|
177
245
|
const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
|
|
178
246
|
.flatMap((pattern) => ['--path-ignore-patterns', pattern]);
|
|
@@ -189,10 +257,11 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
189
257
|
try {
|
|
190
258
|
const job = await podCommand({
|
|
191
259
|
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
260
|
+
...(bunCache ? { bunCache } : {}),
|
|
192
261
|
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
193
262
|
...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
194
263
|
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"`],
|
|
264
|
+
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"`],
|
|
196
265
|
});
|
|
197
266
|
jobExitCode = job.exitCode;
|
|
198
267
|
const logPath = join(job.artifactsDir, 'shard.log');
|
|
@@ -216,13 +285,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
216
285
|
const destination = join(logDir, `pod-${shard}${branch}.log`);
|
|
217
286
|
if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
|
|
218
287
|
else rmSync(destination, { force: true });
|
|
219
|
-
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths }) + '\n', { mode: 0o600 });
|
|
288
|
+
writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths), shardCount: shards.length, commit }) + '\n', { mode: 0o600 });
|
|
220
289
|
const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
|
|
221
290
|
if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
|
|
222
291
|
else rmSync(junitDestination, { force: true });
|
|
223
292
|
}
|
|
224
|
-
debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1 });
|
|
225
|
-
|
|
293
|
+
debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
|
|
294
|
+
let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
226
295
|
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
227
296
|
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
228
297
|
const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
|
|
@@ -243,6 +312,21 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
243
312
|
unattributedDetail = (error instanceof Error ? error.message : String(error)).split('\n', 1)[0]!.slice(0, 300);
|
|
244
313
|
}
|
|
245
314
|
if (reason === 'unattributed') namedFailures = [...clean.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
|
|
315
|
+
// 콘솔 요약은 실패를 세는데 `(fail)` 이름 줄이 하나도 없으면 같은 조각의 junit 에서 이름을 가져온다
|
|
316
|
+
// (10-01 0.2.6 게이트: `--dots` 로 도는 한 파일 조각이 이름 없이 끝나 게이트 전체가 error 로 멈췄다).
|
|
317
|
+
if (reason === 'unattributed' && namedFailures === 0 && junit) {
|
|
318
|
+
const named = junitFailures(junit);
|
|
319
|
+
if (named.length === fails) {
|
|
320
|
+
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`;
|
|
321
|
+
try {
|
|
322
|
+
if (failuresOf({ rc, output: patched }, `pod-${shard} shard`).failures.length === fails) {
|
|
323
|
+
clean = patched;
|
|
324
|
+
reason = undefined;
|
|
325
|
+
debug.log('release-loop.gate', 'pod-shard-junit-attribution', { shard, files: paths, failures: named });
|
|
326
|
+
}
|
|
327
|
+
} catch { /* 그래도 못 맞추면 원래대로 멈춘다 */ }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
246
330
|
}
|
|
247
331
|
if (reason) {
|
|
248
332
|
// 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
|
|
@@ -276,10 +360,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
276
360
|
}
|
|
277
361
|
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
278
362
|
errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
|
|
279
|
-
ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
|
|
363
|
+
ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
280
364
|
};
|
|
281
365
|
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
282
|
-
const settled = await Promise.allSettled(shards.map((
|
|
366
|
+
const settled = await Promise.allSettled(shards.map((item, shard) => runShard(item.files, shard)));
|
|
283
367
|
const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
284
368
|
if (rejected) throw rejected.reason;
|
|
285
369
|
const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
@@ -291,7 +375,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
291
375
|
if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
|
|
292
376
|
if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
|
|
293
377
|
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
|
|
378
|
+
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`,
|
|
379
|
+
passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort() };
|
|
295
380
|
};
|
|
296
381
|
return {
|
|
297
382
|
command,
|
|
@@ -423,7 +508,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
423
508
|
return baselineCommit(root, version);
|
|
424
509
|
};
|
|
425
510
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
426
|
-
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
|
|
511
|
+
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
|
|
427
512
|
let work: string | undefined;
|
|
428
513
|
let baseSnapshot = false;
|
|
429
514
|
let cutFailures: SweepFailures | undefined;
|
|
@@ -433,7 +518,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
433
518
|
|| (opts.baselineVersion && !versionPattern.test(opts.baselineVersion))
|
|
434
519
|
|| (opts.baselineCommit && !sha.test(opts.baselineCommit))) throw new Error('invalid commit or version');
|
|
435
520
|
if (!opts.baselineVersion) throw new Error('previous release version required (--baseline-version)');
|
|
436
|
-
if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ??
|
|
521
|
+
if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ?? 24) || (opts.pod.shards ?? 24) < 1
|
|
437
522
|
|| !Number.isSafeInteger(opts.pod.shardTimeoutSeconds ?? 1200) || (opts.pod.shardTimeoutSeconds ?? 1200) < 1)) throw new Error('invalid pod sweep options');
|
|
438
523
|
if (opts.remote && !/^(?:[\w.-]+@)?[\w.-]+$/.test(opts.remote)) throw new Error('invalid ssh host');
|
|
439
524
|
if (opts.remote) {
|
|
@@ -472,13 +557,15 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
472
557
|
trees.push(cutTree);
|
|
473
558
|
await runner.add(cutTree, opts.commit);
|
|
474
559
|
for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
|
|
475
|
-
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
|
|
560
|
+
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
|
|
561
|
+
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
|
|
476
562
|
const cut = failuresOf(cutRun, 'cut sweep');
|
|
477
563
|
cutFailures = cut;
|
|
478
564
|
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
479
565
|
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
480
|
-
const
|
|
481
|
-
|
|
566
|
+
const recorded = cached ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), baseSha);
|
|
567
|
+
const trusted = cached?.saved ?? (recorded?.complete ? recorded : undefined);
|
|
568
|
+
result.baselineSource = cached ? cached.source : recorded?.complete ? 'cut-logs' : 'swept';
|
|
482
569
|
debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
|
|
483
570
|
let baseTree: string | undefined;
|
|
484
571
|
const getBaseTree = async () => {
|
|
@@ -501,7 +588,14 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
501
588
|
baseline = failuresOf(baseRun, 'baseline sweep');
|
|
502
589
|
}
|
|
503
590
|
for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
|
|
504
|
-
const
|
|
591
|
+
const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
|
|
592
|
+
const cutIds = [...cut.failures, ...cut.errors];
|
|
593
|
+
const baselineIds = [...baseline.failures, ...baseline.errors];
|
|
594
|
+
const countedCut = splitKnownEnv(cutIds, known);
|
|
595
|
+
const countedBaseline = splitKnownEnv(baselineIds, known);
|
|
596
|
+
result.knownEnv = countedCut.knownEnv.length;
|
|
597
|
+
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
|
|
598
|
+
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
505
599
|
result.fixed = diff.fixed.length;
|
|
506
600
|
result.preexisting = diff.common.length;
|
|
507
601
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
@@ -551,7 +645,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
551
645
|
if (opts.remote) runner.remoteMirror = undefined;
|
|
552
646
|
result.durationMs = Date.now() - start;
|
|
553
647
|
}
|
|
554
|
-
if (
|
|
648
|
+
if (cutFailures && !result.stalledShards?.length) {
|
|
555
649
|
try {
|
|
556
650
|
for (const location of roots) {
|
|
557
651
|
const path = failureFile(location, opts.version);
|
|
@@ -575,10 +669,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
575
669
|
export function graphGateResult(result: GateResult, graph: boolean) {
|
|
576
670
|
return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
|
|
577
671
|
verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
|
|
578
|
-
summary: result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}` };
|
|
672
|
+
summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}` };
|
|
579
673
|
}
|
|
580
674
|
|
|
581
|
-
function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
|
|
675
|
+
export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
|
|
582
676
|
const context = env.ELANOUS_GRAPH_CONTEXT && args.every((arg) => arg === '--json') ? readGraphContext(env) : undefined;
|
|
583
677
|
const fromGraph: Record<string, unknown> = context?.input ?? {};
|
|
584
678
|
const graphCommit = context?.outputs['version-release']?.commit;
|
|
@@ -596,6 +690,7 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
|
|
|
596
690
|
pool: fromGraph.gatePodPool,
|
|
597
691
|
...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
|
|
598
692
|
...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
|
|
693
|
+
...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
|
|
599
694
|
} : undefined,
|
|
600
695
|
};
|
|
601
696
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -632,7 +727,7 @@ if (import.meta.main) {
|
|
|
632
727
|
version = opts.version;
|
|
633
728
|
result = await judgeGate(opts);
|
|
634
729
|
} catch (error) {
|
|
635
|
-
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, durationMs: Date.now() - start, error: String(error) };
|
|
730
|
+
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
|
|
636
731
|
}
|
|
637
732
|
if (result.error) console.error(result.error);
|
|
638
733
|
debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { emitNodeResult, readGraphContext, runCommand, type CommandRunner, type GraphContext } from './node-verdict.js';
|
|
5
|
+
|
|
6
|
+
const FALLBACK = 'Known issues have been reported for this release.';
|
|
7
|
+
// No fix schedule is promised here — the inputs do not carry one (review R2).
|
|
8
|
+
const INTRO = 'The following issues are known in this release.';
|
|
9
|
+
// A known-issues bullet describes a problem that is still there; «fixed / resolved» wording turns it into a false claim.
|
|
10
|
+
const resolved = /\b(?:fixed|resolved|no longer|has been addressed|is addressed|was addressed)\b/i;
|
|
11
|
+
const unsafe = /[^\x20-\x7e]|#\d+|\/Users\/|\.test\.ts\b|\[(?:TC|MK|OP|UX|S|T|F|O)\]|\b(?:TC|MK|OP|UX)\s+track\b|\b(?:PR|pull request)\s*\d+\b|\b(?:scripts|src|test)\/\S+|\bfile:\/\/|(?<![\w:/])\/(?!\/)[\w.-]+(?:\/[\w.-]+)*|\b[A-Z]:\\(?:[^\s\\]+\\)*[^\s\\]+|\b(?:RFC|HANDOFF|MANUAL)-/i;
|
|
12
|
+
|
|
13
|
+
type Issue = { id: string; title?: string; evidence?: string; note?: string };
|
|
14
|
+
|
|
15
|
+
function issues(value: unknown, kind: 'checklist' | 'accepted'): Issue[] {
|
|
16
|
+
if (value === undefined) return [];
|
|
17
|
+
if (!Array.isArray(value) || !value.every((item) => item && typeof item === 'object'
|
|
18
|
+
&& typeof item.id === 'string' && item.id.trim()
|
|
19
|
+
&& (kind === 'checklist' ? typeof item.title === 'string' && typeof item.evidence === 'string' : typeof item.note === 'string'))) {
|
|
20
|
+
throw new Error(`invalid ${kind} known issues`);
|
|
21
|
+
}
|
|
22
|
+
return value as Issue[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generatedBullets(raw: string, sourceIds: string[]): string[] {
|
|
26
|
+
const envelope: unknown = JSON.parse(raw.trim());
|
|
27
|
+
const reply = envelope && typeof envelope === 'object' && !Array.isArray(envelope) && 'reply' in envelope
|
|
28
|
+
? envelope.reply : envelope;
|
|
29
|
+
if (typeof reply !== 'string') throw new Error('empty ask reply');
|
|
30
|
+
const entries: unknown = JSON.parse(reply.replace(/^```(?:json)?\s*\n|\n```\s*$/g, '').trim());
|
|
31
|
+
if (!Array.isArray(entries) || entries.length !== sourceIds.length || new Set(sourceIds).size !== sourceIds.length) {
|
|
32
|
+
throw new Error('known issues response does not cover every input ID');
|
|
33
|
+
}
|
|
34
|
+
const byId = new Map<string, string>();
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
37
|
+
|| typeof entry.id !== 'string' || typeof entry.bullet !== 'string'
|
|
38
|
+
|| byId.has(entry.id) || !sourceIds.includes(entry.id)) {
|
|
39
|
+
throw new Error('known issues response does not cover every input ID');
|
|
40
|
+
}
|
|
41
|
+
byId.set(entry.id, entry.bullet);
|
|
42
|
+
}
|
|
43
|
+
if (sourceIds.some((id) => !byId.has(id))) throw new Error('known issues response does not cover every input ID');
|
|
44
|
+
return sourceIds.map((id) => byId.get(id)!);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function coversIssue(bullet: string, issue: Issue, others: Issue[]): boolean {
|
|
48
|
+
const source = issue.title ?? issue.note ?? '';
|
|
49
|
+
const words = source.toLowerCase().match(/[a-z]{5,}/g) ?? [];
|
|
50
|
+
const otherWords = new Set(others.flatMap((other) => ((other.title ?? other.note ?? '').toLowerCase().match(/[a-z]{5,}/g) ?? []).map((word) => word.slice(0, 5))));
|
|
51
|
+
const publicWords = new Set((bullet.toLowerCase().match(/[a-z]{5,}/g) ?? []).map((word) => word.slice(0, 5)));
|
|
52
|
+
const distinctive = [...new Set(words.map((word) => word.slice(0, 5)))].filter((word) => !otherWords.has(word));
|
|
53
|
+
const negative = /\b(?:not|never|no|cannot|can't|doesn't|don't|won't|fails? to|isn't|aren't)\b/i;
|
|
54
|
+
if (negative.test(source) && !negative.test(bullet)) return false;
|
|
55
|
+
// A source with no English words to compare (e.g. a Korean checklist title) is covered structurally —
|
|
56
|
+
// one bullet per input ID — instead of blocking the release (review R2).
|
|
57
|
+
if (words.length === 0) return true;
|
|
58
|
+
return distinctive.length > 0 && distinctive.filter((word) => publicWords.has(word)).length >= Math.min(issue.note === undefined ? 1 : 2, distinctive.length);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function cleanBullet(text: string): string | null {
|
|
62
|
+
const line = text.trim();
|
|
63
|
+
if (!line || /^[-*]\s/.test(line) || unsafe.test(line) || /[{}<>\[\]`]/.test(line) || (line.match(/[.!?](?:\s|$)/g) ?? []).length > 2) return null;
|
|
64
|
+
return line;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function command(run: CommandRunner, program: string, args: string[], cwd?: string): string {
|
|
68
|
+
const result = run(program, args, cwd);
|
|
69
|
+
if (result.status !== 0) throw new Error(`${program} ${args[0]} failed: ${result.stderr || result.stdout}`);
|
|
70
|
+
return result.stdout;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function runKnownIssues(context: GraphContext = readGraphContext(), run: CommandRunner = runCommand) {
|
|
74
|
+
const version = context.input.version;
|
|
75
|
+
const worktree = context.outputs.docs?.worktree;
|
|
76
|
+
if (typeof worktree !== 'string' || !worktree || !existsSync(worktree)) throw new Error('docs 워크트리 없음');
|
|
77
|
+
if (context.outputs.docs?.branch !== `release-docs/${version}`) throw new Error('docs branch/version mismatch');
|
|
78
|
+
const notes = join(worktree, 'release/public/docs/releases', `${version}.md`);
|
|
79
|
+
if (!existsSync(notes)) throw new Error(`release notes missing: ${notes}`);
|
|
80
|
+
const checklist = issues(context.outputs['checklist-gate']?.knownIssues, 'checklist');
|
|
81
|
+
const accepted = issues(context.input.acceptedRegressions, 'accepted');
|
|
82
|
+
const sources = { checklist: checklist.length, accepted: accepted.length };
|
|
83
|
+
if (!checklist.length && !accepted.length) return { outcome: 'ok' as const, verdict: 'pass' as const, summary: '알려진 문제 없음', count: 0, bullets: [] as string[], sources, fallbackUsed: false };
|
|
84
|
+
|
|
85
|
+
let bullets: string[];
|
|
86
|
+
let fallbackUsed = false;
|
|
87
|
+
const allIssues = [...checklist, ...accepted];
|
|
88
|
+
const sourceIds = allIssues.map((issue) => issue.id);
|
|
89
|
+
const prompt = `Write public English release-note bullets for these known issues. Describe only user-visible impact; include a one-line workaround when available. No internal names, test paths, track names, PR numbers, or people. Each bullet must have at most two sentences. Return ONLY a JSON array with exactly one object {"id":"input ID","bullet":"English public sentence"} for each input, using each input ID exactly once. Do not include IDs in bullet text. Input: ${JSON.stringify({ checklist, acceptedRegressions: accepted })}`;
|
|
90
|
+
let response: ReturnType<CommandRunner>;
|
|
91
|
+
try {
|
|
92
|
+
response = run('elanous', ['--test', 'ask', '--json', prompt]);
|
|
93
|
+
} catch {
|
|
94
|
+
response = { status: 1, stdout: '', stderr: '' };
|
|
95
|
+
}
|
|
96
|
+
if (response.status !== 0) {
|
|
97
|
+
bullets = [FALLBACK];
|
|
98
|
+
fallbackUsed = true;
|
|
99
|
+
} else {
|
|
100
|
+
try {
|
|
101
|
+
bullets = generatedBullets(response.stdout, sourceIds).map((line, index) => {
|
|
102
|
+
const safe = cleanBullet(line);
|
|
103
|
+
if (!safe) {
|
|
104
|
+
fallbackUsed = true;
|
|
105
|
+
return FALLBACK;
|
|
106
|
+
}
|
|
107
|
+
if (resolved.test(safe)) throw new Error(`bullet for ${sourceIds[index]} claims the issue is resolved`);
|
|
108
|
+
if (!coversIssue(safe, allIssues[index]!, allIssues.filter((_, otherIndex) => otherIndex !== index))) {
|
|
109
|
+
throw new Error(`bullet does not identify input ${sourceIds[index]}`);
|
|
110
|
+
}
|
|
111
|
+
return safe;
|
|
112
|
+
});
|
|
113
|
+
if (new Set(bullets).size !== bullets.length && bullets.some((bullet) => bullet !== FALLBACK)) {
|
|
114
|
+
throw new Error('known issues response repeats an issue instead of covering each input');
|
|
115
|
+
}
|
|
116
|
+
bullets = [...new Set(bullets)];
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`known issues coverage not verified: ${error instanceof Error ? error.message : String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const original = readFileSync(notes, 'utf8');
|
|
122
|
+
const heading = /^## Known issues\s*$/m;
|
|
123
|
+
const match = heading.exec(original);
|
|
124
|
+
let updated: string;
|
|
125
|
+
if (match) {
|
|
126
|
+
const start = match.index + match[0].length;
|
|
127
|
+
const next = /^## /gm;
|
|
128
|
+
next.lastIndex = start;
|
|
129
|
+
const end = next.exec(original)?.index ?? original.length;
|
|
130
|
+
const section = original.slice(start, end);
|
|
131
|
+
const additions = bullets.filter((bullet) => !section.split(/\r?\n/).some((line) => line.trim() === `- ${bullet}`));
|
|
132
|
+
const before = original.slice(0, end);
|
|
133
|
+
updated = additions.length ? `${before}${before.endsWith('\n') ? '' : '\n'}${additions.map((bullet) => `- ${bullet}`).join('\n')}\n\n${original.slice(end)}` : original;
|
|
134
|
+
} else {
|
|
135
|
+
updated = `${original.trimEnd()}\n\n## Known issues\n\n${INTRO}\n\n${bullets.map((bullet) => `- ${bullet}`).join('\n')}\n`;
|
|
136
|
+
}
|
|
137
|
+
if (updated !== original) {
|
|
138
|
+
writeFileSync(notes, updated);
|
|
139
|
+
command(run, 'git', ['add', '--', `release/public/docs/releases/${version}.md`], worktree);
|
|
140
|
+
command(run, 'git', ['commit', '-m', `docs: known issues for ${version}`], worktree);
|
|
141
|
+
}
|
|
142
|
+
// Push whenever the branch is ahead of its remote — a rerun after a failed push finds the bullets already in the file (review R2).
|
|
143
|
+
command(run, 'git', ['fetch', 'origin', `release-docs/${version}`], worktree);
|
|
144
|
+
const ahead = Number(command(run, 'git', ['rev-list', '--count', `origin/release-docs/${version}..HEAD`], worktree).trim());
|
|
145
|
+
if (!Number.isFinite(ahead)) throw new Error('could not compare release-docs branch with its remote');
|
|
146
|
+
if (ahead > 0) command(run, 'git', ['push', 'origin', `release-docs/${version}`], worktree);
|
|
147
|
+
return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `known issues: ${bullets.length}`, count: bullets.length, bullets, sources, fallbackUsed };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (import.meta.main) {
|
|
151
|
+
try {
|
|
152
|
+
const result = runKnownIssues();
|
|
153
|
+
emitNodeResult(result);
|
|
154
|
+
process.exitCode = 0;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: error instanceof Error ? error.message : String(error), count: 0, bullets: [], sources: { checklist: 0, accepted: 0 }, fallbackUsed: false });
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { debug } from '../../src/debug/log.js';
|
|
7
|
+
import { sendOutbound } from '../../src/domains/outbound-alert.js';
|
|
8
|
+
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
9
|
+
import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
|
|
10
|
+
|
|
11
|
+
type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
|
|
12
|
+
type Exec = (command: string, args: string[], env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
|
|
13
|
+
type Registry = (url: string) => Promise<{ status: number; body: unknown }>;
|
|
14
|
+
export interface NpmPublishDeps {
|
|
15
|
+
exec?: Exec;
|
|
16
|
+
registry?: Registry;
|
|
17
|
+
sleep?: (ms: number) => Promise<void>;
|
|
18
|
+
stateDir?: string;
|
|
19
|
+
alert?: (text: string, kind: string) => boolean;
|
|
20
|
+
log?: (version: string, waitedMinutes: number) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const exec: Exec = (command, args, env) => {
|
|
24
|
+
const result = spawnSync(command, args, { encoding: 'utf8', env: env ?? process.env, maxBuffer: 1024 * 1024, timeout: 300_000 });
|
|
25
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? String(result.error ?? '') };
|
|
26
|
+
};
|
|
27
|
+
const registry: Registry = async (url) => {
|
|
28
|
+
const response = await fetch(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(15_000) });
|
|
29
|
+
return { status: response.status, body: response.ok ? await response.json() : null };
|
|
30
|
+
};
|
|
31
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
const PACKAGE_URL = 'https://registry.npmjs.org/elanous';
|
|
33
|
+
|
|
34
|
+
function redact(text: string, token: string): string {
|
|
35
|
+
return token ? text.replaceAll(token, '[REDACTED]') : text;
|
|
36
|
+
}
|
|
37
|
+
function reason(error: unknown, token: string): string {
|
|
38
|
+
return redact(error instanceof Error ? error.message : String(error), token).replace(/[\r\n]+/g, ' ').slice(0, 500);
|
|
39
|
+
}
|
|
40
|
+
function metadata(value: unknown): { name?: string; version?: string } {
|
|
41
|
+
if (!value || typeof value !== 'object') return {};
|
|
42
|
+
return value as { name?: string; version?: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runNpmPublish(context: GraphContext = readGraphContext(), deps: NpmPublishDeps = {}): Promise<Result> {
|
|
46
|
+
const version = context.input.version;
|
|
47
|
+
let token = '';
|
|
48
|
+
try {
|
|
49
|
+
if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
|
|
50
|
+
const stateDir = deps.stateDir ?? effectiveInstanceRoot();
|
|
51
|
+
const candidate = join(stateDir, 'release', version, 'prepared', 'dist', 'elanous.tgz');
|
|
52
|
+
const run = deps.exec ?? exec;
|
|
53
|
+
const readRegistry = deps.registry ?? registry;
|
|
54
|
+
const wait = deps.sleep ?? sleep;
|
|
55
|
+
const waitMinutes = context.input.npmWaitMinutes ?? 40;
|
|
56
|
+
if (typeof waitMinutes !== 'number' || !Number.isFinite(waitMinutes) || waitMinutes < 0 || waitMinutes > 40) throw new Error('input.npmWaitMinutes must be between 0 and 40');
|
|
57
|
+
const archive = run('tar', ['-xOzf', candidate, 'package/package.json']);
|
|
58
|
+
if (archive.status !== 0) throw new Error(`npm archive invalid: ${archive.stderr}`);
|
|
59
|
+
const pkg = metadata(JSON.parse(archive.stdout));
|
|
60
|
+
if (pkg.name !== 'elanous' || pkg.version !== version) throw new Error('npm archive package name/version mismatch');
|
|
61
|
+
const inspect = async (): Promise<{ visible: boolean; tagged: boolean }> => {
|
|
62
|
+
const [manifest, tags] = await Promise.all([readRegistry(`${PACKAGE_URL}/${version}`), readRegistry(PACKAGE_URL)]);
|
|
63
|
+
if (manifest.status !== 200 && manifest.status !== 404) throw new Error(`npm registry version lookup failed (${manifest.status})`);
|
|
64
|
+
if (tags.status !== 200) throw new Error(`npm registry tags lookup failed (${tags.status})`);
|
|
65
|
+
const latest = tags.body && typeof tags.body === 'object' && 'dist-tags' in tags.body
|
|
66
|
+
? (tags.body as { 'dist-tags'?: { latest?: unknown } })['dist-tags']?.latest : undefined;
|
|
67
|
+
if (manifest.status === 200 && (metadata(manifest.body).name !== 'elanous' || metadata(manifest.body).version !== version))
|
|
68
|
+
throw new Error('npm registry version metadata mismatch');
|
|
69
|
+
return { visible: manifest.status === 200, tagged: manifest.status === 200 && latest === version };
|
|
70
|
+
};
|
|
71
|
+
const before = await inspect();
|
|
72
|
+
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with latest tag` };
|
|
73
|
+
if (!before.visible) {
|
|
74
|
+
const tokenFile = context.input.npmTokenFile ?? join(stateDir, 'secrets', 'npm-token');
|
|
75
|
+
if (typeof tokenFile !== 'string' || !tokenFile.trim()) throw new Error('input.npmTokenFile must be a path');
|
|
76
|
+
token = readFileSync(tokenFile, 'utf8').trim();
|
|
77
|
+
if (!token) throw new Error('npm token file empty');
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), 'release-npmrc-'));
|
|
79
|
+
let published;
|
|
80
|
+
try {
|
|
81
|
+
const npmrc = join(dir, 'npmrc');
|
|
82
|
+
writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}\n`, { mode: 0o600, flag: 'wx' });
|
|
83
|
+
published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag', 'latest', '--access', 'public'], { ...process.env, NPM_CONFIG_USERCONFIG: npmrc });
|
|
84
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
85
|
+
const output = `${published.stdout}\n${published.stderr}`;
|
|
86
|
+
const staged = published.status !== 0 && /\bE409\b/i.test(output) && /previously staged/i.test(output);
|
|
87
|
+
if (published.status !== 0 && !staged) throw new Error(`npm publish failed (rc=${published.status}): ${redact(output, token)}`);
|
|
88
|
+
}
|
|
89
|
+
const polls = Math.ceil(waitMinutes * 60 / 30);
|
|
90
|
+
let last = before;
|
|
91
|
+
for (let i = 0; i <= polls; i++) {
|
|
92
|
+
last = await inspect();
|
|
93
|
+
if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with latest tag` };
|
|
94
|
+
if (i < polls) await wait(Math.min(30_000, waitMinutes * 60_000 - i * 30_000));
|
|
95
|
+
}
|
|
96
|
+
if (last.visible) throw new Error(`npm ${version} visible but dist-tags.latest did not reach ${version} within ${waitMinutes} minutes`);
|
|
97
|
+
try { (deps.log ?? ((v, minutes) => debug.log('release-loop.npm', 'staged-not-visible', { version: v, waitedMinutes: minutes })))(version, waitMinutes); } catch { /* observation must not mask npm's staged state */ }
|
|
98
|
+
try { (deps.alert ?? sendOutbound)(`npm ${version} 가 스테이징에 멈춤 — 계정 소유자 확인 필요`, 'alert'); } catch { /* alert failure must not block docs */ }
|
|
99
|
+
return { outcome: 'ok', verdict: 'pass', npm: 'staged-not-visible', humanAction: 'npm-support', summary: 'npm 스테이징 — 레지스트리에 아직 없음' };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { outcome: 'fail', verdict: 'fail', summary: `npm ${version} failed: ${reason(error, token)}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (import.meta.main) {
|
|
106
|
+
let version = '';
|
|
107
|
+
try {
|
|
108
|
+
const context = readGraphContext();
|
|
109
|
+
version = context.input.version;
|
|
110
|
+
const result = await runNpmPublish(context);
|
|
111
|
+
debug.log('release-loop.npm', 'result', { version, outcome: result.outcome, npm: result.npm });
|
|
112
|
+
emitNodeResult(result);
|
|
113
|
+
process.exitCode = result.outcome === 'ok' ? 0 : 1;
|
|
114
|
+
} catch {
|
|
115
|
+
emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: 'npm publish context unavailable' });
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
}
|
|
118
|
+
}
|