elanous 0.2.18 → 0.2.20
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/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.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 +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 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- 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 +16 -15
- 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 +16 -15
- 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 +24 -23
- 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/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- 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 +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- 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/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- 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/digest.ts +39 -15
- 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/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -5,4 +5,9 @@ export function unattendedSetupHint(): string {
|
|
|
5
5
|
return `unattended: \`${UNATTENDED_SETUP_COMMAND}\``;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** One-line next step printed after a non-TTY onboarding refusal (FIRST-CHAT-NONTTY). */
|
|
9
|
+
export function unattendedNextStepLine(): string {
|
|
10
|
+
return `next: ${unattendedSetupHint()}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export const SKILLS_LIST_DESCRIPTION = 'List elanous skills from configured skill directories (skills.activeSet / skills.dirs and packaged skills): subdirectories and the first non-heading line of each SKILL.md. Read-only.';
|
package/src/onboarding.ts
CHANGED
|
@@ -59,7 +59,7 @@ import type {
|
|
|
59
59
|
import { showStepOr, chooseFrom } from './onboarding/io-extended.js';
|
|
60
60
|
import { askWithHelp } from './onboarding/wire-helpers.js';
|
|
61
61
|
import { stepTransition } from './onboarding/transition.js';
|
|
62
|
-
import { unattendedSetupHint } from './onboarding/entry-hints.js';
|
|
62
|
+
import { unattendedNextStepLine, unattendedSetupHint } from './onboarding/entry-hints.js';
|
|
63
63
|
import { continueHereLines } from './onboarding/continue-here.js';
|
|
64
64
|
import type { NexusShowResult } from './cli/nexus-show.js';
|
|
65
65
|
import type { TailscaleProbe } from './nexus/onboarding/tailscale-probe.js';
|
|
@@ -1370,15 +1370,33 @@ export class OnboardingRefusedError extends Error {
|
|
|
1370
1370
|
}
|
|
1371
1371
|
}
|
|
1372
1372
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1373
|
+
export interface OnboardingRefusalIO {
|
|
1374
|
+
printError: (line: string) => void;
|
|
1375
|
+
setExitCode: (code: number) => void;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const defaultRefusalIO: OnboardingRefusalIO = {
|
|
1379
|
+
printError: (line) => console.error(line),
|
|
1380
|
+
setExitCode: (code) => { process.exitCode = code; },
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
/** Handle only typed refusals; the agent setup hint applies to its non-TTY path alone.
|
|
1384
|
+
* FIRST-CHAT-NONTTY: on the non-TTY path of any other entrance the refusal ends with a
|
|
1385
|
+
* one-line next step (`unattendedNextStepLine()`), so a script or app that called first knows
|
|
1386
|
+
* what to run. The exit code stays 2. */
|
|
1387
|
+
export function handleOnboardingRefusal(
|
|
1388
|
+
error: unknown,
|
|
1389
|
+
entrance: 'agent' | 'other',
|
|
1390
|
+
io: OnboardingRefusalIO = defaultRefusalIO,
|
|
1391
|
+
): boolean {
|
|
1375
1392
|
if (!(error instanceof OnboardingRefusedError) || error.code !== 'onboarding-refused') return false;
|
|
1376
1393
|
if (entrance === 'agent' && error.reason === 'non-tty') {
|
|
1377
|
-
|
|
1394
|
+
io.printError(`elanous agent needs a configured LLM. Run \`elanous onboarding\` in a terminal, or ${unattendedSetupHint()} for unattended setup.`);
|
|
1378
1395
|
} else {
|
|
1379
|
-
|
|
1396
|
+
io.printError(error.message);
|
|
1397
|
+
if (error.reason === 'non-tty') io.printError(unattendedNextStepLine());
|
|
1380
1398
|
}
|
|
1381
|
-
|
|
1399
|
+
io.setExitCode(2);
|
|
1382
1400
|
return true;
|
|
1383
1401
|
}
|
|
1384
1402
|
|
|
@@ -40,6 +40,7 @@ export interface ChecklistHistory {
|
|
|
40
40
|
dev: string;
|
|
41
41
|
force?: true;
|
|
42
42
|
reason?: string;
|
|
43
|
+
generation?: number;
|
|
43
44
|
}
|
|
44
45
|
export interface Checklist {
|
|
45
46
|
version: string;
|
|
@@ -101,6 +102,7 @@ export function statusChangesSince(history: readonly ChecklistHistory[], sinceIs
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function mutate(v: string, apply: (data: Checklist, otherItems: (id: string) => store.ChecklistCollision[]) => boolean): Checklist {
|
|
105
|
+
// addItem/setItem/claimItem/removeItem/seedFromRoadmap reach the generation-fenced ledger write here.
|
|
104
106
|
return decodeClaimHistory(store.mutate(v, store.releasedVersion(), devVersion(), apply));
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -5,6 +5,8 @@ import { Database } from 'bun:sqlite';
|
|
|
5
5
|
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
6
6
|
import { CliUserError } from '../cli/cli-user-error.js';
|
|
7
7
|
import { debug } from '../debug/log.js';
|
|
8
|
+
import { getUserConfig } from '../user-config.js';
|
|
9
|
+
import { hqSeenGenerationPath, readHqLease, readSeenGeneration } from '../hq/hq.js';
|
|
8
10
|
import { devVersion, type Checklist, type ChecklistHistory, type ChecklistItem } from './checklist.js';
|
|
9
11
|
|
|
10
12
|
export function validateVersion(version: string): void {
|
|
@@ -32,9 +34,9 @@ export function releasedVersion(ledgerRoot = releaseLedgerRoot()): string {
|
|
|
32
34
|
const walWait = new Int32Array(new SharedArrayBuffer(4));
|
|
33
35
|
const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
|
|
34
36
|
imported_versions: ['json_hash', 'imported_at'],
|
|
35
|
-
events: ['reason'],
|
|
37
|
+
events: ['reason', 'generation'],
|
|
36
38
|
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
|
|
37
|
-
release_schedules: ['freeze_from', 'freeze_until'],
|
|
39
|
+
release_schedules: ['freeze_from', 'freeze_until', 'publish_at'],
|
|
38
40
|
};
|
|
39
41
|
function schemaCurrent(db: Database): boolean {
|
|
40
42
|
return Object.entries(SCHEMA_COLUMNS).every(([table, names]) => {
|
|
@@ -72,23 +74,25 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
72
74
|
const columns = db.query('PRAGMA table_info(imported_versions)').all() as Array<{ name: string }>;
|
|
73
75
|
if (!columns.some((column) => column.name === 'json_hash')) db.exec('ALTER TABLE imported_versions ADD COLUMN json_hash TEXT');
|
|
74
76
|
if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
|
|
75
|
-
|
|
77
|
+
const eventColumns = db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>;
|
|
78
|
+
if (!eventColumns.some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
|
|
79
|
+
if (!eventColumns.some((column) => column.name === 'generation')) db.exec('ALTER TABLE events ADD COLUMN generation INTEGER');
|
|
76
80
|
db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
|
|
77
81
|
const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
|
|
78
82
|
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT']] as const) {
|
|
79
83
|
if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
|
|
80
84
|
}
|
|
81
85
|
const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
|
|
82
|
-
for (const name of ['freeze_from', 'freeze_until']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
|
|
86
|
+
for (const name of ['freeze_from', 'freeze_until', 'publish_at']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
|
|
83
87
|
}).immediate();
|
|
84
88
|
return db;
|
|
85
89
|
} catch (error) { db.close(); throw error; }
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; updatedAt: string; updatedBy: string }
|
|
89
|
-
type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; updated_at: string; updated_by: string };
|
|
92
|
+
export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; publishAt?: string | null; updatedAt: string; updatedBy: string }
|
|
93
|
+
type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; publish_at?: string | null; updated_at: string; updated_by: string };
|
|
90
94
|
function scheduleRow(row: StoredSchedule): ScheduleRow {
|
|
91
|
-
return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
|
|
95
|
+
return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), ...(row.publish_at ? { publishAt: row.publish_at } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
export function readSchedule(version: string, root?: string): ScheduleRow | null {
|
|
@@ -115,7 +119,7 @@ export function assignedVersions(root = releaseLedgerRoot()): string[] {
|
|
|
115
119
|
finally { db.close(); }
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ScheduleRow {
|
|
122
|
+
export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; publishAt?: string }, by: string, root?: string): ScheduleRow {
|
|
119
123
|
validateVersion(version);
|
|
120
124
|
const db = open(root);
|
|
121
125
|
try {
|
|
@@ -126,19 +130,22 @@ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?:
|
|
|
126
130
|
const landBy = patch.landBy ?? previous?.land_by ?? null;
|
|
127
131
|
const freezeFrom = patch.freezeFrom ?? previous?.freeze_from ?? null;
|
|
128
132
|
const freezeUntil = patch.freezeUntil ?? previous?.freeze_until ?? null;
|
|
133
|
+
const publishAt = patch.publishAt ?? previous?.publish_at ?? null;
|
|
134
|
+
if (publishAt && Date.parse(publishAt) <= Date.parse(cutAt)) throw new CliUserError('발행 시각은 컷보다 뒤여야 한다');
|
|
129
135
|
if (Boolean(freezeFrom) !== Boolean(freezeUntil) || (freezeFrom && freezeUntil && Date.parse(freezeFrom) >= Date.parse(freezeUntil))) throw new CliUserError('동결 시작·끝은 함께 주고 시작이 끝보다 앞서야 한다');
|
|
130
|
-
if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil) return scheduleRow(previous);
|
|
136
|
+
if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil && (previous.publish_at ?? null) === publishAt) return scheduleRow(previous);
|
|
131
137
|
const at = new Date().toISOString();
|
|
132
|
-
db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
133
|
-
ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
134
|
-
.run(version, cutAt, landBy, freezeFrom, freezeUntil, at, by);
|
|
138
|
+
db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, publish_at, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
139
|
+
ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, publish_at=excluded.publish_at, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
140
|
+
.run(version, cutAt, landBy, freezeFrom, freezeUntil, publishAt, at, by);
|
|
135
141
|
for (const [field, from, to] of [
|
|
136
142
|
['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
|
|
137
143
|
['freeze_from', previous?.freeze_from ?? null, freezeFrom], ['freeze_until', previous?.freeze_until ?? null, freezeUntil],
|
|
144
|
+
['publish_at', previous?.publish_at ?? null, publishAt],
|
|
138
145
|
] as const) {
|
|
139
146
|
if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
|
|
140
147
|
}
|
|
141
|
-
return { version, cutAt, landBy, updatedAt: at, updatedBy: by };
|
|
148
|
+
return { version, cutAt, landBy, ...(publishAt ? { publishAt } : {}), updatedAt: at, updatedBy: by };
|
|
142
149
|
});
|
|
143
150
|
} finally { db.close(); }
|
|
144
151
|
}
|
|
@@ -155,15 +162,15 @@ function readTransaction<T>(db: Database, work: () => T): T {
|
|
|
155
162
|
catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string; reason: string | null };
|
|
165
|
+
type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string; reason: string | null; generation: number | null };
|
|
159
166
|
function decode(value: string | null): unknown { return value === null ? null : JSON.parse(value); }
|
|
160
167
|
function historyRow(row: EventRow): ChecklistHistory {
|
|
161
168
|
return { at: row.at, by: row.by, id: row.feature_id, field: row.field, from: decode(row.from), to: decode(row.to), released: row.released, dev: row.dev,
|
|
162
|
-
...(row.reason !== null ? { reason: row.reason } : {}) };
|
|
169
|
+
...(row.reason !== null ? { reason: row.reason } : {}), ...(row.generation !== null ? { generation: row.generation } : {}) };
|
|
163
170
|
}
|
|
164
171
|
function record(db: Database, version: string, entry: ChecklistHistory): void {
|
|
165
|
-
db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
166
|
-
.run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev, entry.reason ?? null);
|
|
172
|
+
db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev, reason, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
173
|
+
.run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev, entry.reason ?? null, entry.generation ?? null);
|
|
167
174
|
}
|
|
168
175
|
function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
169
176
|
return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
|
|
@@ -334,6 +341,30 @@ export interface ChecklistCollision { version: string; owner?: string; title: st
|
|
|
334
341
|
|
|
335
342
|
export function mutate(version: string, released: string, dev: string, apply: (data: Checklist, otherItems: (id: string) => ChecklistCollision[]) => boolean): Checklist {
|
|
336
343
|
validateVersion(version);
|
|
344
|
+
// COORD-HA ①(RFC-loop-agent-map §A8b): a write carries the generation this process saw; a stale one is refused only
|
|
345
|
+
// under hq.boardFencing 'enforce' (default shadow = write and log). The lease is read ONCE, before the write transaction —
|
|
346
|
+
// with an ssh arbiter a lease read inside the transaction would hold the ledger write lock across the network.
|
|
347
|
+
// Only 'enforce' (opt-in) re-reads once inside the transaction just before writing, so a lease taken over meanwhile
|
|
348
|
+
// still rolls the write back; shadow never touches the lease under the lock.
|
|
349
|
+
const hq = getUserConfig().hq;
|
|
350
|
+
const writerGeneration = hq ? readSeenGeneration(hqSeenGenerationPath(hq)) : null;
|
|
351
|
+
let recordedGeneration: number | null = null;
|
|
352
|
+
let firstMeasured = false;
|
|
353
|
+
if (hq) {
|
|
354
|
+
let lease: ReturnType<typeof readHqLease>['record'] | undefined;
|
|
355
|
+
try { lease = readHqLease({ config: hq }).record; }
|
|
356
|
+
catch (error) { debug.log('hq.fence', 'board-lease-unmeasured', { writer: writerGeneration, version, reason: String(error).slice(0, 200) }); }
|
|
357
|
+
// A missing lease (null) and a broken one (parseLease throws → caught above with its reason) are both «unmeasured».
|
|
358
|
+
if (lease === null) debug.log('hq.fence', 'board-lease-unmeasured', { writer: writerGeneration, version, reason: 'no-lease' });
|
|
359
|
+
if (lease) {
|
|
360
|
+
firstMeasured = true;
|
|
361
|
+
recordedGeneration = writerGeneration;
|
|
362
|
+
if (writerGeneration !== null && writerGeneration < lease.generation) {
|
|
363
|
+
debug.log('hq.fence', 'board-stale-write', { writer: writerGeneration, lease: lease.generation, version });
|
|
364
|
+
if (hq.boardFencing === 'enforce') throw new CliUserError(`낡은 세대 ${writerGeneration} < ${lease.generation} — 쓰기 거부`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
337
368
|
const db = open();
|
|
338
369
|
try {
|
|
339
370
|
let imported = false;
|
|
@@ -352,6 +383,22 @@ export function mutate(version: string, released: string, dev: string, apply: (d
|
|
|
352
383
|
return { version: item.version, ...(item.owner !== null ? { owner: item.owner } : {}), title: item.title };
|
|
353
384
|
});
|
|
354
385
|
})) {
|
|
386
|
+
if (hq?.boardFencing === 'enforce') {
|
|
387
|
+
let late: ReturnType<typeof readHqLease>['record'] = null;
|
|
388
|
+
let lateReason = 'no-lease';
|
|
389
|
+
try { late = readHqLease({ config: hq }).record; }
|
|
390
|
+
catch (error) { late = null; lateReason = String(error).slice(0, 200); }
|
|
391
|
+
if (!late) {
|
|
392
|
+
// Unmeasured right before the write: the write still goes through, but carries no generation it cannot vouch for.
|
|
393
|
+
// (Logged only when the first read had measured — an unmeasured first read was already logged once.)
|
|
394
|
+
if (firstMeasured) debug.log('hq.fence', 'board-lease-unmeasured', { writer: writerGeneration, version, reason: lateReason, phase: 'before-write' });
|
|
395
|
+
recordedGeneration = null;
|
|
396
|
+
}
|
|
397
|
+
if (late && writerGeneration !== null && writerGeneration < late.generation) {
|
|
398
|
+
debug.log('hq.fence', 'board-stale-write', { writer: writerGeneration, lease: late.generation, version });
|
|
399
|
+
throw new CliUserError(`낡은 세대 ${writerGeneration} < ${late.generation} — 쓰기 거부`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
355
402
|
for (const item of data.items) if (before.get(item.id) !== JSON.stringify(item)) {
|
|
356
403
|
const previous = before.get(item.id);
|
|
357
404
|
const evidenceChanged = previous && (JSON.parse(previous) as ChecklistItem).evidence !== item.evidence;
|
|
@@ -365,7 +412,10 @@ export function mutate(version: string, released: string, dev: string, apply: (d
|
|
|
365
412
|
db.query('DELETE FROM evidence WHERE feature_id = ? AND version = ?').run(id, version);
|
|
366
413
|
db.query('DELETE FROM assignments WHERE feature_id = ? AND version = ?').run(id, version);
|
|
367
414
|
}
|
|
368
|
-
for (const entry of data.history.slice(count))
|
|
415
|
+
for (const entry of data.history.slice(count)) {
|
|
416
|
+
if (recordedGeneration !== null) entry.generation = recordedGeneration;
|
|
417
|
+
record(db, version, entry);
|
|
418
|
+
}
|
|
369
419
|
}
|
|
370
420
|
return data;
|
|
371
421
|
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// GATE-LIVE-OBS — 게이트 «조각 상태» 한 장(`<ledger>/release/<v>/gate-logs/cut/shards.json`)의 계약.
|
|
2
|
+
// 쓰는 쪽 = gate-node(TC · GATE-TIMEOUT-HEAL) · 읽는 쪽 = /v1/ops/release/runs ⊕ 외출 알림(UX).
|
|
3
|
+
// 로그가 아니라 발행 원장 파일인 이유: 게이트 사건은 실행 트리의 test 우주로 가지만 원장은 운영 한 곳이다(TC·OP 10-07 19:12).
|
|
4
|
+
// ⛔ 못 읽으면 `null` — 「조각 0」으로 읽지 않는다.
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
8
|
+
|
|
9
|
+
export const SHARD_STATES = ['pending', 'running', 'done', 'retry', 'timeout', 'failed'] as const;
|
|
10
|
+
export type ShardState = typeof SHARD_STATES[number];
|
|
11
|
+
|
|
12
|
+
export interface GateShard {
|
|
13
|
+
id: string;
|
|
14
|
+
state: ShardState;
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
endedAt?: string;
|
|
17
|
+
/** 설치(install) 단계에 쓴 초. */
|
|
18
|
+
installSec?: number;
|
|
19
|
+
rc?: number;
|
|
20
|
+
/** 대기 이유(예: «CPU 부족 31.2/32»). pending 일 때 화면이 그대로 보인다. */
|
|
21
|
+
waitReason?: string;
|
|
22
|
+
/** 계획 분 — 남은 시간 추정의 재료. */
|
|
23
|
+
plannedMin?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GateShardsFile {
|
|
27
|
+
v: 1;
|
|
28
|
+
version: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
shards: GateShard[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function gateShardsPath(version: string, root: string = releaseLedgerRoot()): string {
|
|
34
|
+
return join(root, 'release', version, 'gate-logs', 'cut', 'shards.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const str = (value: unknown, max = 160): string | undefined =>
|
|
38
|
+
typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : undefined;
|
|
39
|
+
const num = (value: unknown): number | undefined => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
40
|
+
|
|
41
|
+
function shardOf(value: unknown): GateShard | null {
|
|
42
|
+
if (!value || typeof value !== 'object') return null;
|
|
43
|
+
const raw = value as Record<string, unknown>;
|
|
44
|
+
const id = str(raw.id, 80);
|
|
45
|
+
const state = SHARD_STATES.find((s) => s === raw.state);
|
|
46
|
+
if (!id || !state) return null;
|
|
47
|
+
const startedAt = str(raw.startedAt, 40);
|
|
48
|
+
const endedAt = str(raw.endedAt, 40);
|
|
49
|
+
const installSec = num(raw.installSec);
|
|
50
|
+
const rc = num(raw.rc);
|
|
51
|
+
const waitReason = str(raw.waitReason);
|
|
52
|
+
const plannedMin = num(raw.plannedMin);
|
|
53
|
+
return {
|
|
54
|
+
id, state,
|
|
55
|
+
...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}),
|
|
56
|
+
...(installSec !== undefined ? { installSec } : {}), ...(rc !== undefined ? { rc } : {}),
|
|
57
|
+
...(waitReason ? { waitReason } : {}), ...(plannedMin !== undefined ? { plannedMin } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 파일이 없거나 깨졌으면 null. 알 수 없는 상태값의 조각은 버리되 파일 전체는 살린다. */
|
|
62
|
+
export function readGateShards(path: string): GateShardsFile | null {
|
|
63
|
+
let parsed: unknown;
|
|
64
|
+
try { parsed = JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
65
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
66
|
+
const raw = parsed as Record<string, unknown>;
|
|
67
|
+
if (raw.v !== 1 || !Array.isArray(raw.shards)) return null;
|
|
68
|
+
const version = str(raw.version, 40);
|
|
69
|
+
const updatedAt = str(raw.updatedAt, 40);
|
|
70
|
+
if (!version || !updatedAt) return null;
|
|
71
|
+
return { v: 1, version, updatedAt, shards: raw.shards.map(shardOf).filter((s): s is GateShard => s !== null) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** tmp → rename — 읽는 쪽이 반쯤 쓴 파일을 보지 않게. */
|
|
75
|
+
export function writeGateShards(path: string, file: GateShardsFile): void {
|
|
76
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
77
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
78
|
+
writeFileSync(tmp, `${JSON.stringify(file)}\n`);
|
|
79
|
+
renameSync(tmp, path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GateShardsSummary {
|
|
83
|
+
total: number;
|
|
84
|
+
counts: Record<ShardState, number>;
|
|
85
|
+
/** 대기 이유별 조각 수(많은 순). */
|
|
86
|
+
waitReasons: Array<{ reason: string; count: number }>;
|
|
87
|
+
/** 남은 분 추정 — 계획 분이 모자라면 null(«추정 불가»). */
|
|
88
|
+
etaMin: number | null;
|
|
89
|
+
/** 도는 조각 중 계획 분을 가장 많이 넘긴 분(없으면 0). 넘겼으면 etaMin 은 «하한»일 뿐이라 화면이 그렇게 말한다
|
|
90
|
+
* — canary 10-07 실측: 계획 0.1분 · 실제 22분 → 넘김을 안 보이면 내내 «남은 약 0분». */
|
|
91
|
+
overrunMin: number;
|
|
92
|
+
/** 파일이 마지막으로 바뀐 뒤 지난 분. 크면 쓰는 쪽이 멎었다는 뜻이다. */
|
|
93
|
+
staleMin: number | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const minutesBetween = (fromIso: string | undefined, now: number): number | null => {
|
|
97
|
+
const at = fromIso ? Date.parse(fromIso) : Number.NaN;
|
|
98
|
+
return Number.isFinite(at) ? Math.max(0, (now - at) / 60_000) : null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 남은 시간 추정: 도는 조각은 «계획 − 경과», 기다리는 조각(pending·retry)은 계획 분을 지금 도는 자리 수로 나눠 뒤에 잇는다.
|
|
103
|
+
* 도는 조각이 없으면 자리 1로 본다. 계획 분이 없는 조각이 하나라도 남아 있으면 null.
|
|
104
|
+
*/
|
|
105
|
+
function estimate(shards: GateShard[], now: number): number | null {
|
|
106
|
+
const running = shards.filter((s) => s.state === 'running');
|
|
107
|
+
const waiting = shards.filter((s) => s.state === 'pending' || s.state === 'retry');
|
|
108
|
+
if (!running.length && !waiting.length) return 0;
|
|
109
|
+
if ([...running, ...waiting].some((s) => s.plannedMin === undefined)) return null;
|
|
110
|
+
const runningLeft = running.map((s) => Math.max(0, s.plannedMin! - (minutesBetween(s.startedAt, now) ?? 0)));
|
|
111
|
+
const slots = Math.max(1, running.length);
|
|
112
|
+
const waitingSum = waiting.reduce((sum, s) => sum + s.plannedMin!, 0);
|
|
113
|
+
return Math.ceil(Math.max(0, ...runningLeft) + waitingSum / slots);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function summarizeShards(file: GateShardsFile, now: number = Date.now()): GateShardsSummary {
|
|
117
|
+
const counts = Object.fromEntries(SHARD_STATES.map((s) => [s, 0])) as Record<ShardState, number>;
|
|
118
|
+
const reasons = new Map<string, number>();
|
|
119
|
+
for (const shard of file.shards) {
|
|
120
|
+
counts[shard.state] += 1;
|
|
121
|
+
if (shard.waitReason && (shard.state === 'pending' || shard.state === 'retry')) {
|
|
122
|
+
reasons.set(shard.waitReason, (reasons.get(shard.waitReason) ?? 0) + 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const stale = minutesBetween(file.updatedAt, now);
|
|
126
|
+
const overrun = Math.max(0, ...file.shards
|
|
127
|
+
.filter((s) => s.state === 'running' && s.plannedMin !== undefined)
|
|
128
|
+
.map((s) => (minutesBetween(s.startedAt, now) ?? 0) - s.plannedMin!));
|
|
129
|
+
return {
|
|
130
|
+
total: file.shards.length,
|
|
131
|
+
counts,
|
|
132
|
+
waitReasons: [...reasons].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count),
|
|
133
|
+
etaMin: estimate(file.shards, now),
|
|
134
|
+
overrunMin: Math.floor(overrun),
|
|
135
|
+
staleMin: stale === null ? null : Math.floor(stale),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const SHARD_WORD: Record<ShardState, string> = {
|
|
140
|
+
pending: '대기', running: '돌기', done: '통과', retry: '재시도', timeout: '잘림', failed: '실패',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** 외출 알림·/release 한 줄 — «조각 24 · 돌기 5 · 대기 8(CPU 부족 8) · 잘림 2 · 통과 9 · 남은 약 40분». */
|
|
144
|
+
export function shardsLine(summary: GateShardsSummary): string {
|
|
145
|
+
const order: ShardState[] = ['running', 'pending', 'retry', 'timeout', 'failed', 'done'];
|
|
146
|
+
const parts = [`조각 ${summary.total}`];
|
|
147
|
+
for (const state of order) {
|
|
148
|
+
const n = summary.counts[state];
|
|
149
|
+
if (!n) continue;
|
|
150
|
+
const why = state === 'pending' && summary.waitReasons.length
|
|
151
|
+
? `(${summary.waitReasons.slice(0, 2).map((w) => `${w.reason} ${w.count}`).join(' · ')})` : '';
|
|
152
|
+
parts.push(`${SHARD_WORD[state]} ${n}${why}`);
|
|
153
|
+
}
|
|
154
|
+
if (summary.overrunMin >= 1) parts.push(`계획보다 ${summary.overrunMin}분 넘게 도는 중 — 남은 시간 추정 불가`);
|
|
155
|
+
else parts.push(summary.etaMin === null ? '남은 시간 추정 불가(계획 분 없음)' : `남은 약 ${summary.etaMin}분`);
|
|
156
|
+
if (summary.staleMin !== null && summary.staleMin >= 15) parts.push(`⚠️ ${summary.staleMin}분째 갱신 없음`);
|
|
157
|
+
return parts.join(' · ');
|
|
158
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/** LIGHT-RC (0.2.19) — a light release-candidate check between cuts: the changed-file self gate over everything that
|
|
2
|
+
* landed since the previous release's cut, plus an always-run test list (release/light-rc-always.txt), on the remote
|
|
3
|
+
* gate host (GATE-REMOTE). Read-only for the release ledger: it reads the previous published version and never writes.
|
|
4
|
+
* It does not touch the landing freeze or launch Pods. */
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { debug } from '../debug/log.js';
|
|
9
|
+
import { runSelfGateCli, type SelfGateCliOptions, type SelfGateCliResult } from '../self-implement/gate-cli.js';
|
|
10
|
+
import { dispatchHeavyCheck, isValidGateHost, type DispatchDeps, type DispatchOptions } from '../self-implement/gate-remote.js';
|
|
11
|
+
import { latestPublishedPreviousVersion } from '../../scripts/release-loop/unattended-release.js';
|
|
12
|
+
import { baseVersion, isReleaseVersion } from '../../scripts/release-loop/release-version.js';
|
|
13
|
+
|
|
14
|
+
export const LIGHT_RC_ALWAYS_FILE = 'release/light-rc-always.txt';
|
|
15
|
+
export const LIGHT_RC_DEFAULT_SHARDS = 4;
|
|
16
|
+
export const LIGHT_RC_NO_PREVIOUS_CUT = '직전 판 컷을 못 찾았다 — --base 로 준다';
|
|
17
|
+
|
|
18
|
+
export interface LightRcOptions {
|
|
19
|
+
base?: string;
|
|
20
|
+
/** Gate host; absent = config gateRemote.host (the dispatch's own default). */
|
|
21
|
+
remote?: string;
|
|
22
|
+
/** Run on this machine (the remote child is re-invoked with it). */
|
|
23
|
+
local?: boolean;
|
|
24
|
+
shards?: number;
|
|
25
|
+
/** Release ledger root for the previous-version lookup (release run's deps.ledgerRoot). */
|
|
26
|
+
ledgerRoot?: string;
|
|
27
|
+
log?: (line: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LightRcResult {
|
|
31
|
+
version: string;
|
|
32
|
+
base: string;
|
|
33
|
+
/** New failures vs. the base; null = the gate produced no measurement (never read as green). */
|
|
34
|
+
introduced: number | null;
|
|
35
|
+
preexisting: number | null;
|
|
36
|
+
/** Failures the baseline could not classify (unknown + precondition-unmet); null = unmeasured. Non-zero is not green. */
|
|
37
|
+
unclassified: number | null;
|
|
38
|
+
alwaysInclude: number;
|
|
39
|
+
alwaysIncludeMissing: string[];
|
|
40
|
+
shards: number;
|
|
41
|
+
/** Host the gate ran on; null = this machine. */
|
|
42
|
+
remote: string | null;
|
|
43
|
+
gateExitCode: number;
|
|
44
|
+
ok: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LightRcDeps {
|
|
48
|
+
cwd?: string;
|
|
49
|
+
git?: (args: string[], cwd: string) => { rc: number | null; stdout: string };
|
|
50
|
+
previousVersion?: (version: string, ledgerRoot?: string) => string;
|
|
51
|
+
readFile?: (path: string) => string | undefined;
|
|
52
|
+
runGate?: (cwd: string, options: SelfGateCliOptions) => SelfGateCliResult;
|
|
53
|
+
dispatch?: (opts: DispatchOptions, deps?: DispatchDeps) => Promise<number>;
|
|
54
|
+
/** Extra dispatch deps (runner · settings) — tests drive the real dispatch with a fake host. */
|
|
55
|
+
dispatchDeps?: Omit<DispatchDeps, 'write'>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Shown when the gate ran remotely but the host name did not come back. */
|
|
59
|
+
export const LIGHT_RC_UNKNOWN_HOST = '(remote · host unknown)';
|
|
60
|
+
|
|
61
|
+
const defaultGit = (args: string[], cwd: string) => {
|
|
62
|
+
const run = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
63
|
+
return { rc: run.error ? null : run.status, stdout: run.stdout ?? '' };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** One repository-relative path per line; `#` starts a comment; blank lines are skipped. */
|
|
67
|
+
export function parseAlwaysIncludeList(text: string): string[] {
|
|
68
|
+
return [...new Set(text.split('\n').map((line) => line.replace(/#.*/, '').trim()).filter(Boolean))];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The previous release's cut: merge-base of origin/main and origin/release/<previous published version>. */
|
|
72
|
+
export function previousCutBase(version: string, deps: LightRcDeps & { cwd: string; ledgerRoot?: string }): string {
|
|
73
|
+
let previous: string;
|
|
74
|
+
try {
|
|
75
|
+
previous = (deps.previousVersion ?? latestPublishedPreviousVersion)(baseVersion(version), deps.ledgerRoot);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`${LIGHT_RC_NO_PREVIOUS_CUT} (${error instanceof Error ? error.message : String(error)})`);
|
|
78
|
+
}
|
|
79
|
+
const mergeBase = (deps.git ?? defaultGit)(['merge-base', 'origin/main', `origin/release/${previous}`], deps.cwd);
|
|
80
|
+
const sha = mergeBase.stdout.trim();
|
|
81
|
+
if (mergeBase.rc !== 0 || !sha) throw new Error(`${LIGHT_RC_NO_PREVIOUS_CUT} (git merge-base origin/main origin/release/${previous} rc=${mergeBase.rc})`);
|
|
82
|
+
return sha;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function repoRoot(deps: LightRcDeps): string {
|
|
86
|
+
if (deps.cwd) return deps.cwd;
|
|
87
|
+
const top = defaultGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
88
|
+
if (top.rc !== 0 || !top.stdout.trim()) throw new Error('release light-rc: not inside a git repository');
|
|
89
|
+
return top.stdout.trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRemoteResult(stdout: string): Partial<LightRcResult> | undefined {
|
|
93
|
+
const line = stdout.split('\n').map((l) => l.trim()).reverse().find((l) => l.startsWith('{'));
|
|
94
|
+
if (!line) return undefined;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(line) as Partial<LightRcResult> & { host?: unknown };
|
|
97
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
98
|
+
} catch { return undefined; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function runLightRc(version: string, opts: LightRcOptions = {}, deps: LightRcDeps = {}): Promise<LightRcResult> {
|
|
102
|
+
if (!isReleaseVersion(version)) throw new Error(`release version must be x.y.z: ${version}`);
|
|
103
|
+
const shards = opts.shards ?? LIGHT_RC_DEFAULT_SHARDS;
|
|
104
|
+
if (!Number.isSafeInteger(shards) || shards < 1) throw new Error(`invalid --shards count: ${opts.shards}`);
|
|
105
|
+
if (opts.remote !== undefined && !isValidGateHost(opts.remote)) throw new Error(`invalid --remote host: ${opts.remote}`);
|
|
106
|
+
const log = opts.log ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
107
|
+
const cwd = repoRoot(deps);
|
|
108
|
+
const base = opts.base ?? previousCutBase(version, { ...deps, cwd, ledgerRoot: opts.ledgerRoot });
|
|
109
|
+
const listPath = join(cwd, LIGHT_RC_ALWAYS_FILE);
|
|
110
|
+
const listText = (deps.readFile ?? ((path: string) => existsSync(path) ? readFileSync(path, 'utf8') : undefined))(listPath);
|
|
111
|
+
if (listText === undefined) throw new Error(`release light-rc: ${LIGHT_RC_ALWAYS_FILE} not found`);
|
|
112
|
+
const alwaysInclude = parseAlwaysIncludeList(listText);
|
|
113
|
+
|
|
114
|
+
let local: SelfGateCliResult | undefined;
|
|
115
|
+
const runLocal = () => {
|
|
116
|
+
local = (deps.runGate ?? runSelfGateCli)(cwd, { base, shards, alwaysInclude });
|
|
117
|
+
for (const line of local.lines) log(line);
|
|
118
|
+
return local.exitCode;
|
|
119
|
+
};
|
|
120
|
+
const captured: Buffer[] = [];
|
|
121
|
+
const rc = await (deps.dispatch ?? dispatchHeavyCheck)({
|
|
122
|
+
tool: 'self-gate', repo: cwd, installPwa: true, refs: [base], json: true,
|
|
123
|
+
// The host re-runs this command with --local (and ELANOUS_GATE_REMOTE_CHILD=1), so it never recurses.
|
|
124
|
+
remoteArgv: (sha) => ['bun', 'bin/elanous.mjs', 'release', 'light-rc', '--version', version, '--base', sha(base), '--shards', String(shards), '--local', '--json'],
|
|
125
|
+
flags: { remote: opts.local ? undefined : opts.remote ?? true, local: !!opts.local },
|
|
126
|
+
runLocal,
|
|
127
|
+
}, {
|
|
128
|
+
...deps.dispatchDeps,
|
|
129
|
+
write: {
|
|
130
|
+
out: (data) => { captured.push(Buffer.from(data)); },
|
|
131
|
+
err: (data) => { process.stderr.write(data); },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let result: LightRcResult;
|
|
136
|
+
if (local) {
|
|
137
|
+
result = {
|
|
138
|
+
version, base,
|
|
139
|
+
introduced: local.baseline?.introduced ?? null,
|
|
140
|
+
preexisting: local.baseline?.preexisting ?? null,
|
|
141
|
+
unclassified: local.baseline ? local.baseline.unknown + local.baseline.preconditionUnmet : null,
|
|
142
|
+
alwaysInclude: alwaysInclude.length,
|
|
143
|
+
alwaysIncludeMissing: [...local.alwaysIncludeMissing],
|
|
144
|
+
shards, remote: null, gateExitCode: local.exitCode, ok: false,
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
const remote = parseRemoteResult(Buffer.concat(captured).toString('utf8'));
|
|
148
|
+
if (!remote) throw new Error(`release light-rc: remote gate returned no result (rc=${rc})`);
|
|
149
|
+
const host = (remote as { host?: unknown }).host;
|
|
150
|
+
result = {
|
|
151
|
+
version, base,
|
|
152
|
+
introduced: typeof remote.introduced === 'number' ? remote.introduced : null,
|
|
153
|
+
preexisting: typeof remote.preexisting === 'number' ? remote.preexisting : null,
|
|
154
|
+
unclassified: typeof remote.unclassified === 'number' ? remote.unclassified : null,
|
|
155
|
+
alwaysInclude: alwaysInclude.length,
|
|
156
|
+
alwaysIncludeMissing: Array.isArray(remote.alwaysIncludeMissing) ? remote.alwaysIncludeMissing.map(String) : [],
|
|
157
|
+
shards, remote: typeof host === 'string' && host ? host : opts.remote ?? LIGHT_RC_UNKNOWN_HOST,
|
|
158
|
+
gateExitCode: typeof remote.gateExitCode === 'number' ? remote.gateExitCode : rc, ok: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Unmeasured (null) or unclassified failures are never green; policy-gate findings in the gate output are reported
|
|
162
|
+
// (gate rc), not a regression.
|
|
163
|
+
result.ok = result.introduced === 0 && result.unclassified === 0;
|
|
164
|
+
debug.log('release.light-rc', 'done', { version, base, introduced: result.introduced, preexisting: result.preexisting, unclassified: result.unclassified, shards, remote: result.remote });
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function formatLightRc(result: LightRcResult): string[] {
|
|
169
|
+
return [
|
|
170
|
+
`${result.ok ? '✅' : '⛔'} light-rc ${result.version} · base ${result.base.slice(0, 12)} · ${result.remote ? `host ${result.remote}` : 'local'} · shards ${result.shards}`,
|
|
171
|
+
` introduced ${result.introduced ?? '못 쟀다'} · preexisting ${result.preexisting ?? '못 쟀다'} · unclassified ${result.unclassified ?? '못 쟀다'} · always-include ${result.alwaysInclude}${result.alwaysIncludeMissing.length ? ` (missing: ${result.alwaysIncludeMissing.join(', ')})` : ''} · gate rc ${result.gateExitCode}`,
|
|
172
|
+
];
|
|
173
|
+
}
|