elanous 0.2.6 → 0.2.8
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 +4 -0
- 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/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- 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/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
- 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/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.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/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
- 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/7032-f4ef698518fba61c.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/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.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-8f5ff96c522b5f55.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-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.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-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.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-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.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-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_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/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 +10 -0
- package/graphs/release/release-loop.yaml +10 -3
- package/graphs/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +6 -5
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/card-followup/README.md +13 -4
- package/plugins/card-followup/graphs/card-followup.yaml +3 -1
- package/plugins/card-followup/graphs/recipes.yaml +5 -1
- package/plugins/card-followup/graphs/run-step.ts +122 -18
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +1 -1
- 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 +3 -1
- package/plugins/geo-check/graphs/run-step.ts +58 -13
- package/plugins/geo-check/plugin.json +1 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +1 -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/field-feed/deliver-node.ts +57 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +164 -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 +109 -21
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/persona-presets/lint.ts +79 -0
- 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 +2 -2
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +108 -22
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +124 -0
- package/scripts/release-loop/ops-upgrade-node.ts +95 -0
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- 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 +12 -9
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +138 -31
- package/skills/explainer-video/engine/reel.sh +2 -1
- 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/auth-methods.ts +30 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +96 -20
- 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/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +23 -3
- 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/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -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/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +41 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/import-cli.ts +60 -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/mcp-login.ts +13 -4
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/plugin-cli.ts +25 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +50 -8
- 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/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +37 -16
- package/src/dashboard/index.ts +18 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +73 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +94 -12
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +146 -0
- package/src/exec-requests/runner.ts +378 -0
- package/src/exec-requests/store.ts +78 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +203 -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/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +168 -21
- package/src/input-core/text-entry.ts +13 -5
- 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-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +221 -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 +128 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +40 -3
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +71 -4
- package/src/nexus/api/http-server.ts +82 -8
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +28 -1
- package/src/nexus/setup-status.ts +11 -7
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/browser-availability.ts +14 -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-quota-policy.ts +16 -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 +34 -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 +90 -0
- package/src/onboarding.ts +287 -26
- package/src/persona/presets.ts +105 -0
- 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 +19 -7
- 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/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/release-loop/checklist.ts +37 -13
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/orchestrate.ts +18 -11
- 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/session/chat.ts +17 -5
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/skills/trigger-extract.ts +1 -1
- 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-command-job.ts +16 -8
- 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-commands.ts +13 -0
- package/src/telegram.ts +72 -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 +189 -15
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- 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/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -8,7 +8,9 @@ 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
10
|
import { diffFailures, junitFailures, parseFailures } from './gate-diff';
|
|
11
|
-
import {
|
|
11
|
+
import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
|
|
12
|
+
import { baselineFromCutLogs } from './baseline-from-logs';
|
|
13
|
+
import { planShards, readFileDurations, readFileMemory } from './shard-plan';
|
|
12
14
|
import { deriveCdpTestPatterns } from '../test-deterministic';
|
|
13
15
|
import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
14
16
|
import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
@@ -16,7 +18,7 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
|
|
|
16
18
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
17
19
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
18
20
|
|
|
19
|
-
interface CommandResult { rc: number; output: string }
|
|
21
|
+
interface CommandResult { rc: number; output: string; passedIds?: string[] }
|
|
20
22
|
export interface GateRunner {
|
|
21
23
|
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
22
24
|
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
@@ -38,7 +40,7 @@ export interface GateOptions {
|
|
|
38
40
|
instanceRoot?: string;
|
|
39
41
|
ledgerRoot?: string;
|
|
40
42
|
repo?: string;
|
|
41
|
-
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string };
|
|
43
|
+
pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string };
|
|
42
44
|
}
|
|
43
45
|
export interface GateResult {
|
|
44
46
|
outcome: 'ok' | 'regression' | 'error';
|
|
@@ -46,7 +48,9 @@ export interface GateResult {
|
|
|
46
48
|
introduced: string[];
|
|
47
49
|
preexisting: number;
|
|
48
50
|
fixed: number;
|
|
49
|
-
|
|
51
|
+
knownEnv: number;
|
|
52
|
+
knownEnvCleared: string[];
|
|
53
|
+
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
50
54
|
durationMs: number;
|
|
51
55
|
error?: string;
|
|
52
56
|
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
@@ -64,6 +68,38 @@ function check(result: CommandResult, label: string): void {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
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
|
+
}
|
|
67
103
|
// A test may spawn a nested `bun test` whose own summary lines land earlier in the same output
|
|
68
104
|
// (09-29 0.2.4 gate: «0 fail · Ran 2 tests across 1 file» at line 6765 of a 60064-test sweep) —
|
|
69
105
|
// the sweep's own summary is always the «last» one, so read the last match, never the first.
|
|
@@ -76,6 +112,19 @@ const summaryCount = (output: string, label: string) => Number(lastMatch(output,
|
|
|
76
112
|
/** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
|
|
77
113
|
const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
|
|
78
114
|
export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
|
|
115
|
+
/** GT1b — a file whose measured peak is at least this runs alone at the isolation limit from the start: one such file
|
|
116
|
+
* sank a whole 203-file root shard at 16Gi (0.2.6 shard 12 · `unwired-exports` 9.9 GB) and cost two halving rounds. */
|
|
117
|
+
export const POD_HEAVY_FILE_MB = 4096;
|
|
118
|
+
/** Per-file peak memory, read from the cut tree (absent → no heavy files; the halving path still catches OOM). */
|
|
119
|
+
export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-2026-10-01.tsv';
|
|
120
|
+
/** Integration tests that the Pod sweep does not run — each is judged by a real run elsewhere at the cut.
|
|
121
|
+
* 0.2.7 run 5 (10-01): `scripts/install.test.ts` (real `bun pm pack` → `bun add` installs, 301 s alone on node-b)
|
|
122
|
+
* sat in a shard of its own past the 1200 s deadline with no output and stopped the whole gate. Judged on the
|
|
123
|
+
* mbp at the cut instead (TD1 moves it to the integration line). Keep this list short and explained. */
|
|
124
|
+
/** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
|
|
125
|
+
export const POD_SHARD_FILE_CAP = 27;
|
|
126
|
+
|
|
127
|
+
export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
|
|
79
128
|
const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
|
|
80
129
|
const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
|
|
81
130
|
let last: string | undefined;
|
|
@@ -92,10 +141,15 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
|
92
141
|
const reportedErrors = summaryCount(output, 'error');
|
|
93
142
|
const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
|
|
94
143
|
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)}`);
|
|
95
|
-
if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (run.rc !== 0 && run.rc !== 1)) throw incomplete('summary/exit');
|
|
96
144
|
const count = summaryCount(output, 'fail');
|
|
97
145
|
const failures = parseFailures(output);
|
|
98
146
|
const occurrences = [...output.matchAll(/\(fail\)\s+.+?(?:\s+\[[\d.]+(?:ms|s)\])?\s*$/gm)].length;
|
|
147
|
+
// A test that leaves process.exitCode set leaks a non-zero exit into a run whose own summary is complete and clean
|
|
148
|
+
// (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.
|
|
149
|
+
const leakedExit = !!ran && run.rc !== 0 && run.rc !== 1 && count === 0 && errorCount === 0 && occurrences === 0
|
|
150
|
+
&& !/^# Unhandled error between tests/m.test(output);
|
|
151
|
+
const rc = leakedExit ? 0 : run.rc;
|
|
152
|
+
if (!ran || (Number(ran[1]) === 0 && errorCount === 0) || (rc !== 0 && rc !== 1)) throw incomplete('summary/exit');
|
|
99
153
|
// Bun reports file-level unhandled errors separately from test failures.
|
|
100
154
|
const errors: string[] = [];
|
|
101
155
|
let file: string | undefined;
|
|
@@ -110,7 +164,7 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
|
|
|
110
164
|
}
|
|
111
165
|
if (errors.length !== errorCount) throw incomplete(`error attribution identified=${errors.length}`);
|
|
112
166
|
if (!Number.isFinite(count) || count !== occurrences || (count > 0 && failures.length === 0)
|
|
113
|
-
|| (
|
|
167
|
+
|| (rc === 1 && count === 0 && errorCount === 0) || (rc === 0 && count + errorCount > 0)) {
|
|
114
168
|
throw incomplete(`failure attribution incomplete: summary=${count}, identified=${failures.length}`);
|
|
115
169
|
}
|
|
116
170
|
return { failures, errors: [...new Set(errors)].sort() };
|
|
@@ -156,10 +210,20 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
156
210
|
const cdpPatterns = cdp.output.split(/\r?\n/).filter((path) => path.endsWith('.test.ts'));
|
|
157
211
|
// CDP 시험은 `test:deterministic` 이 스스로 뺀다 — 조각에 그것만 남으면 bun «시험 없음» exit 1 을 «불완전»으로 읽었다(09-30 G1d 실측 둘).
|
|
158
212
|
// bun 의 탐색은 숨은 디렉터리(`.x/`)를 안 연다 — 사람의 `bun test` 가 한 번도 안 돌리는 빈 픽스처가 조각에 혼자 남아 «Ran 0 tests» 를 «불완전»으로 읽었다(09-30 G1e 5번 조각).
|
|
159
|
-
const assignable = files.filter((file) => !cdpPatterns.includes(file) && !
|
|
213
|
+
const assignable = files.filter((file) => !cdpPatterns.includes(file) && !POD_SWEEP_INTEGRATION_ONLY.includes(file)
|
|
214
|
+
&& !file.split('/').some((part) => part.startsWith('.')));
|
|
215
|
+
const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
|
|
216
|
+
if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
|
|
160
217
|
if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
|
|
161
218
|
const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
|
|
162
|
-
const
|
|
219
|
+
const memory = readFileMemory(join(tree, POD_MEMORY_SOURCE));
|
|
220
|
+
const heavy = new Set(assignable.filter((file) => (memory.get(file) ?? 0) >= POD_HEAVY_FILE_MB));
|
|
221
|
+
const light = assignable.filter((file) => !heavy.has(file));
|
|
222
|
+
const shards = [
|
|
223
|
+
...(light.length ? planShards(light, durations, shardCount) : []),
|
|
224
|
+
...[...heavy].map((file) => ({ files: [file], plannedSeconds: durations.get(file) ?? 0 })),
|
|
225
|
+
];
|
|
226
|
+
if (heavy.size) debug.log('release-loop.gate', 'pod-heavy-alone', { files: [...heavy], thresholdMb: POD_HEAVY_FILE_MB, memoryLimit: POD_ISOLATION_MEMORY_LIMIT });
|
|
163
227
|
const known = assignable.filter((file) => durations.has(file)).length;
|
|
164
228
|
debug.log('release-loop.gate', 'pod-shard-plan', {
|
|
165
229
|
shards: shards.length, known, unknown: assignable.length - known,
|
|
@@ -169,7 +233,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
169
233
|
const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
|
|
170
234
|
if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
|
|
171
235
|
const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
|
|
172
|
-
const
|
|
236
|
+
const configCache = typeof pod.bunCache === 'string' ? pod.bunCache.trim() : '';
|
|
237
|
+
const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
|
|
238
|
+
const bunCache = configCache || envCache || undefined;
|
|
239
|
+
debug.log('release-loop.gate', 'pod-bun-cache', { source: configCache ? 'config' : envCache ? 'env' : 'none' });
|
|
173
240
|
const cachePrefix = bunCache ? `${podBunCacheVolume(bunCache).shellPrefix} ` : '';
|
|
174
241
|
const jobLogTail = (job: PodCommandResult): string => {
|
|
175
242
|
if (podLogTail) return podLogTail(job);
|
|
@@ -179,7 +246,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
179
246
|
}
|
|
180
247
|
return '';
|
|
181
248
|
};
|
|
182
|
-
type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number };
|
|
249
|
+
type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number; passedIds: string[] };
|
|
183
250
|
type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
|
|
184
251
|
// 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
|
|
185
252
|
const isolationRemaining = shards.map((item) => item.files.length);
|
|
@@ -207,7 +274,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
207
274
|
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
208
275
|
...(bunCache ? { bunCache } : {}),
|
|
209
276
|
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
210
|
-
...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
277
|
+
...(paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
211
278
|
name: `gate-${randomUUID()}`,
|
|
212
279
|
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
280
|
});
|
|
@@ -233,7 +300,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
233
300
|
const destination = join(logDir, `pod-${shard}${branch}.log`);
|
|
234
301
|
if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
|
|
235
302
|
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 });
|
|
303
|
+
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 });
|
|
237
304
|
const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
|
|
238
305
|
if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
|
|
239
306
|
else rmSync(junitDestination, { force: true });
|
|
@@ -298,6 +365,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
298
365
|
...(lastFile ? { lastFile } : {}),
|
|
299
366
|
...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
|
|
300
367
|
}] };
|
|
368
|
+
// GT1 — a big shard that failed at the root goes straight to cap-sized chunks instead of halving twice: memory
|
|
369
|
+
// piles up when one bun process runs many files (0.2.7: only bundles of 54+ OOMed), and each halving round costs
|
|
370
|
+
// a Pod start, an install and a rerun. A failing chunk then isolates its files (depth 2), as before.
|
|
371
|
+
if (depth === 0 && paths.length > POD_SHARD_FILE_CAP) {
|
|
372
|
+
const chunks = Array.from({ length: Math.ceil(paths.length / POD_SHARD_FILE_CAP) }, (_, i) => paths.slice(i * POD_SHARD_FILE_CAP, (i + 1) * POD_SHARD_FILE_CAP));
|
|
373
|
+
debug.log('release-loop.gate', 'pod-shard-cap-split', { shard, files: paths.length, chunks: chunks.length, cap: POD_SHARD_FILE_CAP, reason });
|
|
374
|
+
const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`)));
|
|
375
|
+
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
376
|
+
}
|
|
301
377
|
debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
|
|
302
378
|
const middle = Math.ceil(paths.length / 2);
|
|
303
379
|
const children = await Promise.all([
|
|
@@ -308,7 +384,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
308
384
|
}
|
|
309
385
|
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
310
386
|
errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
|
|
311
|
-
ran: Number(ran![1]), files: Number(ran![2]) }], stalled: [] };
|
|
387
|
+
ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
312
388
|
};
|
|
313
389
|
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
314
390
|
const settled = await Promise.allSettled(shards.map((item, shard) => runShard(item.files, shard)));
|
|
@@ -323,7 +399,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
323
399
|
if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
|
|
324
400
|
if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
|
|
325
401
|
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
|
|
402
|
+
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`,
|
|
403
|
+
passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort() };
|
|
327
404
|
};
|
|
328
405
|
return {
|
|
329
406
|
command,
|
|
@@ -455,7 +532,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
455
532
|
return baselineCommit(root, version);
|
|
456
533
|
};
|
|
457
534
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
458
|
-
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, durationMs: 0 };
|
|
535
|
+
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
|
|
459
536
|
let work: string | undefined;
|
|
460
537
|
let baseSnapshot = false;
|
|
461
538
|
let cutFailures: SweepFailures | undefined;
|
|
@@ -510,8 +587,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
510
587
|
cutFailures = cut;
|
|
511
588
|
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
512
589
|
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
513
|
-
const
|
|
514
|
-
|
|
590
|
+
const recorded = cached ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), baseSha);
|
|
591
|
+
const trusted = cached?.saved ?? (recorded?.complete ? recorded : undefined);
|
|
592
|
+
result.baselineSource = cached ? cached.source : recorded?.complete ? 'cut-logs' : 'swept';
|
|
515
593
|
debug.log('release-loop.gate', 'baseline', { version: opts.baselineVersion, source: result.baselineSource, commit: baseSha });
|
|
516
594
|
let baseTree: string | undefined;
|
|
517
595
|
const getBaseTree = async () => {
|
|
@@ -534,7 +612,14 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
534
612
|
baseline = failuresOf(baseRun, 'baseline sweep');
|
|
535
613
|
}
|
|
536
614
|
for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
|
|
537
|
-
const
|
|
615
|
+
const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
|
|
616
|
+
const cutIds = [...cut.failures, ...cut.errors];
|
|
617
|
+
const baselineIds = [...baseline.failures, ...baseline.errors];
|
|
618
|
+
const countedCut = splitKnownEnv(cutIds, known);
|
|
619
|
+
const countedBaseline = splitKnownEnv(baselineIds, known);
|
|
620
|
+
result.knownEnv = countedCut.knownEnv.length;
|
|
621
|
+
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
|
|
622
|
+
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
538
623
|
result.fixed = diff.fixed.length;
|
|
539
624
|
result.preexisting = diff.common.length;
|
|
540
625
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
@@ -584,7 +669,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
584
669
|
if (opts.remote) runner.remoteMirror = undefined;
|
|
585
670
|
result.durationMs = Date.now() - start;
|
|
586
671
|
}
|
|
587
|
-
if (
|
|
672
|
+
if (cutFailures && !result.stalledShards?.length) {
|
|
588
673
|
try {
|
|
589
674
|
for (const location of roots) {
|
|
590
675
|
const path = failureFile(location, opts.version);
|
|
@@ -608,10 +693,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
608
693
|
export function graphGateResult(result: GateResult, graph: boolean) {
|
|
609
694
|
return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
|
|
610
695
|
verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
|
|
611
|
-
summary: result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}` };
|
|
696
|
+
summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}` };
|
|
612
697
|
}
|
|
613
698
|
|
|
614
|
-
function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
|
|
699
|
+
export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
|
|
615
700
|
const context = env.ELANOUS_GRAPH_CONTEXT && args.every((arg) => arg === '--json') ? readGraphContext(env) : undefined;
|
|
616
701
|
const fromGraph: Record<string, unknown> = context?.input ?? {};
|
|
617
702
|
const graphCommit = context?.outputs['version-release']?.commit;
|
|
@@ -629,6 +714,7 @@ function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'he
|
|
|
629
714
|
pool: fromGraph.gatePodPool,
|
|
630
715
|
...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
|
|
631
716
|
...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
|
|
717
|
+
...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
|
|
632
718
|
} : undefined,
|
|
633
719
|
};
|
|
634
720
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -665,7 +751,7 @@ if (import.meta.main) {
|
|
|
665
751
|
version = opts.version;
|
|
666
752
|
result = await judgeGate(opts);
|
|
667
753
|
} catch (error) {
|
|
668
|
-
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, durationMs: Date.now() - start, error: String(error) };
|
|
754
|
+
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
|
|
669
755
|
}
|
|
670
756
|
if (result.error) console.error(result.error);
|
|
671
757
|
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,124 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, 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, prodInstanceRoot } 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
|
+
/** Test seam — the production root whose secrets/npm-token is the last fallback. */
|
|
20
|
+
productionRoot?: string;
|
|
21
|
+
alert?: (text: string, kind: string) => boolean;
|
|
22
|
+
log?: (version: string, waitedMinutes: number) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const exec: Exec = (command, args, env) => {
|
|
26
|
+
const result = spawnSync(command, args, { encoding: 'utf8', env: env ?? process.env, maxBuffer: 1024 * 1024, timeout: 300_000 });
|
|
27
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? String(result.error ?? '') };
|
|
28
|
+
};
|
|
29
|
+
const registry: Registry = async (url) => {
|
|
30
|
+
const response = await fetch(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(15_000) });
|
|
31
|
+
return { status: response.status, body: response.ok ? await response.json() : null };
|
|
32
|
+
};
|
|
33
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
const PACKAGE_URL = 'https://registry.npmjs.org/elanous';
|
|
35
|
+
|
|
36
|
+
function redact(text: string, token: string): string {
|
|
37
|
+
return token ? text.replaceAll(token, '[REDACTED]') : text;
|
|
38
|
+
}
|
|
39
|
+
function reason(error: unknown, token: string): string {
|
|
40
|
+
return redact(error instanceof Error ? error.message : String(error), token).replace(/[\r\n]+/g, ' ').slice(0, 500);
|
|
41
|
+
}
|
|
42
|
+
function metadata(value: unknown): { name?: string; version?: string } {
|
|
43
|
+
if (!value || typeof value !== 'object') return {};
|
|
44
|
+
return value as { name?: string; version?: string };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runNpmPublish(context: GraphContext = readGraphContext(), deps: NpmPublishDeps = {}): Promise<Result> {
|
|
48
|
+
const version = context.input.version;
|
|
49
|
+
let token = '';
|
|
50
|
+
try {
|
|
51
|
+
if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
|
|
52
|
+
const stateDir = deps.stateDir ?? effectiveInstanceRoot();
|
|
53
|
+
// REL3 — the archive prepare actually built (its output) wins over this process's universe: a resumed run can
|
|
54
|
+
// resolve another universe (10-01 0.2.7: prepared in the pilot test universe, resumed in production).
|
|
55
|
+
const prepared = context.outputs.prepare?.candidate;
|
|
56
|
+
const candidate = typeof prepared === 'string' && prepared.endsWith('.tgz') ? prepared : join(stateDir, 'release', version, 'prepared', 'dist', 'elanous.tgz');
|
|
57
|
+
const run = deps.exec ?? exec;
|
|
58
|
+
const readRegistry = deps.registry ?? registry;
|
|
59
|
+
const wait = deps.sleep ?? sleep;
|
|
60
|
+
const waitMinutes = context.input.npmWaitMinutes ?? 40;
|
|
61
|
+
if (typeof waitMinutes !== 'number' || !Number.isFinite(waitMinutes) || waitMinutes < 0 || waitMinutes > 40) throw new Error('input.npmWaitMinutes must be between 0 and 40');
|
|
62
|
+
const archive = run('tar', ['-xOzf', candidate, 'package/package.json']);
|
|
63
|
+
if (archive.status !== 0) throw new Error(`npm archive invalid: ${archive.stderr}`);
|
|
64
|
+
const pkg = metadata(JSON.parse(archive.stdout));
|
|
65
|
+
if (pkg.name !== 'elanous' || pkg.version !== version) throw new Error('npm archive package name/version mismatch');
|
|
66
|
+
const inspect = async (): Promise<{ visible: boolean; tagged: boolean }> => {
|
|
67
|
+
const [manifest, tags] = await Promise.all([readRegistry(`${PACKAGE_URL}/${version}`), readRegistry(PACKAGE_URL)]);
|
|
68
|
+
if (manifest.status !== 200 && manifest.status !== 404) throw new Error(`npm registry version lookup failed (${manifest.status})`);
|
|
69
|
+
if (tags.status !== 200) throw new Error(`npm registry tags lookup failed (${tags.status})`);
|
|
70
|
+
const latest = tags.body && typeof tags.body === 'object' && 'dist-tags' in tags.body
|
|
71
|
+
? (tags.body as { 'dist-tags'?: { latest?: unknown } })['dist-tags']?.latest : undefined;
|
|
72
|
+
if (manifest.status === 200 && (metadata(manifest.body).name !== 'elanous' || metadata(manifest.body).version !== version))
|
|
73
|
+
throw new Error('npm registry version metadata mismatch');
|
|
74
|
+
return { visible: manifest.status === 200, tagged: manifest.status === 200 && latest === version };
|
|
75
|
+
};
|
|
76
|
+
const before = await inspect();
|
|
77
|
+
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with latest tag` };
|
|
78
|
+
if (!before.visible) {
|
|
79
|
+
// The npm token is a machine secret (like the GitHub App key): input → this universe → the production root.
|
|
80
|
+
const tokenFile = context.input.npmTokenFile ?? [join(stateDir, 'secrets', 'npm-token'), join(deps.productionRoot ?? prodInstanceRoot(), 'secrets', 'npm-token')].find((p) => existsSync(p)) ?? join(stateDir, 'secrets', 'npm-token');
|
|
81
|
+
if (typeof tokenFile !== 'string' || !tokenFile.trim()) throw new Error('input.npmTokenFile must be a path');
|
|
82
|
+
token = readFileSync(tokenFile, 'utf8').trim();
|
|
83
|
+
if (!token) throw new Error('npm token file empty');
|
|
84
|
+
const dir = mkdtempSync(join(tmpdir(), 'release-npmrc-'));
|
|
85
|
+
let published;
|
|
86
|
+
try {
|
|
87
|
+
const npmrc = join(dir, 'npmrc');
|
|
88
|
+
writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}\n`, { mode: 0o600, flag: 'wx' });
|
|
89
|
+
published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag', 'latest', '--access', 'public'], { ...process.env, NPM_CONFIG_USERCONFIG: npmrc });
|
|
90
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
91
|
+
const output = `${published.stdout}\n${published.stderr}`;
|
|
92
|
+
const staged = published.status !== 0 && /\bE409\b/i.test(output) && /previously staged/i.test(output);
|
|
93
|
+
if (published.status !== 0 && !staged) throw new Error(`npm publish failed (rc=${published.status}): ${redact(output, token)}`);
|
|
94
|
+
}
|
|
95
|
+
const polls = Math.ceil(waitMinutes * 60 / 30);
|
|
96
|
+
let last = before;
|
|
97
|
+
for (let i = 0; i <= polls; i++) {
|
|
98
|
+
last = await inspect();
|
|
99
|
+
if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with latest tag` };
|
|
100
|
+
if (i < polls) await wait(Math.min(30_000, waitMinutes * 60_000 - i * 30_000));
|
|
101
|
+
}
|
|
102
|
+
if (last.visible) throw new Error(`npm ${version} visible but dist-tags.latest did not reach ${version} within ${waitMinutes} minutes`);
|
|
103
|
+
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 */ }
|
|
104
|
+
try { (deps.alert ?? sendOutbound)(`npm ${version} 가 스테이징에 멈춤 — 계정 소유자 확인 필요`, 'alert'); } catch { /* alert failure must not block docs */ }
|
|
105
|
+
return { outcome: 'ok', verdict: 'pass', npm: 'staged-not-visible', humanAction: 'npm-support', summary: 'npm 스테이징 — 레지스트리에 아직 없음' };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return { outcome: 'fail', verdict: 'fail', summary: `npm ${version} failed: ${reason(error, token)}` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (import.meta.main) {
|
|
112
|
+
let version = '';
|
|
113
|
+
try {
|
|
114
|
+
const context = readGraphContext();
|
|
115
|
+
version = context.input.version;
|
|
116
|
+
const result = await runNpmPublish(context);
|
|
117
|
+
debug.log('release-loop.npm', 'result', { version, outcome: result.outcome, npm: result.npm });
|
|
118
|
+
emitNodeResult(result);
|
|
119
|
+
process.exitCode = result.outcome === 'ok' ? 0 : 1;
|
|
120
|
+
} catch {
|
|
121
|
+
emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: 'npm publish context unavailable' });
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|