elanous 0.2.17 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +25 -24
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +24 -23
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +28 -27
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +6 -3
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/lib/pch-regress-map.ts +23 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +436 -59
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +120 -0
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +174 -0
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +45 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +255 -0
- package/src/briefing/brief-items.ts +86 -17
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +85 -7
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +128 -19
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +403 -4
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +244 -40
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/graph-runner/runner.ts +25 -5
- package/src/harness/harness-cli-command.ts +12 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/harness-salvage-cli.ts +78 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/hq.ts +6 -1
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +185 -18
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +106 -4
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +450 -54
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/http-server.ts +16 -5
- package/src/nexus/api/loop-edges.ts +352 -23
- package/src/nexus/api/ops-api.ts +96 -28
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/nexus/index.ts +7 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +106 -6
- package/src/release-loop/feature-store.ts +64 -16
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +64 -20
- package/src/release-loop/rubric.ts +136 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +179 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +61 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +101 -7
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +203 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +49 -5
- package/src/task-agent/live-moves.ts +176 -0
- package/src/task-agent/mission.ts +449 -0
- package/src/task-agent/shadow.ts +258 -0
- package/src/task-agent/task-hand.ts +337 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
- package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +88 -20
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +11 -0
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/telegram.ts +5 -20
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +196 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// GATE-PARTIAL (0.2.19 · 10-07): a re-gate after `release cut-branch --append` runs only the test files that can have
|
|
2
|
+
// changed — not the whole 24-shard sweep again (0.2.18: two timeout fixes on top of an 84-minute gate meant another full
|
|
3
|
+
// gate). The plan is opt-in (`release resume --from gate --partial`) and fail-closed: any non-test source change, a prior
|
|
4
|
+
// gate record for another commit, or a missing record means «full gate», never a guess.
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export interface PartialPlan {
|
|
10
|
+
version: string;
|
|
11
|
+
/** Commit the previous (full) gate measured — its recorded failures are carried for every file not re-run. */
|
|
12
|
+
priorCommit: string;
|
|
13
|
+
/** The commit this plan is for — a gate on any other commit ignores it. */
|
|
14
|
+
forCommit: string;
|
|
15
|
+
/** Test files re-run: changed test files ⊕ files of the prior failures. */
|
|
16
|
+
files: string[];
|
|
17
|
+
priorFailures: string[];
|
|
18
|
+
priorErrors: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const partialPlanPath = (root: string, version: string) => join(root, 'release', version, 'gate-partial.json');
|
|
22
|
+
/** The same test-file set the gate sweep runs (`git ls-files` filtered by this suffix). */
|
|
23
|
+
const TEST_FILE = /^(?:[\w.-]+\/)*[\w.-]+\.test\.(?:tsx?|jsx?|mts|cts)$/;
|
|
24
|
+
/** Changes that cannot alter a test result on their own: Markdown that is not test input. A fixture (any file under a
|
|
25
|
+
* test/, fixtures, __fixtures__ or __snapshots__ path), JSON or script is not inert — the gate runs in full. */
|
|
26
|
+
const INERT_MD = /\.md$/;
|
|
27
|
+
const TEST_INPUT = /(?:^|\/)(?:test|tests|fixtures|__fixtures__|__snapshots__|__mocks__)\//;
|
|
28
|
+
const isInert = (file: string) => INERT_MD.test(file) && !TEST_INPUT.test(file);
|
|
29
|
+
|
|
30
|
+
/** The test file of a failure id (`<file> > <case>`). One rule for the gate and the partial plan: ids come only from the
|
|
31
|
+
* gate's output parsers, which accept `dir/….test.ts(x)` — anything else (a root file, `..`, no case) is unsafe. */
|
|
32
|
+
export function testFileOfId(id: string): string {
|
|
33
|
+
const file = id.split(' > ', 1)[0]!;
|
|
34
|
+
if (!id.includes(' > ') || !/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Decide whether a partial re-gate is safe for `changedFiles` (prior → for). Never guesses: any doubt is a full gate. */
|
|
39
|
+
export function planPartialRegate(input: { version: string; priorCommit: string; forCommit: string; changedFiles: readonly string[];
|
|
40
|
+
priorFailures: readonly string[]; priorErrors: readonly string[] }):
|
|
41
|
+
{ ok: true; plan: PartialPlan } | { ok: false; reason: string } {
|
|
42
|
+
if (!/^[0-9a-f]{40}$/i.test(input.priorCommit) || !/^[0-9a-f]{40}$/i.test(input.forCommit)) return { ok: false, reason: 'commit is not a full sha' };
|
|
43
|
+
if (input.priorCommit === input.forCommit) return { ok: false, reason: 'no new commit since the prior gate' };
|
|
44
|
+
const source = input.changedFiles.filter((file) => !TEST_FILE.test(file) && !isInert(file));
|
|
45
|
+
if (source.length) return { ok: false, reason: `non-test change → full gate: ${source.slice(0, 5).join(', ')}${source.length > 5 ? ` (+${source.length - 5})` : ''}` };
|
|
46
|
+
let failureFiles: string[];
|
|
47
|
+
try { failureFiles = [...input.priorFailures, ...input.priorErrors].map(testFileOfId); }
|
|
48
|
+
catch (error) { return { ok: false, reason: String(error instanceof Error ? error.message : error) }; }
|
|
49
|
+
const files = [...new Set([...input.changedFiles.filter((file) => TEST_FILE.test(file)), ...failureFiles])].sort();
|
|
50
|
+
if (!files.length) return { ok: false, reason: 'nothing to re-run' };
|
|
51
|
+
return { ok: true, plan: { version: input.version, priorCommit: input.priorCommit, forCommit: input.forCommit, files,
|
|
52
|
+
priorFailures: [...input.priorFailures], priorErrors: [...input.priorErrors] } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writePartialPlan(root: string, plan: PartialPlan): string {
|
|
56
|
+
const path = partialPlanPath(root, plan.version);
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
59
|
+
writeFileSync(temp, `${JSON.stringify(plan, null, 2)}\n`, { mode: 0o600 });
|
|
60
|
+
renameSync(temp, path);
|
|
61
|
+
return path;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clearPartialPlan(root: string, version: string): void {
|
|
65
|
+
const path = partialPlanPath(root, version);
|
|
66
|
+
if (existsSync(path)) rmSync(path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The plan for exactly this commit, or null. A malformed plan is an error (never silently a full or partial gate). */
|
|
70
|
+
export function readPartialPlan(roots: readonly string[], version: string, commit: string): PartialPlan | null {
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
const path = partialPlanPath(root, version);
|
|
73
|
+
if (!existsSync(path)) continue;
|
|
74
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as Partial<PartialPlan>;
|
|
75
|
+
if (raw.version !== version || typeof raw.priorCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(raw.priorCommit)
|
|
76
|
+
|| typeof raw.forCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(raw.forCommit) || raw.priorCommit === raw.forCommit
|
|
77
|
+
|| !Array.isArray(raw.files) || raw.files.length === 0 || !raw.files.every((file) => typeof file === 'string' && TEST_FILE.test(file) && !file.split('/').includes('..'))
|
|
78
|
+
|| !Array.isArray(raw.priorFailures) || !Array.isArray(raw.priorErrors)) throw new Error(`invalid gate partial plan: ${path}`);
|
|
79
|
+
// Every prior failure's file is re-run (planPartialRegate guarantees it) — a plan that would carry a failure it did
|
|
80
|
+
// not re-run is malformed, not «partial».
|
|
81
|
+
const rerun = new Set(raw.files);
|
|
82
|
+
let failureFiles: string[];
|
|
83
|
+
try { failureFiles = [...raw.priorFailures, ...raw.priorErrors].map((id) => testFileOfId(String(id))); }
|
|
84
|
+
catch { throw new Error(`invalid gate partial plan: ${path}`); }
|
|
85
|
+
if (failureFiles.some((file) => !rerun.has(file))) throw new Error(`invalid gate partial plan (prior failure not re-run): ${path}`);
|
|
86
|
+
if (raw.forCommit !== commit) continue;
|
|
87
|
+
return raw as PartialPlan;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Cut failures after a partial re-run: prior results for files not re-run ⊕ this run's results for the re-run files. */
|
|
93
|
+
export function mergePartialFailures(plan: PartialPlan, measured: { failures: string[]; errors: string[] }): { failures: string[]; errors: string[] } {
|
|
94
|
+
const rerun = new Set(plan.files);
|
|
95
|
+
const keep = (ids: readonly string[]) => ids.filter((id) => !rerun.has(testFileOfId(id)));
|
|
96
|
+
return { failures: [...keep(plan.priorFailures), ...measured.failures], errors: [...keep(plan.priorErrors), ...measured.errors] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Files changed between two commits — both sides of a rename (`--no-renames`), so moving a source file into a test or
|
|
100
|
+
* Markdown name still shows the source path that disappeared. */
|
|
101
|
+
export function changedFilesBetween(repo: string, from: string, to: string): string[] {
|
|
102
|
+
// git-spawn-allow: read-only diff of two release commits.
|
|
103
|
+
const result = spawnSync('git', ['diff', '--name-only', '--no-renames', `${from}..${to}`], { cwd: repo, encoding: 'utf8' });
|
|
104
|
+
if (result.status !== 0 || result.error) throw new Error(`git diff failed: ${(result.stderr || result.error || 'no output').toString().trim()}`);
|
|
105
|
+
return result.stdout.split(/\r?\n/).filter(Boolean);
|
|
106
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// F5 GATE-TIMING-REPORT — 게이트 한 판의 «시간 비용»을 보이게 한다(GATE-SPEED 의 자).
|
|
2
|
+
// 입력 = 게이트 로그 디렉토리(`<ledger>/release/<version>/gate-logs/cut`)의 `pod-*.json`.
|
|
3
|
+
// 첫 판 샤드 = `pod-<n>.json` · 재시도 = 그 밖의 모든 `pod-*.json`(`-c<k>`·`-c<k>-file-<m>`·`-retry` …).
|
|
4
|
+
// ⛔ 측정이 없으면 `measured:false` 로 표시한다 — 0 을 «잰 값»처럼 내지 않는다.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
|
|
9
|
+
export interface ShardTiming { name: string; min: number; files: number; plannedMin: number | null }
|
|
10
|
+
|
|
11
|
+
export interface GateTimingReport {
|
|
12
|
+
dir: string;
|
|
13
|
+
/** 읽은 pod-*.json 중 durationMs 를 가진 것이 하나라도 있었나. false 면 아래 수는 «측정 없음». */
|
|
14
|
+
measured: boolean;
|
|
15
|
+
/** JSON 파싱 실패·durationMs 부재로 건너뛴 파일 이름. */
|
|
16
|
+
unreadable: string[];
|
|
17
|
+
firstPass: { shards: number; sumMin: number; medianMin: number; maxMin: number; failedShards: string[] };
|
|
18
|
+
retries: { runs: number; sumMin: number; medianMin: number };
|
|
19
|
+
/** 재시도 합 / (첫 판 합 + 재시도 합). 측정이 없으면 null. */
|
|
20
|
+
retryShare: number | null;
|
|
21
|
+
slowestShards: ShardTiming[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FIRST_PASS = /^pod-\d+\.json$/;
|
|
25
|
+
const POD_JSON = /^pod-.+\.json$/;
|
|
26
|
+
|
|
27
|
+
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
28
|
+
|
|
29
|
+
function median(xs: number[]): number {
|
|
30
|
+
if (xs.length === 0) return 0;
|
|
31
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
32
|
+
const m = s.length >> 1;
|
|
33
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PodRecord { durationMs?: unknown; rc?: unknown; files?: unknown; plannedSeconds?: unknown; jobFailed?: unknown }
|
|
37
|
+
|
|
38
|
+
export function gateTiming(dir: string): GateTimingReport {
|
|
39
|
+
const names = existsSync(dir) && statSync(dir).isDirectory()
|
|
40
|
+
? readdirSync(dir).filter((n) => POD_JSON.test(n)).sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
|
|
41
|
+
: [];
|
|
42
|
+
const first: Array<ShardTiming & { failed: boolean }> = [];
|
|
43
|
+
const retryMin: number[] = [];
|
|
44
|
+
const unreadable: string[] = [];
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
let rec: PodRecord;
|
|
47
|
+
try { rec = JSON.parse(readFileSync(join(dir, name), 'utf8')) as PodRecord; } catch { unreadable.push(name); continue; }
|
|
48
|
+
if (!rec || typeof rec !== 'object' || typeof rec.durationMs !== 'number' || !Number.isFinite(rec.durationMs)) { unreadable.push(name); continue; }
|
|
49
|
+
const min = rec.durationMs / 60_000;
|
|
50
|
+
if (FIRST_PASS.test(name)) {
|
|
51
|
+
first.push({
|
|
52
|
+
name: name.replace(/\.json$/, ''),
|
|
53
|
+
min,
|
|
54
|
+
files: Array.isArray(rec.files) ? rec.files.length : 0,
|
|
55
|
+
plannedMin: typeof rec.plannedSeconds === 'number' ? rec.plannedSeconds / 60 : null,
|
|
56
|
+
failed: rec.jobFailed === true || (typeof rec.rc === 'number' && rec.rc !== 0),
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
retryMin.push(min);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const firstMins = first.map((s) => s.min);
|
|
63
|
+
const firstSum = firstMins.reduce((a, b) => a + b, 0);
|
|
64
|
+
const retrySum = retryMin.reduce((a, b) => a + b, 0);
|
|
65
|
+
const total = firstSum + retrySum;
|
|
66
|
+
const measured = first.length + retryMin.length > 0;
|
|
67
|
+
return {
|
|
68
|
+
dir,
|
|
69
|
+
measured,
|
|
70
|
+
unreadable,
|
|
71
|
+
firstPass: {
|
|
72
|
+
shards: first.length,
|
|
73
|
+
sumMin: round1(firstSum),
|
|
74
|
+
medianMin: round1(median(firstMins)),
|
|
75
|
+
maxMin: round1(firstMins.length ? Math.max(...firstMins) : 0),
|
|
76
|
+
failedShards: first.filter((s) => s.failed).map((s) => s.name),
|
|
77
|
+
},
|
|
78
|
+
retries: { runs: retryMin.length, sumMin: round1(retrySum), medianMin: round1(median(retryMin)) },
|
|
79
|
+
retryShare: measured && total > 0 ? Math.round((retrySum / total) * 1000) / 1000 : null,
|
|
80
|
+
slowestShards: [...first].sort((a, b) => b.min - a.min).slice(0, 5)
|
|
81
|
+
.map(({ name, min, files, plannedMin }) => ({ name, min: round1(min), files, plannedMin: plannedMin === null ? null : round1(plannedMin) })),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `<version>` 이면 원장 루트(`releaseLedgerRoot()`)의 `release/<version>/gate-logs/cut`, 경로처럼 보이면 그대로. */
|
|
86
|
+
export function resolveGateLogDir(arg: string, ledgerRoot: string = releaseLedgerRoot()): string {
|
|
87
|
+
if (arg.includes('/') || arg.startsWith('.') || existsSync(arg)) return arg;
|
|
88
|
+
return join(ledgerRoot, 'release', arg, 'gate-logs', 'cut');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatGateTiming(r: GateTimingReport): string {
|
|
92
|
+
if (!r.measured) {
|
|
93
|
+
return [`게이트 시간 — 측정 없음 (${r.dir})`, ' pod-*.json 에 durationMs 가 하나도 없다 · 0 은 «잰 값»이 아니다',
|
|
94
|
+
...(r.unreadable.length ? [` 읽지 못한 파일 ${r.unreadable.length}개`] : [])].join('\n');
|
|
95
|
+
}
|
|
96
|
+
const f = r.firstPass;
|
|
97
|
+
const lines = [
|
|
98
|
+
`게이트 시간 (${r.dir})`,
|
|
99
|
+
` 첫 판 샤드 ${f.shards}개 · 합 ${f.sumMin}분 · 중앙 ${f.medianMin}분 · 최대 ${f.maxMin}분 · 실패 샤드 ${f.failedShards.length}개${f.failedShards.length ? ` (${f.failedShards.join(', ')})` : ''}`,
|
|
100
|
+
` 재시도 ${r.retries.runs}회 · 합 ${r.retries.sumMin}분 · 중앙 ${r.retries.medianMin}분`,
|
|
101
|
+
` 재시도 비중 ${r.retryShare === null ? '측정 없음' : `${round1(r.retryShare * 100)}%`} (재시도 합 / 전체 합)`,
|
|
102
|
+
' 느린 샤드 상위 5:',
|
|
103
|
+
...r.slowestShards.map((s) => ` ${s.name} ${s.min}분 · 파일 ${s.files}개 · 계획 ${s.plannedMin === null ? '—' : `${s.plannedMin}분`}`),
|
|
104
|
+
];
|
|
105
|
+
if (r.unreadable.length) lines.push(` ⚠ 읽지 못한 파일 ${r.unreadable.length}개: ${r.unreadable.slice(0, 5).join(', ')}${r.unreadable.length > 5 ? ' …' : ''}`);
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (import.meta.main) {
|
|
110
|
+
const args = process.argv.slice(2);
|
|
111
|
+
const json = args.includes('--json');
|
|
112
|
+
const target = args.find((a) => !a.startsWith('--'));
|
|
113
|
+
if (!target) {
|
|
114
|
+
console.error('사용법: bun scripts/release-loop/gate-timing.ts <version|dir> [--json]');
|
|
115
|
+
process.exit(2);
|
|
116
|
+
}
|
|
117
|
+
const report = gateTiming(resolveGateLogDir(target));
|
|
118
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatGateTiming(report));
|
|
119
|
+
if (!report.measured) process.exit(1);
|
|
120
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { isReleaseVersion } from './release-version.js';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { debug } from '../../src/debug/log.js';
|
|
5
6
|
|
|
@@ -17,7 +18,7 @@ export function readGraphContext(env: NodeJS.ProcessEnv = process.env): GraphCon
|
|
|
17
18
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid graph context');
|
|
18
19
|
const context = value as Record<string, unknown>;
|
|
19
20
|
const input = context.input as Record<string, unknown> | undefined;
|
|
20
|
-
if (!input || typeof input.version !== 'string' ||
|
|
21
|
+
if (!input || typeof input.version !== 'string' || !isReleaseVersion(input.version)
|
|
21
22
|
|| typeof input.previousVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(input.previousVersion)) throw new Error('version and previousVersion required');
|
|
22
23
|
return { input: input as GraphContext['input'], outputs: context.outputs && typeof context.outputs === 'object' ? context.outputs as GraphContext['outputs'] : {} };
|
|
23
24
|
}
|
|
@@ -7,6 +7,7 @@ import { debug } from '../../src/debug/log.js';
|
|
|
7
7
|
import { sendOutbound } from '../../src/domains/outbound-alert.js';
|
|
8
8
|
import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
|
|
9
9
|
import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
|
|
10
|
+
import { prereleaseKind } from './release-version.js';
|
|
10
11
|
|
|
11
12
|
type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
|
|
12
13
|
type Exec = (command: string, args: string[], env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
|
|
@@ -47,8 +48,11 @@ function metadata(value: unknown): { name?: string; version?: string } {
|
|
|
47
48
|
export async function runNpmPublish(context: GraphContext = readGraphContext(), deps: NpmPublishDeps = {}): Promise<Result> {
|
|
48
49
|
const version = context.input.version;
|
|
49
50
|
let token = '';
|
|
51
|
+
// RELEASE-REHEARSAL-RC: a prerelease goes to `next` so `npm i elanous` never resolves to a rehearsal build.
|
|
52
|
+
let distTag = 'latest';
|
|
50
53
|
try {
|
|
51
54
|
if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
|
|
55
|
+
distTag = prereleaseKind(version) === null ? 'latest' : 'next';
|
|
52
56
|
const stateDir = deps.stateDir ?? effectiveInstanceRoot();
|
|
53
57
|
// REL3 — the archive prepare actually built (its output) wins over this process's universe: a resumed run can
|
|
54
58
|
// resolve another universe (10-01 0.2.7: prepared in the pilot test universe, resumed in production).
|
|
@@ -67,14 +71,14 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
67
71
|
const [manifest, tags] = await Promise.all([readRegistry(`${PACKAGE_URL}/${version}`), readRegistry(PACKAGE_URL)]);
|
|
68
72
|
if (manifest.status !== 200 && manifest.status !== 404) throw new Error(`npm registry version lookup failed (${manifest.status})`);
|
|
69
73
|
if (tags.status !== 200) throw new Error(`npm registry tags lookup failed (${tags.status})`);
|
|
70
|
-
const
|
|
71
|
-
? (tags.body as { 'dist-tags'?:
|
|
74
|
+
const tagged = tags.body && typeof tags.body === 'object' && 'dist-tags' in tags.body
|
|
75
|
+
? (tags.body as { 'dist-tags'?: Record<string, unknown> })['dist-tags']?.[distTag] : undefined;
|
|
72
76
|
if (manifest.status === 200 && (metadata(manifest.body).name !== 'elanous' || metadata(manifest.body).version !== version))
|
|
73
77
|
throw new Error('npm registry version metadata mismatch');
|
|
74
|
-
return { visible: manifest.status === 200, tagged: manifest.status === 200 &&
|
|
78
|
+
return { visible: manifest.status === 200, tagged: manifest.status === 200 && tagged === version };
|
|
75
79
|
};
|
|
76
80
|
const before = await inspect();
|
|
77
|
-
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with
|
|
81
|
+
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with ${distTag} tag` };
|
|
78
82
|
if (!before.visible) {
|
|
79
83
|
// The npm token is a machine secret (like the GitHub App key): input → this universe → the production root.
|
|
80
84
|
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');
|
|
@@ -86,7 +90,7 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
86
90
|
try {
|
|
87
91
|
const npmrc = join(dir, 'npmrc');
|
|
88
92
|
writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}\n`, { mode: 0o600, flag: 'wx' });
|
|
89
|
-
published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag',
|
|
93
|
+
published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag', distTag, '--access', 'public'], { ...process.env, NPM_CONFIG_USERCONFIG: npmrc });
|
|
90
94
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
91
95
|
const output = `${published.stdout}\n${published.stderr}`;
|
|
92
96
|
const staged = published.status !== 0 && /\bE409\b/i.test(output) && /previously staged/i.test(output);
|
|
@@ -96,10 +100,10 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
96
100
|
let last = before;
|
|
97
101
|
for (let i = 0; i <= polls; i++) {
|
|
98
102
|
last = await inspect();
|
|
99
|
-
if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with
|
|
103
|
+
if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with ${distTag} tag` };
|
|
100
104
|
if (i < polls) await wait(Math.min(30_000, waitMinutes * 60_000 - i * 30_000));
|
|
101
105
|
}
|
|
102
|
-
if (last.visible) throw new Error(`npm ${version} visible but dist-tags
|
|
106
|
+
if (last.visible) throw new Error(`npm ${version} visible but dist-tags.${distTag} did not reach ${version} within ${waitMinutes} minutes`);
|
|
103
107
|
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
108
|
try { (deps.alert ?? sendOutbound)(`npm ${version} 가 스테이징에 멈춤 — 계정 소유자 확인 필요`, 'alert'); } catch { /* alert failure must not block docs */ }
|
|
105
109
|
return { outcome: 'ok', verdict: 'pass', npm: 'staged-not-visible', humanAction: 'npm-support', summary: 'npm 스테이징 — 레지스트리에 아직 없음' };
|
|
@@ -4,6 +4,7 @@ import { homedir, tmpdir } from 'node:os';
|
|
|
4
4
|
import { join, isAbsolute } from 'node:path';
|
|
5
5
|
import { debug } from '../../src/debug/log.js';
|
|
6
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
|
|
7
|
+
import { prereleaseKind } from './release-version.js';
|
|
7
8
|
|
|
8
9
|
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
|
|
9
10
|
/** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
|
|
@@ -199,6 +200,12 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
|
|
|
199
200
|
|
|
200
201
|
if (import.meta.main) {
|
|
201
202
|
let version = '';
|
|
202
|
-
try {
|
|
203
|
+
try {
|
|
204
|
+
version = readGraphContext().input.version;
|
|
205
|
+
// RELEASE-REHEARSAL-RC: a prerelease never lands docs on main or upgrades the operating hosts.
|
|
206
|
+
process.exitCode = finishNode('ops-upgrade', version, prereleaseKind(version) !== null
|
|
207
|
+
? { outcome: 'ok', verdict: 'pass', summary: `ops-upgrade skipped — ${version} is a prerelease`, skipped: 'prerelease' }
|
|
208
|
+
: runOpsUpgrade());
|
|
209
|
+
}
|
|
203
210
|
catch (error) { process.exitCode = finishNode('ops-upgrade', version, errorResult(error)); }
|
|
204
211
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// `elanous release preflight` — 컷 30분 전 사전 점검(RELEASE-PREFLIGHT · 10-06).
|
|
2
|
+
//
|
|
3
|
+
// 🩸 계기(10-06 실측): 0.2.17 릴리스 런이 첫 노드(checklist-gate)에서 멎었다 — «발행 뒤» 칸(POSTPUB-VERIFY)이 그 판에
|
|
4
|
+
// 남아 있었고 게이트가 그것을 미완으로 셌다. 컷이 올 때까지 아무도 몰랐다. 새 규칙 = 착지 마감 = 컷 − 30분, 그 30분에 이 점검.
|
|
5
|
+
// ⛔ 읽기 전용 — 칸을 옮기지도, 동결을 바꾸지도, push 하지도 않는다. 옮길 것은 «명령»으로 제안만 한다.
|
|
6
|
+
// ⛔ 게이트 규칙을 다시 쓰지 않는다 — checklist-gate 노드 · release run 진입 검사 · 예약 시작 준비 검사(releaseReadiness, #24557)가
|
|
7
|
+
// 함께 쓰는 단 하나의 판정 cutChecklistGate 를 그대로 부르고, 예약 시작 경로는 releaseReadiness 를 CLI 와 같은 인자로 부른다.
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { cutChecklistGate, listChecklist, type ChecklistItem } from '../../src/release-loop/checklist.js';
|
|
11
|
+
import { getSchedule, formatKst, type ReleaseSchedule } from '../../src/release-loop/release-schedule.js';
|
|
12
|
+
import { readLandingFreeze, landingFreezeMessage, type LandingFreeze } from '../../src/release-loop/landing-freeze.js';
|
|
13
|
+
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
14
|
+
import { debug } from '../../src/debug/log.js';
|
|
15
|
+
import { releaseReadiness } from './release-readiness.js';
|
|
16
|
+
import { latestPublishedPreviousVersion, runUnattendedRelease, type UnattendedReleaseDeps } from './unattended-release.js';
|
|
17
|
+
|
|
18
|
+
export type PreflightLevel = 'ok' | 'warn' | 'block';
|
|
19
|
+
export type PreflightCheck = 'checklist' | 'postpub' | 'run-input' | 'freeze' | 'schedule';
|
|
20
|
+
export interface PreflightFinding { level: PreflightLevel; check: PreflightCheck; id?: string; message: string; suggestion?: string }
|
|
21
|
+
export interface GateMeasure { version: string; runId: string; gateSeconds: number; toPublishSeconds?: number }
|
|
22
|
+
const compareStable = (a: string, b: string) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); return x[0]! - y[0]! || x[1]! - y[1]! || x[2]! - y[2]!; };
|
|
23
|
+
export interface PreflightResult {
|
|
24
|
+
version: string;
|
|
25
|
+
next: string;
|
|
26
|
+
verdict: PreflightLevel;
|
|
27
|
+
blockers: number;
|
|
28
|
+
warnings: number;
|
|
29
|
+
findings: PreflightFinding[];
|
|
30
|
+
schedule: { cutAt: string; landBy: string | null; publishAt: string | null; publishAtSource: 'option' | 'cadence' | null } | null;
|
|
31
|
+
/** The immediately previous release (by version order) whose gate is compared. */
|
|
32
|
+
previousVersion: string | null;
|
|
33
|
+
gateMeasure: GateMeasure | null;
|
|
34
|
+
/** When the previous release has no record: the newest measured older release, labelled with its version. */
|
|
35
|
+
fallbackGateMeasure: GateMeasure | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreflightDeps {
|
|
39
|
+
now?: Date;
|
|
40
|
+
/** Planned publish time (ISO). Without it, the October cadence (07:00 · 18:00 KST) after the cut is assumed. */
|
|
41
|
+
publishAt?: string;
|
|
42
|
+
runInput?: (version: string) => Promise<{ input: unknown }>;
|
|
43
|
+
freeze?: () => LandingFreeze | null;
|
|
44
|
+
schedule?: (version: string) => ReleaseSchedule | null;
|
|
45
|
+
graphRunsRoot?: string;
|
|
46
|
+
releaseRunDeps?: UnattendedReleaseDeps;
|
|
47
|
+
/** The immediately previous release; default = the run input's previousVersion (release ledger). */
|
|
48
|
+
previousVersion?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const POSTPUB_ID = /^POSTPUB-/i;
|
|
52
|
+
// Only a title that «starts» with the marker: a pre-publish cell often mentions «발행 뒤» in its body (a slot, a plan).
|
|
53
|
+
const POSTPUB_TITLE = /^[«"'(]?\s*(발행\s*(뒤|후)|post-?publish)/i;
|
|
54
|
+
const STABLE = /^\d+\.\d+\.\d+$/;
|
|
55
|
+
|
|
56
|
+
export function isPostPublishCell(item: Pick<ChecklistItem, 'id' | 'title'> & { kind?: string }): boolean {
|
|
57
|
+
return POSTPUB_ID.test(item.id) || POSTPUB_TITLE.test(item.title) || (item as { kind?: string }).kind === 'postpub';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function nextPatch(version: string): string {
|
|
61
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
62
|
+
return `${major}.${minor}.${patch! + 1}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const kstHour = (ms: number) => Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', hourCycle: 'h23' }).format(new Date(ms)));
|
|
66
|
+
|
|
67
|
+
/** October cadence: the first 07:00 or 18:00 KST strictly after the cut (a cut at 07:00 publishes at 18:00). */
|
|
68
|
+
export function cadencePublishAt(cutAt: string): string {
|
|
69
|
+
const cut = Date.parse(cutAt);
|
|
70
|
+
const startOfHour = Math.floor(cut / 3_600_000) * 3_600_000 + 3_600_000;
|
|
71
|
+
for (let t = startOfHour; t < cut + 48 * 3_600_000; t += 3_600_000) {
|
|
72
|
+
const h = kstHour(t);
|
|
73
|
+
if (h === 7 || h === 18) return new Date(t).toISOString();
|
|
74
|
+
}
|
|
75
|
+
return new Date(cut).toISOString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A release's measured gate: the newest finished release-loop run of «that» version (`{ version }`) or, with
|
|
80
|
+
* `{ below }`, the most recently measured run of any version under it. Node timings are not stored in the run state, so the gate time is the start of the gate node's context file to
|
|
81
|
+
* the start of the next node's (contexts/<n>.json is written when node n starts). Null when nothing was measured.
|
|
82
|
+
*/
|
|
83
|
+
export function releaseGateMeasure(which: { version: string } | { below: string }, root = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop')): GateMeasure | null {
|
|
84
|
+
if (!existsSync(root)) return null;
|
|
85
|
+
const runs: Array<{ runId: string; version: string; startedAt: string; path: string[]; file: string }> = [];
|
|
86
|
+
for (const file of readdirSync(root)) {
|
|
87
|
+
if (!file.endsWith('.json') || file.includes('.decision')) continue;
|
|
88
|
+
try {
|
|
89
|
+
const state = JSON.parse(readFileSync(join(root, file), 'utf8')) as { runId?: string; status?: string; startedAt?: string; path?: string[]; input?: { version?: string } };
|
|
90
|
+
const v = state.input?.version;
|
|
91
|
+
if (state.status !== 'done' || typeof v !== 'string' || !STABLE.test(v) || !Array.isArray(state.path) || typeof state.startedAt !== 'string') continue;
|
|
92
|
+
if ('version' in which ? v !== which.version : compareStable(v, which.below) >= 0) continue;
|
|
93
|
+
runs.push({ runId: state.runId ?? file.slice(0, -5), version: v, startedAt: state.startedAt, path: state.path, file });
|
|
94
|
+
} catch { /* an unreadable run is skipped — the measure is best-effort */ }
|
|
95
|
+
}
|
|
96
|
+
// `{ version }`: the newest run of that release. `{ below }` (only a labelled reference when the previous release has
|
|
97
|
+
// no record): the most recently measured run, not the highest version.
|
|
98
|
+
runs.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
|
|
99
|
+
for (const run of runs) {
|
|
100
|
+
const contexts = join(root, `${run.file}.contexts`);
|
|
101
|
+
const index = run.path.indexOf('gate');
|
|
102
|
+
if (index < 0 || !existsSync(contexts)) continue;
|
|
103
|
+
const at = (i: number): number | null => {
|
|
104
|
+
const path = join(contexts, `${i + 1}.json`);
|
|
105
|
+
try {
|
|
106
|
+
const ctx = JSON.parse(readFileSync(path, 'utf8')) as { nodeId?: string };
|
|
107
|
+
return ctx.nodeId === run.path[i] ? statSync(path).mtimeMs : null;
|
|
108
|
+
} catch { return null; }
|
|
109
|
+
};
|
|
110
|
+
const gateStart = at(index), gateEnd = at(index + 1);
|
|
111
|
+
if (gateStart === null || gateEnd === null || gateEnd < gateStart) continue;
|
|
112
|
+
const publishIndex = run.path.indexOf('publish');
|
|
113
|
+
const publishStart = publishIndex >= 0 ? at(publishIndex) : null;
|
|
114
|
+
return {
|
|
115
|
+
version: run.version, runId: run.runId, gateSeconds: Math.round((gateEnd - gateStart) / 1000),
|
|
116
|
+
...(publishStart !== null ? { toPublishSeconds: Math.round((publishStart - Date.parse(run.startedAt)) / 1000) } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const minutes = (seconds: number) => `${Math.round(seconds / 60)}분`;
|
|
123
|
+
const short = (title: string) => (title.length > 60 ? `${title.slice(0, 60)}…` : title);
|
|
124
|
+
|
|
125
|
+
export async function releasePreflight(version: string, deps: PreflightDeps = {}): Promise<PreflightResult> {
|
|
126
|
+
if (!STABLE.test(version)) throw new Error(`release preflight takes a stable version x.y.z: ${version}`);
|
|
127
|
+
const now = deps.now ?? new Date();
|
|
128
|
+
const next = nextPatch(version);
|
|
129
|
+
const findings: PreflightFinding[] = [];
|
|
130
|
+
const moveHint = (id: string) => `elanous release checklist move ${id} --from ${version} --to ${next} --reason "발행 뒤 칸 — 다음 판에서 확인"`;
|
|
131
|
+
|
|
132
|
+
// 1·2. Checklist gate simulation «at the cut» (the node and the run entry both judge after land-by) ⊕ post-publish cells.
|
|
133
|
+
const schedule = (deps.schedule ?? ((v: string) => getSchedule(v, deps.releaseRunDeps?.ledgerRoot)))(version);
|
|
134
|
+
// Always the stored cut (before or after it): the preflight simulates «the cut», not the moment it is run.
|
|
135
|
+
const judgeAt = schedule ? new Date(Date.parse(schedule.cutAt)) : now;
|
|
136
|
+
const data = listChecklist(version);
|
|
137
|
+
const { autoMoved, ...gate } = cutChecklistGate(version, schedule?.landBy, judgeAt);
|
|
138
|
+
const p0 = data.items.filter((item) => item.status === 'yellow' && item.priority === 'P0').map((item) => item.id);
|
|
139
|
+
const byId = new Map(data.items.map((item) => [item.id, item]));
|
|
140
|
+
const blockReason = new Map<string, string>();
|
|
141
|
+
for (const id of gate.red) blockReason.set(id, '빨강');
|
|
142
|
+
for (const id of gate.undecided) blockReason.set(id, '노랑 · 처분 없음(판정 없음)');
|
|
143
|
+
for (const id of gate.blocked) blockReason.set(id, p0.includes(id) ? 'P0 노랑' : '노랑 · 처분 block');
|
|
144
|
+
// The scheduled start (`release run --if-ready` · `auto-start`) first asks releaseReadiness — the same call the CLI
|
|
145
|
+
// makes, judged at the cut. It shares cutChecklistGate (#24557); an injected deps.checklist may still differ, so any
|
|
146
|
+
// cell it refuses that the cut judgement did not is reported too (report, don't re-rule).
|
|
147
|
+
try {
|
|
148
|
+
const readiness = releaseReadiness(version, { ledgerRoot: deps.releaseRunDeps?.ledgerRoot, checklist: deps.releaseRunDeps?.checklist, now: () => judgeAt });
|
|
149
|
+
if (readiness.reason === 'checklist-blocked') {
|
|
150
|
+
for (const id of [...readiness.details.red, ...readiness.details.undecided, ...readiness.details.blocked]) {
|
|
151
|
+
if (!blockReason.has(id)) blockReason.set(id, '예약 시작(release run --if-ready · auto-start)의 준비 검사(releaseReadiness)가 런을 거부한다');
|
|
152
|
+
}
|
|
153
|
+
} else if (readiness.reason === 'already-published' || readiness.reason === 'already-running') {
|
|
154
|
+
// Both refuse the scheduled start — a running release (live run.lock) or a published one.
|
|
155
|
+
findings.push({ level: 'block', check: 'run-input', message: `준비 검사(releaseReadiness)가 예약 시작을 거부한다: ${readiness.reason} (${readiness.details})` });
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
findings.push({ level: 'block', check: 'run-input', message: `준비 검사(releaseReadiness) 오류 — ${error instanceof Error ? error.message : String(error)}` });
|
|
159
|
+
}
|
|
160
|
+
for (const [id, reason] of blockReason) {
|
|
161
|
+
const item = byId.get(id);
|
|
162
|
+
const postpub = item ? isPostPublishCell(item) : false;
|
|
163
|
+
findings.push({
|
|
164
|
+
level: 'block', check: postpub ? 'postpub' : 'checklist', id,
|
|
165
|
+
message: `${id} — ${reason}${postpub ? ' · 발행 뒤 칸이 이 판에 남아 있다' : ''}${item ? ` · «${short(item.title)}»` : ''}`,
|
|
166
|
+
...(postpub ? { suggestion: moveHint(id) } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const item of data.items) {
|
|
170
|
+
if (blockReason.has(item.id) || !isPostPublishCell(item)) continue;
|
|
171
|
+
findings.push({ level: 'warn', check: 'postpub', id: item.id, message: `${item.id} — 발행 뒤 칸이 이 판에 남아 있다(지금은 막지 않음 · 상태 ${item.status} · 처분 ${item.disposition ?? '-'})`, suggestion: moveHint(item.id) });
|
|
172
|
+
}
|
|
173
|
+
for (const id of autoMoved) {
|
|
174
|
+
if (!blockReason.has(id)) findings.push({ level: 'warn', check: 'checklist', id, message: `${id} — 처분 없는 노랑 · 컷(착지 마감 뒤)에서 노드가 ${next} 로 자동 이월한다` });
|
|
175
|
+
}
|
|
176
|
+
for (const p of gate.parity ?? []) findings.push({ level: 'warn', check: 'checklist', id: p.id, message: `${p.id} — 화면 짝 경고: ${p.why}` });
|
|
177
|
+
if (!blockReason.size) findings.push({ level: 'ok', check: 'checklist', message: `체크리스트 게이트 모의 통과 — ${data.items.length}칸 · 이월 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length}` });
|
|
178
|
+
|
|
179
|
+
// 3. The same input check `release run --dry-run` makes.
|
|
180
|
+
let runPrevious: string | undefined;
|
|
181
|
+
try {
|
|
182
|
+
const result = await (deps.runInput ?? ((v: string) => runUnattendedRelease({ version: v, dryRun: true }, deps.releaseRunDeps ?? {})))(version);
|
|
183
|
+
const input = result.input as { previousVersion?: string; gatePodPool?: string };
|
|
184
|
+
runPrevious = input.previousVersion;
|
|
185
|
+
findings.push({ level: 'ok', check: 'run-input', message: `release run --dry-run 입력 OK — 직전 판 ${input.previousVersion ?? '-'} · 게이트 풀 ${input.gatePodPool ?? '-'}` });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
findings.push({ level: 'block', check: 'run-input', message: `release run --dry-run 입력 오류 — ${error instanceof Error ? error.message : String(error)}` });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 4. Landing freeze (emergency switch since RELEASE-BRANCH): when on, `release run` refuses.
|
|
191
|
+
try {
|
|
192
|
+
const frozen = (deps.freeze ?? (() => readLandingFreeze(deps.releaseRunDeps?.freezeRoot)))();
|
|
193
|
+
if (frozen) findings.push({ level: 'block', check: 'freeze', message: `${landingFreezeMessage(frozen)} — release run 이 거부한다`, suggestion: 'elanous freeze off (또는 의도한 강행이면 release run --force-freeze)' });
|
|
194
|
+
else findings.push({ level: 'ok', check: 'freeze', message: '착지 동결 꺼짐' });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
findings.push({ level: 'block', check: 'freeze', message: `동결 상태를 못 읽었다 — ${error instanceof Error ? error.message : String(error)}` });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 5. Schedule sanity: land-by = cut − 30 min · cut→publish window vs the previous measured gate.
|
|
200
|
+
let scheduleOut: PreflightResult['schedule'] = null;
|
|
201
|
+
let gateMeasure: GateMeasure | null = null;
|
|
202
|
+
let fallbackGateMeasure: GateMeasure | null = null;
|
|
203
|
+
let previousVersion: string | null = deps.previousVersion ?? runPrevious ?? null;
|
|
204
|
+
if (!previousVersion) { try { previousVersion = latestPublishedPreviousVersion(version, deps.releaseRunDeps?.ledgerRoot); } catch { previousVersion = null; } }
|
|
205
|
+
if (!schedule) {
|
|
206
|
+
findings.push({ level: 'warn', check: 'schedule', message: `판 일정 없음 — elanous release schedule set --version ${version} --cut-at <iso> --land-by <iso>` });
|
|
207
|
+
} else {
|
|
208
|
+
const cut = Date.parse(schedule.cutAt);
|
|
209
|
+
const publishAt = deps.publishAt ? new Date(deps.publishAt).toISOString() : cadencePublishAt(schedule.cutAt);
|
|
210
|
+
scheduleOut = { cutAt: schedule.cutAt, landBy: schedule.landBy, publishAt, publishAtSource: deps.publishAt ? 'option' : 'cadence' };
|
|
211
|
+
if (!schedule.landBy) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 없음 — 컷 ${formatKst(schedule.cutAt)} · 규칙은 컷 − 30분` });
|
|
212
|
+
else if (cut - Date.parse(schedule.landBy) < 30 * 60_000) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 ${formatKst(schedule.landBy)} 이 컷 − 30분보다 늦다(컷 ${formatKst(schedule.cutAt)})` });
|
|
213
|
+
else findings.push({ level: 'ok', check: 'schedule', message: `일정 — 착지 마감 ${formatKst(schedule.landBy)} · 컷 ${formatKst(schedule.cutAt)}` });
|
|
214
|
+
gateMeasure = previousVersion ? releaseGateMeasure({ version: previousVersion }, deps.graphRunsRoot) : null;
|
|
215
|
+
if (!gateMeasure) fallbackGateMeasure = releaseGateMeasure({ below: version }, deps.graphRunsRoot);
|
|
216
|
+
const windowSeconds = Math.round((Date.parse(publishAt) - cut) / 1000);
|
|
217
|
+
const publishLabel = `발행 ${formatKst(publishAt)}${deps.publishAt ? '' : '(기본: 판 주기 07·18시 · --publish-at 로 바꾼다)'}`;
|
|
218
|
+
if (!gateMeasure) {
|
|
219
|
+
const fallback = fallbackGateMeasure ? ` · 참고: 가장 최근 실측은 ${fallbackGateMeasure.version} 게이트 ${minutes(fallbackGateMeasure.gateSeconds)}(직전 판 아님)` : ' · 게이트 실측 없음';
|
|
220
|
+
findings.push({ level: 'warn', check: 'schedule', message: `직전 판${previousVersion ? `(${previousVersion})` : ''} 실측 없음 — 컷→${publishLabel} 창 ${minutes(windowSeconds)}${fallback}` });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const measured = `직전 실측 ${gateMeasure.version} 게이트 ${minutes(gateMeasure.gateSeconds)}${gateMeasure.toPublishSeconds !== undefined ? ` · 런 시작→발행 노드 ${minutes(gateMeasure.toPublishSeconds)}` : ''}`;
|
|
224
|
+
const need = Math.max(gateMeasure.gateSeconds, gateMeasure.toPublishSeconds ?? 0);
|
|
225
|
+
findings.push(windowSeconds < need
|
|
226
|
+
? { level: 'warn', check: 'schedule', message: `컷→${publishLabel} 창 ${minutes(windowSeconds)} < ${measured} — 발행이 늦어진다` }
|
|
227
|
+
: { level: 'ok', check: 'schedule', message: `컷→${publishLabel} 창 ${minutes(windowSeconds)} ≥ ${measured}` });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const blockers = findings.filter((f) => f.level === 'block').length;
|
|
232
|
+
const warnings = findings.filter((f) => f.level === 'warn').length;
|
|
233
|
+
debug.log('release.preflight', 'checked', { version, blockers, warnings });
|
|
234
|
+
return { version, next, verdict: blockers ? 'block' : warnings ? 'warn' : 'ok', blockers, warnings, findings, schedule: scheduleOut, previousVersion, gateMeasure, fallbackGateMeasure };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ICON: Record<PreflightLevel, string> = { ok: '✅', warn: '⚠️', block: '⛔' };
|
|
238
|
+
|
|
239
|
+
export function formatPreflight(result: PreflightResult): string[] {
|
|
240
|
+
const order: PreflightLevel[] = ['block', 'warn', 'ok'];
|
|
241
|
+
const lines = [...result.findings].sort((a, b) => order.indexOf(a.level) - order.indexOf(b.level))
|
|
242
|
+
.flatMap((f) => [`${ICON[f.level]} ${f.message}`, ...(f.suggestion ? [` → ${f.suggestion}`] : [])]);
|
|
243
|
+
const verdict = result.verdict === 'block' ? `⛔ 판정: 막힘 — ⛔${result.blockers} ⚠️${result.warnings} · 컷 전에 고친다`
|
|
244
|
+
: result.verdict === 'warn' ? `⚠️ 판정: 통과(경고 ${result.warnings})` : '✅ 판정: 통과';
|
|
245
|
+
return [`release preflight ${result.version} (다음 판 ${result.next})`, ...lines, verdict];
|
|
246
|
+
}
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { WARNING_ONLY } from './auto-approve-node.js';
|
|
6
6
|
import { beginLandingMerge, landingFreezeMessage } from '../../src/release-loop/landing-freeze.js';
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import { prereleaseKind } from './release-version.js';
|
|
8
9
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
|
|
9
10
|
|
|
10
11
|
export function publicNotes(markdown: string, pages: { pages: Array<{ id: string; slug?: string; source?: string }> }): string {
|
|
@@ -20,6 +21,12 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** RELEASE-REHEARSAL-RC: prerelease notes open with a label, so nobody mistakes a rehearsal for the stable release. */
|
|
25
|
+
export function labelPrerelease(version: string, body: string): string {
|
|
26
|
+
if (prereleaseKind(version) === null) return body;
|
|
27
|
+
return `> **Pre-release** — ${version} is a pre-release, not the latest version. Install it with \`npm i -g elanous@next\`; \`latest\` stays on the current stable release.\n\n${body}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/** Polls each release asset until it answers 200 (following redirects). Returns the names still missing. */
|
|
24
31
|
export function waitForAssets(run: CommandRunner, repo: string, tag: string, names: readonly string[], waitSeconds: unknown = 600,
|
|
25
32
|
sleep: (ms: number) => void = (ms) => Bun.sleepSync(ms)): string[] {
|
|
@@ -61,7 +68,7 @@ export function runPublish(run: CommandRunner = runCommand, prodFreezeRoot?: str
|
|
|
61
68
|
const registry = run('git', ['show', `${branch}:website/pages.json`]);
|
|
62
69
|
if (doc.status !== 0 || registry.status !== 0) throw new Error(`docs branch missing notes or pages: ${branch}`);
|
|
63
70
|
if (!doc.stdout.startsWith(`# ${version}\n`)) throw new Error('release notes version mismatch');
|
|
64
|
-
const body = publicNotes(doc.stdout, JSON.parse(registry.stdout));
|
|
71
|
+
const body = labelPrerelease(version, publicNotes(doc.stdout, JSON.parse(registry.stdout)));
|
|
65
72
|
const dir = mkdtempSync(join(tmpdir(), 'release-publish-notes-'));
|
|
66
73
|
try {
|
|
67
74
|
const file = join(dir, 'notes.md');
|