elanous 0.2.16 → 0.2.18
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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- 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 +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- 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/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- 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 +77 -11
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- 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 +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- 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 +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +149 -41
- package/scripts/release-story/draft.ts +42 -14
- 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/src/agent-mission/driver.ts +53 -10
- 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/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/tasks-cli.ts +309 -3
- 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-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- 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/harness/harness-cli-command.ts +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- 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 -4
- package/src/intake-plane/discord-seat-work.ts +8 -2
- 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/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +24 -14
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- package/src/self-dev/ask-path-candidates.ts +68 -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/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +93 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.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/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.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/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -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[] {
|
|
@@ -36,7 +43,7 @@ export function waitForAssets(run: CommandRunner, repo: string, tag: string, nam
|
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
export function runPublish(run: CommandRunner = runCommand) {
|
|
46
|
+
export function runPublish(run: CommandRunner = runCommand, prodFreezeRoot?: string) {
|
|
40
47
|
const context = readGraphContext();
|
|
41
48
|
const version = context.input.version;
|
|
42
49
|
// A measured automatic decision or the existing human approval must authorize publication.
|
|
@@ -61,14 +68,14 @@ export function runPublish(run: CommandRunner = runCommand) {
|
|
|
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');
|
|
68
75
|
writeFileSync(file, body);
|
|
69
76
|
// Same in-flight marker as merges: `freeze on` waits for a publication that already passed this check.
|
|
70
77
|
// Unlike a merge, a frozen publication is not queued: it fails, and the release run is run again after `freeze off`.
|
|
71
|
-
const landing = beginLandingMerge();
|
|
78
|
+
const landing = beginLandingMerge(undefined, undefined, prodFreezeRoot);
|
|
72
79
|
const frozen = landing.frozen;
|
|
73
80
|
if (frozen) {
|
|
74
81
|
landing.end(); // already released by beginLandingMerge when frozen; kept explicit so no path leaves a marker
|
|
@@ -1,6 +1,140 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../../src/seat-dispatch/seat-request-ledger.js';
|
|
1
5
|
import { debug } from '../../src/debug/log.js';
|
|
2
6
|
import type { ReleaseManifest } from '../../src/release-loop/manifest.js';
|
|
3
7
|
|
|
8
|
+
type DocsFollowEvidence = { location: string; verifiedAt: string; publishedAt: string };
|
|
9
|
+
type DocsFollowExposure = {
|
|
10
|
+
verdict: 'public' | 'beta' | 'internal';
|
|
11
|
+
judge: 'MK';
|
|
12
|
+
rubric: string;
|
|
13
|
+
verifiedAt: string;
|
|
14
|
+
};
|
|
15
|
+
export type DocsFollowItem = {
|
|
16
|
+
id: string;
|
|
17
|
+
status: string;
|
|
18
|
+
sha?: string;
|
|
19
|
+
exposure?: DocsFollowExposure;
|
|
20
|
+
docs?: DocsFollowEvidence;
|
|
21
|
+
homepage?: { elanous: DocsFollowEvidence; elanvital: DocsFollowEvidence };
|
|
22
|
+
readme?: DocsFollowEvidence;
|
|
23
|
+
};
|
|
24
|
+
type DocsFollowCoverage = {
|
|
25
|
+
id: string;
|
|
26
|
+
sha: string | null;
|
|
27
|
+
docs: boolean;
|
|
28
|
+
homepage: boolean;
|
|
29
|
+
readme: boolean;
|
|
30
|
+
missing: Array<'docs' | 'homepage' | 'readme'>;
|
|
31
|
+
};
|
|
32
|
+
type DocsFollowReport = {
|
|
33
|
+
coverage: DocsFollowCoverage[];
|
|
34
|
+
pending: DocsFollowCoverage[];
|
|
35
|
+
unassessed: string[];
|
|
36
|
+
ratio: number | null;
|
|
37
|
+
verdict: 'pass' | 'fail' | 'unmeasured';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** An attestation is not coverage unless it points to a measured, timestamped surface. */
|
|
41
|
+
function evidenced(value: DocsFollowEvidence | undefined, measuredAt?: string): boolean {
|
|
42
|
+
const at = value?.verifiedAt ? Date.parse(value.verifiedAt) : NaN;
|
|
43
|
+
const published = value?.publishedAt ? Date.parse(value.publishedAt) : NaN;
|
|
44
|
+
return !!value?.location?.trim() && Number.isFinite(at) && Number.isFinite(published)
|
|
45
|
+
&& /(?:Z|[+-]\d\d:\d\d)$/.test(value.verifiedAt)
|
|
46
|
+
&& /(?:Z|[+-]\d\d:\d\d)$/.test(value.publishedAt)
|
|
47
|
+
&& published <= at && (measuredAt === undefined || at <= Date.parse(measuredAt));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Unknown exposure/landing never becomes a published claim; it remains visible as an unassessed item. */
|
|
51
|
+
export function calculateDocsFollow(manifest: ReleaseManifest, items: readonly DocsFollowItem[], publishedAt?: string, measuredAt?: string): DocsFollowReport {
|
|
52
|
+
const landed = new Set(manifest.in.map((entry) => entry.sha).filter(Boolean));
|
|
53
|
+
const green = items.filter((item) => item.status === 'green');
|
|
54
|
+
const ids = new Set<string>();
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
if (!item.id?.trim() || ids.has(item.id)) throw new Error('invalid or duplicate docs-follow item');
|
|
57
|
+
ids.add(item.id);
|
|
58
|
+
}
|
|
59
|
+
const judged = (item: DocsFollowItem) => item.exposure?.judge === 'MK'
|
|
60
|
+
&& !!item.exposure.rubric?.trim() && evidenced({ location: item.exposure.rubric, verifiedAt: item.exposure.verifiedAt,
|
|
61
|
+
publishedAt: item.exposure.verifiedAt }, measuredAt)
|
|
62
|
+
&& ['public', 'beta', 'internal'].includes(item.exposure.verdict);
|
|
63
|
+
const unassessed = green.filter((item) => !judged(item) || !item.sha || !landed.has(item.sha))
|
|
64
|
+
.map((item) => item.id);
|
|
65
|
+
const supplied = new Set(items.map((item) => item.sha));
|
|
66
|
+
for (const entry of manifest.in) {
|
|
67
|
+
if (entry.sha && (entry.kind === 'feat' || entry.kind === 'fix' || entry.kind === 'security') && entry.line && !supplied.has(entry.sha)) {
|
|
68
|
+
unassessed.push(`landing:${entry.sha}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const visible = green.filter((item) => judged(item) && (item.exposure?.verdict === 'public' || item.exposure?.verdict === 'beta') && item.sha && landed.has(item.sha));
|
|
72
|
+
const coverage: DocsFollowCoverage[] = visible.map((item) => {
|
|
73
|
+
const docs = evidenced(item.docs, measuredAt);
|
|
74
|
+
const homepage = evidenced(item.homepage?.elanous, measuredAt) && evidenced(item.homepage?.elanvital, measuredAt);
|
|
75
|
+
const readme = evidenced(item.readme, measuredAt);
|
|
76
|
+
return { id: item.id, sha: item.sha!, docs, homepage, readme,
|
|
77
|
+
missing: ([['docs', docs], ['homepage', homepage], ['readme', readme]] as const)
|
|
78
|
+
.filter(([, present]) => !present).map(([surface]) => surface) };
|
|
79
|
+
});
|
|
80
|
+
const pending = coverage.filter((item) => !item.docs || !item.homepage);
|
|
81
|
+
const covered = coverage.filter((item) => item.docs && item.homepage && item.readme).length;
|
|
82
|
+
const start = publishedAt ? Date.parse(publishedAt) : NaN;
|
|
83
|
+
const end = measuredAt ? Date.parse(measuredAt) : NaN;
|
|
84
|
+
const deadline = start + 24 * 60 * 60 * 1000;
|
|
85
|
+
const validWindow = Number.isFinite(start) && Number.isFinite(end) && end >= start
|
|
86
|
+
&& /(?:Z|[+-]\d\d:\d\d)$/.test(publishedAt ?? '')
|
|
87
|
+
&& /(?:Z|[+-]\d\d:\d\d)$/.test(measuredAt ?? '');
|
|
88
|
+
const atDeadline = validWindow && end > deadline
|
|
89
|
+
? calculateDocsFollow(manifest, items, publishedAt, new Date(deadline).toISOString()) : null;
|
|
90
|
+
const timely = atDeadline ?? { coverage, pending, unassessed,
|
|
91
|
+
ratio: coverage.length && !unassessed.length ? covered / coverage.length : null };
|
|
92
|
+
const pass = validWindow && timely.coverage.length > 0 && !timely.unassessed.length
|
|
93
|
+
&& timely.ratio !== null && timely.ratio >= 0.8 && !timely.pending.length;
|
|
94
|
+
// Missing proof may still establish timely coverage; only a verified late publication rules it out.
|
|
95
|
+
const lateProof = (proof: DocsFollowEvidence | undefined) => evidenced(proof, measuredAt)
|
|
96
|
+
&& Date.parse(proof!.publishedAt) > deadline;
|
|
97
|
+
const impossible = validWindow && end > deadline && !pass && !timely.unassessed.length
|
|
98
|
+
&& visible.length > 0 && (
|
|
99
|
+
visible.some((item) => lateProof(item.docs) || lateProof(item.homepage?.elanous) || lateProof(item.homepage?.elanvital))
|
|
100
|
+
|| visible.filter((item) => ![item.docs, item.homepage?.elanous, item.homepage?.elanvital, item.readme]
|
|
101
|
+
.some(lateProof)).length / visible.length < 0.8
|
|
102
|
+
);
|
|
103
|
+
return { coverage, pending, unassessed,
|
|
104
|
+
ratio: coverage.length && !unassessed.length ? covered / coverage.length : null,
|
|
105
|
+
verdict: pass ? 'pass' as const : impossible ? 'fail' as const : 'unmeasured' as const };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Persist a per-release report and queue each missing docs/home item once for MK. */
|
|
109
|
+
export function recordDocsFollow(root: string, manifest: ReleaseManifest, items: readonly DocsFollowItem[], now: Date = new Date(), publishedAt?: string) {
|
|
110
|
+
if (!Number.isFinite(now.getTime())) throw new Error('invalid docs-follow time');
|
|
111
|
+
if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) throw new Error('invalid docs-follow version');
|
|
112
|
+
const report = calculateDocsFollow(manifest, items, publishedAt, now.toISOString());
|
|
113
|
+
const path = join(root, 'release', manifest.version, 'docs-follow.json');
|
|
114
|
+
const journal = join(root, 'seat-requests', 'requests.jsonl');
|
|
115
|
+
withSeatRequestLedgerLock(journal, () => {
|
|
116
|
+
const known = new Set(listSeatRequests(root).map((row) => row.key));
|
|
117
|
+
const missing = [
|
|
118
|
+
...report.pending.map((item) => ({ id: item.id, text: `문서 반영 대기 목록 v${manifest.version} · ${item.id}: ${item.missing.join(', ')} — 착지 ${item.sha}; docs·홈 실측 뒤 닫기` })),
|
|
119
|
+
...report.unassessed.map((id) => ({ id, text: `문서 반영 대기 목록 v${manifest.version} · ${id}: 착지·green·EXPOSE-RUBRIC 근거 미판정 — 확인 전 공개 문면 금지` })),
|
|
120
|
+
];
|
|
121
|
+
const rows = missing.filter((item) => !known.has(`docs-follow:${manifest.version}:${item.id}`)).map((item) => ({
|
|
122
|
+
key: `docs-follow:${manifest.version}:${item.id}`, seat: 'MK', status: 'queued' as const,
|
|
123
|
+
text: item.text, queuedAt: now.toISOString(), ref: `release:${manifest.version}:docs-follow`,
|
|
124
|
+
}));
|
|
125
|
+
mkdirSync(join(root, 'release', manifest.version), { recursive: true });
|
|
126
|
+
const temp = `${path}.${createHash('sha256').update(now.toISOString()).digest('hex').slice(0, 12)}.tmp`;
|
|
127
|
+
try {
|
|
128
|
+
writeFileSync(temp, JSON.stringify({ version: manifest.version, measuredAt: now.toISOString(), ...report }, null, 2) + '\n');
|
|
129
|
+
renameSync(temp, path);
|
|
130
|
+
} finally {
|
|
131
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
132
|
+
}
|
|
133
|
+
appendSeatRequestRows(journal, rows);
|
|
134
|
+
});
|
|
135
|
+
return report;
|
|
136
|
+
}
|
|
137
|
+
|
|
4
138
|
/** Render public IN lines only; keep private titles and PR numbers out of release notes. */
|
|
5
139
|
export function renderReleaseNotes(manifest: ReleaseManifest, version: string): string {
|
|
6
140
|
const sections = [
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
4
|
-
import {
|
|
4
|
+
import { cutChecklistGate, type ChecklistGate } from '../../src/release-loop/checklist.js';
|
|
5
|
+
|
|
6
|
+
/** Only the verdict fields are read — so a `release run` deps checklist (`Pick<…>`) fits as well. */
|
|
7
|
+
export type ReadinessChecklist = Pick<ChecklistGate, 'ok' | 'red' | 'undecided' | 'blocked'>;
|
|
5
8
|
import { getSchedule, formatKst } from '../../src/release-loop/release-schedule.js';
|
|
6
9
|
import { debug } from '../../src/debug/log.js';
|
|
7
10
|
|
|
@@ -9,12 +12,12 @@ export type ReleaseReadiness =
|
|
|
9
12
|
| { ready: false; reason: 'already-published'; details: string }
|
|
10
13
|
| { ready: false; reason: 'already-running'; details: string }
|
|
11
14
|
| { ready: false; reason: 'before-cut'; details: string }
|
|
12
|
-
| { ready: false; reason: 'checklist-blocked'; details:
|
|
13
|
-
| { ready: true; reason: 'ready'; details:
|
|
15
|
+
| { ready: false; reason: 'checklist-blocked'; details: ReadinessChecklist }
|
|
16
|
+
| { ready: true; reason: 'ready'; details: ReadinessChecklist };
|
|
14
17
|
|
|
15
18
|
export interface ReleaseReadinessDeps {
|
|
16
19
|
ledgerRoot?: string;
|
|
17
|
-
checklist?:
|
|
20
|
+
checklist?: (version: string) => ReadinessChecklist;
|
|
18
21
|
isPidAlive?: (pid: number) => boolean;
|
|
19
22
|
now?: () => Date;
|
|
20
23
|
}
|
|
@@ -57,11 +60,13 @@ export function releaseReadiness(version: string, deps: ReleaseReadinessDeps = {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
const schedule = getSchedule(version, deps.ledgerRoot);
|
|
60
|
-
|
|
63
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
64
|
+
if (schedule && now.getTime() < Date.parse(schedule.cutAt)) {
|
|
61
65
|
debug.log('release.schedule', 'before-cut', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
|
|
62
66
|
return { ready: false, reason: 'before-cut', details: `before-cut (${formatKst(schedule.cutAt)})` };
|
|
63
67
|
}
|
|
64
|
-
|
|
68
|
+
// GATE-ENTRY-ALIGN: the same cut judgement as `release run`'s entry check and the checklist-gate node.
|
|
69
|
+
const gate = (deps.checklist ?? ((v: string) => cutChecklistGate(v, schedule?.landBy, now)))(version);
|
|
65
70
|
if (!gate.ok) return { ready: false, reason: 'checklist-blocked', details: gate };
|
|
66
71
|
return { ready: true, reason: 'ready', details: gate };
|
|
67
72
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Release version shapes for the release loop: a stable x.y.z or a prerelease x.y.z-(rc|alpha|beta).N.
|
|
2
|
+
// RELEASE-BRANCH ⊕ RELEASE-REHEARSAL-RC (10-06): prerelease runs publish to npm `next` and skip the
|
|
3
|
+
// post-publish nodes that touch main or the operating hosts.
|
|
4
|
+
|
|
5
|
+
export const PRERELEASE_KINDS = ['rc', 'alpha', 'beta'] as const;
|
|
6
|
+
export type PrereleaseKind = (typeof PRERELEASE_KINDS)[number];
|
|
7
|
+
|
|
8
|
+
const RELEASE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(rc|alpha|beta)\.(0|[1-9]\d*))?$/;
|
|
9
|
+
const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
10
|
+
|
|
11
|
+
export function isStableVersion(version: string): boolean { return STABLE_VERSION.test(version); }
|
|
12
|
+
|
|
13
|
+
export function isReleaseVersion(version: string): boolean { return RELEASE_VERSION.test(version); }
|
|
14
|
+
|
|
15
|
+
/** `0.2.18-rc.0` → `0.2.18`; a stable version is returned as is. Throws on anything else. */
|
|
16
|
+
export function baseVersion(version: string): string {
|
|
17
|
+
const match = RELEASE_VERSION.exec(version);
|
|
18
|
+
if (!match) throw new Error(`invalid release version: ${version}`);
|
|
19
|
+
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The prerelease kind of `0.2.18-rc.0` (`rc`), or null for a stable version. */
|
|
23
|
+
export function prereleaseKind(version: string): PrereleaseKind | null {
|
|
24
|
+
const match = RELEASE_VERSION.exec(version);
|
|
25
|
+
if (!match) throw new Error(`invalid release version: ${version}`);
|
|
26
|
+
return (match[4] as PrereleaseKind | undefined) ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isPrereleaseKind(value: unknown): value is PrereleaseKind {
|
|
30
|
+
return typeof value === 'string' && (PRERELEASE_KINDS as readonly string[]).includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The next free prerelease number for `base` and `kind`, given the names already taken
|
|
35
|
+
* (release branches `release/<v>` and tags `v<v>`, with or without those prefixes).
|
|
36
|
+
*/
|
|
37
|
+
export function nextPrereleaseVersion(base: string, kind: PrereleaseKind, taken: readonly string[]): string {
|
|
38
|
+
if (!STABLE_VERSION.test(base)) throw new Error(`prerelease base must be x.y.z: ${base}`);
|
|
39
|
+
const prefix = `${base}-${kind}.`;
|
|
40
|
+
let next = 0;
|
|
41
|
+
for (const raw of taken) {
|
|
42
|
+
const name = raw.replace(/^refs\/heads\//, '').replace(/^refs\/tags\//, '').replace(/^release\//, '').replace(/^v(?=\d)/, '').replace(/\^\{\}$/, '');
|
|
43
|
+
if (!name.startsWith(prefix)) continue;
|
|
44
|
+
const n = name.slice(prefix.length);
|
|
45
|
+
if (/^(0|[1-9]\d*)$/.test(n)) next = Math.max(next, Number(n) + 1);
|
|
46
|
+
}
|
|
47
|
+
return `${prefix}${next}`;
|
|
48
|
+
}
|