elanous 0.2.8 → 0.2.10
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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/ops/seats/index.html +1 -0
- package/apps/pwa/out/ops/seats/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/today/index.html +1 -0
- package/apps/pwa/out/today/index.txt +33 -0
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +3 -0
- package/package.json +1 -1
- package/plugins/card-followup/graphs/run-step.ts +48 -13
- package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
- package/plugins/doc-draft/graphs/run-step.ts +14 -7
- package/scripts/ci-pwa-build-gate.ts +160 -0
- package/scripts/coord-channel-watch.sh +42 -8
- package/scripts/coord-digest.ts +199 -0
- package/scripts/coord-post.sh +60 -3
- package/scripts/install.ps1 +68 -9
- package/scripts/install.sh +39 -12
- package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
- package/scripts/lib/tui-regress-checks.ts +146 -0
- package/scripts/memory-lifecycle-cycle.ts +2 -2
- package/scripts/ops-now-probe.ts +327 -0
- package/scripts/publish-internal-dist.ts +11 -1
- package/scripts/release-loop/cutoff.ts +31 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +18 -4
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/ops-upgrade-node.ts +72 -16
- package/scripts/release-loop/release-readiness.ts +9 -0
- package/scripts/release-loop/version-node.ts +58 -10
- package/scripts/repo-watch-cycle.ts +1 -1
- package/scripts/retro-cycle.ts +2 -2
- package/scripts/schedule-health-report.ts +1 -1
- package/scripts/storyboard/storyboard.ts +5 -161
- package/scripts/test-diet/lib.ts +31 -1
- package/scripts/test-diet/node.ts +134 -17
- package/scripts/test-timeout-audit.ts +114 -0
- package/scripts/tree-sync-alert.ts +1 -1
- package/scripts/tui-regress.ts +71 -0
- package/scripts/waitlist-daily-digest.ts +1 -1
- package/skills/explainer-video/SKILL.md +4 -0
- package/skills/explainer-video/engine/reel.mjs +18 -13
- package/skills/explainer-video/engine/reel.sh +24 -5
- package/skills/explainer-video/engine/stitch.mjs +145 -0
- package/skills/pitch-deck/SKILL.md +36 -0
- package/skills/pitch-deck/template.html +80 -0
- package/src/acp/codex-approval-adapter.ts +9 -3
- package/src/acp/core-turn-bridge.ts +117 -18
- package/src/acp/fact-question.ts +11 -0
- package/src/acp/server.ts +48 -7
- package/src/acp/tool-approval.ts +126 -0
- package/src/acp/turn-runner.ts +36 -0
- package/src/agent/monad-agent-turn.ts +8 -0
- package/src/ask-user-question/pending-questions.ts +25 -10
- package/src/ask-user-question/types.ts +9 -0
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +164 -0
- package/src/autopilot/build/integrity-gate.ts +12 -0
- package/src/boot/daemon-prompt-turn.ts +3 -0
- package/src/boot/daemon-tools/index.ts +8 -0
- package/src/boot/daemon-tools/types.ts +2 -0
- package/src/boot/daemon-tools/web-search.ts +1 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +61 -9
- package/src/chat/pickers/state.ts +6 -2
- package/src/cli/agent-cli.ts +7 -0
- package/src/cli/decisions-cli.ts +25 -3
- package/src/cli/doctor-bundle-upload.ts +68 -0
- package/src/cli/doctor-cli.ts +38 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/ops-cli.ts +20 -0
- package/src/cli/pod-cli.ts +49 -0
- package/src/cli/pr-cli.ts +37 -4
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +98 -15
- package/src/cli/self-update.ts +14 -0
- package/src/cli/storyboard-cli.ts +37 -0
- package/src/cli/test-state-dir-flag.ts +11 -4
- package/src/connectors/linear.ts +146 -0
- package/src/context-bus/coord-events.ts +115 -0
- package/src/dashboard/dashboard-boot-fixture.ts +23 -0
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +128 -34
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +50 -2
- package/src/decisions/decision-ledger.ts +75 -4
- package/src/decisions/decision-linear-projection.ts +157 -0
- package/src/decisions/web-push-decision-cards.ts +50 -0
- package/src/directive/interpretation-card.ts +36 -0
- package/src/discord.ts +2 -1
- package/src/domains/coo-admin-tool.ts +62 -0
- package/src/domains/core-tools.ts +11 -3
- package/src/domains/elanous-card.ts +29 -0
- package/src/domains/ops-facts-tool.ts +76 -0
- package/src/domains/outbound-alert.ts +41 -13
- package/src/domains/release-tool.ts +168 -0
- package/src/domains/telegram-kind-route.ts +19 -1
- package/src/exec-requests/planner.ts +98 -28
- package/src/exec-requests/runner.ts +107 -10
- package/src/exec-requests/store.ts +8 -2
- package/src/field/field-media.ts +10 -7
- package/src/field/field-reel.ts +14 -4
- package/src/git-fs/worktree.ts +45 -6
- package/src/graph-runner/graph-approval-card.ts +113 -0
- package/src/graph-runner/graph-cli.ts +129 -4
- package/src/graph-runner/graph-notify.ts +25 -3
- package/src/graph-runner/runner.ts +142 -16
- package/src/harness/browser-verify.ts +25 -2
- package/src/harness/harness-cli-command.ts +231 -35
- package/src/harness/harness-incidents-writer.fixture.ts +24 -0
- package/src/harness/harness-incidents.ts +129 -0
- package/src/harness/harness-stop.ts +53 -3
- package/src/hitl/clarification-policy.ts +3 -1
- package/src/index.ts +172 -41
- package/src/intake-plane/author-goals.ts +37 -4
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/discord-seat-work.ts +47 -2
- package/src/intake-plane/seat-doc-route.ts +170 -6
- package/src/intake-plane/telegram-seat-work.ts +44 -2
- package/src/intake-plane/telegram-work.ts +1 -0
- package/src/maturity/cli-maturity.ts +34 -0
- package/src/maturity/feature-maturity.ts +100 -1
- package/src/maturity/tui-slash-maturity.ts +31 -0
- package/src/memory.ts +32 -4
- package/src/nexus/api/exec-requests.ts +33 -3
- package/src/nexus/api/http-server.ts +73 -1
- package/src/nexus/api/meta-api.ts +38 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +148 -0
- package/src/nexus/api/ops-seats-sources.ts +95 -0
- package/src/nexus/api/ops-seats.ts +124 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/seat-requests.ts +88 -8
- package/src/nexus/api/sessions-store.ts +63 -0
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +45 -4
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/first-run.ts +89 -0
- package/src/onboarding/lan-address.ts +45 -0
- package/src/onboarding/web-first.ts +119 -24
- package/src/onboarding.ts +4 -0
- package/src/ops-now/ops-now-refresh.ts +100 -0
- package/src/project/project-store.ts +115 -0
- package/src/release-loop/checklist.ts +76 -61
- package/src/release-loop/feature-store.ts +460 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +98 -0
- package/src/seat-dispatch/ceo-intent.ts +38 -0
- package/src/seat-loop/seat-loop.ts +231 -0
- package/src/seat-loop/seat-report.ts +64 -0
- package/src/self-dev/decompose.ts +66 -11
- package/src/self-dev/run-store.ts +10 -1
- package/src/self-implement/execution-clarification.ts +115 -0
- package/src/self-implement/gate-cli.ts +6 -3
- package/src/self-implement/orchestrator.ts +37 -3
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/run-status-mapping.ts +5 -3
- package/src/session/chat.ts +2 -0
- package/src/session/index.ts +9 -0
- package/src/shell-env-bootstrap.ts +17 -0
- package/src/skills/tools/usage-slash.ts +8 -7
- package/src/steward/directive.ts +51 -2
- package/src/steward/launch.ts +250 -0
- package/src/steward/outcome-notify.ts +115 -0
- package/src/steward/steward-cards.ts +8 -2
- package/src/steward/triage-plan.ts +32 -4
- package/src/steward/triage.ts +93 -9
- package/src/steward/working-backwards.ts +64 -0
- package/src/storyboard/storyboard.ts +141 -0
- package/src/task-cards/card-store.ts +2 -2
- package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
- package/src/telegram-card-followup.ts +233 -0
- package/src/telegram-commands.ts +16 -0
- package/src/telegram-report.ts +15 -9
- package/src/telegram-run.ts +90 -13
- package/src/telegram.ts +29 -8
- package/src/thinking-line.ts +28 -20
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +118 -11
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { Database } from 'bun:sqlite';
|
|
5
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
6
|
+
import { CliUserError } from '../cli/cli-user-error.js';
|
|
7
|
+
import { debug } from '../debug/log.js';
|
|
8
|
+
import { devVersion, type Checklist, type ChecklistHistory, type ChecklistItem } from './checklist.js';
|
|
9
|
+
|
|
10
|
+
export function validateVersion(version: string): void {
|
|
11
|
+
if (!/^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(version)) throw new CliUserError(`체크리스트 판이 아니다: ${version}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function jsonPath(version: string): string { validateVersion(version); return join(releaseLedgerRoot(), 'release', version, 'checklist.json'); }
|
|
15
|
+
function legacyVersions(): string[] {
|
|
16
|
+
const root = join(releaseLedgerRoot(), 'release');
|
|
17
|
+
if (!existsSync(root)) return [];
|
|
18
|
+
return readdirSync(root).filter((version) => /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(version) && existsSync(jsonPath(version)));
|
|
19
|
+
}
|
|
20
|
+
export function releasedVersion(ledgerRoot = releaseLedgerRoot()): string {
|
|
21
|
+
const root = join(ledgerRoot, 'release');
|
|
22
|
+
if (!existsSync(root)) return '';
|
|
23
|
+
return readdirSync(root).filter((version) => /^\d+\.\d+\.\d+$/.test(version) && existsSync(join(root, version, 'release.json')))
|
|
24
|
+
.filter((version) => {
|
|
25
|
+
try { const record = JSON.parse(readFileSync(join(root, version, 'release.json'), 'utf8')) as { version?: string; publishedAt?: string }; return record.version === version && typeof record.publishedAt === 'string'; }
|
|
26
|
+
catch { return false; }
|
|
27
|
+
}).sort((a, b) => {
|
|
28
|
+
const aa = a.split('.').map(Number), bb = b.split('.').map(Number);
|
|
29
|
+
return (bb[0]! - aa[0]!) || (bb[1]! - aa[1]!) || (bb[2]! - aa[2]!);
|
|
30
|
+
})[0] ?? '';
|
|
31
|
+
}
|
|
32
|
+
const walWait = new Int32Array(new SharedArrayBuffer(4));
|
|
33
|
+
function open(root = releaseLedgerRoot()): Database {
|
|
34
|
+
const path = join(root, 'release', 'features.sqlite');
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
const db = new Database(path, { create: true, strict: true });
|
|
37
|
+
try {
|
|
38
|
+
chmodSync(path, 0o600);
|
|
39
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
40
|
+
if ((db.query('PRAGMA journal_mode').get() as { journal_mode: string }).journal_mode !== 'wal') {
|
|
41
|
+
// First open may race another process initializing the same database; journal_mode needs an exclusive lock.
|
|
42
|
+
for (let attempt = 0; ; attempt++) {
|
|
43
|
+
try { db.exec('PRAGMA journal_mode = WAL'); break; }
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!(error instanceof Error) || !/database is locked/.test(error.message) || attempt >= 500) throw error;
|
|
46
|
+
Atomics.wait(walWait, 0, 0, 20);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
51
|
+
if (!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'imported_versions'").get()) db.exec(`CREATE TABLE IF NOT EXISTS features (id TEXT PRIMARY KEY, title TEXT NOT NULL, owner TEXT, kind TEXT, created_at TEXT NOT NULL);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS assignments (feature_id TEXT NOT NULL REFERENCES features(id), version TEXT NOT NULL, status TEXT NOT NULL, disposition TEXT, evidence TEXT, title_override TEXT, owner TEXT, kind TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL, PRIMARY KEY(feature_id, version));
|
|
53
|
+
CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, by TEXT NOT NULL, feature_id TEXT NOT NULL, version TEXT NOT NULL, field TEXT NOT NULL, "from" TEXT, "to" TEXT, released TEXT NOT NULL, dev TEXT NOT NULL);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS evidence (feature_id TEXT NOT NULL, version TEXT NOT NULL, ref TEXT NOT NULL, at TEXT NOT NULL, by TEXT NOT NULL);
|
|
55
|
+
CREATE TABLE IF NOT EXISTS imported_versions (version TEXT PRIMARY KEY, json_hash TEXT, imported_at TEXT);`);
|
|
56
|
+
const columns = db.query('PRAGMA table_info(imported_versions)').all() as Array<{ name: string }>;
|
|
57
|
+
if (!columns.some((column) => column.name === 'json_hash')) db.exec('ALTER TABLE imported_versions ADD COLUMN json_hash TEXT');
|
|
58
|
+
if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
|
|
59
|
+
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)');
|
|
60
|
+
return db;
|
|
61
|
+
} catch (error) { db.close(); throw error; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; updatedAt: string; updatedBy: string }
|
|
65
|
+
type StoredSchedule = { version: string; cut_at: string; land_by: string | null; updated_at: string; updated_by: string };
|
|
66
|
+
function scheduleRow(row: StoredSchedule): ScheduleRow {
|
|
67
|
+
return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, updatedAt: row.updated_at, updatedBy: row.updated_by };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readSchedule(version: string, root?: string): ScheduleRow | null {
|
|
71
|
+
validateVersion(version);
|
|
72
|
+
if (!existsSync(join(root ?? releaseLedgerRoot(), 'release', 'features.sqlite'))) return null;
|
|
73
|
+
const db = open(root);
|
|
74
|
+
try {
|
|
75
|
+
const row = db.query('SELECT * FROM release_schedules WHERE version = ?').get(version) as StoredSchedule | null;
|
|
76
|
+
return row ? scheduleRow(row) : null;
|
|
77
|
+
} finally { db.close(); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readSchedules(root?: string): ScheduleRow[] {
|
|
81
|
+
const db = open(root);
|
|
82
|
+
try { return (db.query('SELECT * FROM release_schedules ORDER BY version').all() as StoredSchedule[]).map(scheduleRow); }
|
|
83
|
+
finally { db.close(); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string }, by: string, root?: string): ScheduleRow {
|
|
87
|
+
validateVersion(version);
|
|
88
|
+
const db = open(root);
|
|
89
|
+
try {
|
|
90
|
+
return transaction(db, () => {
|
|
91
|
+
const previous = db.query('SELECT * FROM release_schedules WHERE version = ?').get(version) as StoredSchedule | null;
|
|
92
|
+
const cutAt = patch.cutAt ?? previous?.cut_at;
|
|
93
|
+
if (!cutAt) throw new CliUserError('새 판에는 --cut-at 이 필요하다');
|
|
94
|
+
const landBy = patch.landBy ?? previous?.land_by ?? null;
|
|
95
|
+
if (previous && previous.cut_at === cutAt && previous.land_by === landBy) return scheduleRow(previous);
|
|
96
|
+
const at = new Date().toISOString();
|
|
97
|
+
db.query(`INSERT INTO release_schedules (version, cut_at, land_by, updated_at, updated_by) VALUES (?, ?, ?, ?, ?)
|
|
98
|
+
ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
99
|
+
.run(version, cutAt, landBy, at, by);
|
|
100
|
+
for (const [field, from, to] of [
|
|
101
|
+
['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
|
|
102
|
+
] as const) {
|
|
103
|
+
if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
|
|
104
|
+
}
|
|
105
|
+
return { version, cutAt, landBy, updatedAt: at, updatedBy: by };
|
|
106
|
+
});
|
|
107
|
+
} finally { db.close(); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function transaction<T>(db: Database, work: () => T): T {
|
|
111
|
+
db.exec('BEGIN IMMEDIATE');
|
|
112
|
+
try { const result = work(); db.exec('COMMIT'); return result; }
|
|
113
|
+
catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
114
|
+
}
|
|
115
|
+
/** Read-only counterpart: one deferred transaction so a snapshot never mixes rows from before and after a concurrent write. */
|
|
116
|
+
function readTransaction<T>(db: Database, work: () => T): T {
|
|
117
|
+
db.exec('BEGIN DEFERRED');
|
|
118
|
+
try { const result = work(); db.exec('COMMIT'); return result; }
|
|
119
|
+
catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string };
|
|
123
|
+
function decode(value: string | null): unknown { return value === null ? null : JSON.parse(value); }
|
|
124
|
+
function historyRow(row: EventRow): ChecklistHistory {
|
|
125
|
+
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 };
|
|
126
|
+
}
|
|
127
|
+
function record(db: Database, version: string, entry: ChecklistHistory): void {
|
|
128
|
+
db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
129
|
+
.run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev);
|
|
130
|
+
}
|
|
131
|
+
function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
132
|
+
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);
|
|
133
|
+
}
|
|
134
|
+
function items(db: Database, version: string): ChecklistItem[] {
|
|
135
|
+
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
136
|
+
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | null>>;
|
|
137
|
+
return rows.map((r) => {
|
|
138
|
+
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id!)].filter((value) => value !== null).join('\n');
|
|
139
|
+
return { id: r.id!, title: r.title!, status: r.status as ChecklistItem['status'],
|
|
140
|
+
...(r.owner !== null ? { owner: r.owner! } : {}), ...(r.kind !== null ? { kind: r.kind as ChecklistItem['kind'] } : {}),
|
|
141
|
+
...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
|
|
142
|
+
updatedAt: r.updated_at!, updatedBy: r.updated_by! };
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function snapshot(db: Database, version: string, released: string, dev: string): Checklist {
|
|
146
|
+
const rows = db.query('SELECT * FROM events WHERE version = ? ORDER BY seq').all(version) as EventRow[];
|
|
147
|
+
return { version, released, dev, items: items(db, version), history: rows.map(historyRow) };
|
|
148
|
+
}
|
|
149
|
+
function putItem(db: Database, version: string, item: ChecklistItem): void {
|
|
150
|
+
const feature = db.query('SELECT title FROM features WHERE id = ?').get(item.id) as { title: string } | null;
|
|
151
|
+
if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
|
|
152
|
+
// features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
|
|
153
|
+
else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
|
|
154
|
+
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
155
|
+
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
156
|
+
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.updatedAt, item.updatedBy);
|
|
157
|
+
}
|
|
158
|
+
function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* When did the ledger take this item away from `version`? A `move` out of it, a `remove` at it, or (carry-forward
|
|
162
|
+
* without a move event) the item now living in another version. null = never left — a JSON-only item is new.
|
|
163
|
+
* OP 10-02: without this, an old build's checklist.json re-added items the ledger had moved or removed (duplicates).
|
|
164
|
+
*/
|
|
165
|
+
function ledgerLeftAt(db: Database, id: string, version: string): string | null {
|
|
166
|
+
const event = db.query(`SELECT MAX(at) AS at FROM events WHERE feature_id = ? AND
|
|
167
|
+
((field = 'move' AND "from" = ?) OR (field = 'remove' AND version = ?))`).get(id, JSON.stringify(version), version) as { at: string | null };
|
|
168
|
+
const elsewhere = db.query('SELECT MAX(updated_at) AS at FROM assignments WHERE feature_id = ? AND version <> ?').get(id, version) as { at: string | null };
|
|
169
|
+
const times = [event.at, elsewhere.at].filter((t): t is string => typeof t === 'string');
|
|
170
|
+
return times.length ? times.sort().at(-1)! : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function importOnDb(db: Database, version: string): boolean {
|
|
174
|
+
const previous = db.query('SELECT json_hash FROM imported_versions WHERE version = ?').get(version) as { json_hash: string | null } | null;
|
|
175
|
+
const path = jsonPath(version);
|
|
176
|
+
if (!existsSync(path)) {
|
|
177
|
+
if (!previous) db.query('INSERT INTO imported_versions (version) VALUES (?)').run(version);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const contents = readFileSync(path);
|
|
181
|
+
const hash = jsonHash(contents);
|
|
182
|
+
if (previous && previous.json_hash === hash) return false;
|
|
183
|
+
const data = JSON.parse(contents.toString('utf8')) as Checklist;
|
|
184
|
+
if (data.version !== version) throw new CliUserError(`체크리스트 판 불일치: ${path}`);
|
|
185
|
+
if (!Array.isArray(data.items) || !Array.isArray(data.history)) throw new CliUserError(`잘못된 체크리스트: ${path}`);
|
|
186
|
+
const duplicate = new Set<string>();
|
|
187
|
+
for (const item of data.items) {
|
|
188
|
+
if (!item.id?.trim() || !item.title?.trim() || duplicate.has(item.id) || !['green', 'yellow', 'red', 'done'].includes(item.status)) throw new CliUserError(`잘못된 체크리스트 칸: ${version} ${item.id}`);
|
|
189
|
+
duplicate.add(item.id);
|
|
190
|
+
}
|
|
191
|
+
if (!previous) {
|
|
192
|
+
for (const item of data.items) {
|
|
193
|
+
const existing = db.query('SELECT title FROM features WHERE id = ?').get(item.id) as { title: string } | null;
|
|
194
|
+
// Keep a legacy per-version title until an explicit retitle unifies the feature identity.
|
|
195
|
+
if (!existing) putItem(db, version, item);
|
|
196
|
+
else {
|
|
197
|
+
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
198
|
+
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.updatedAt, item.updatedBy);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const entry of data.history) record(db, version, entry);
|
|
202
|
+
db.query('INSERT INTO imported_versions (version, json_hash, imported_at) VALUES (?, ?, ?)').run(version, hash, new Date().toISOString());
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const ledger = new Map(items(db, version).map((item) => [item.id, item]));
|
|
207
|
+
const changed: string[] = [];
|
|
208
|
+
let added = 0;
|
|
209
|
+
const ledgerWonIds: string[] = [];
|
|
210
|
+
const leftIds: string[] = [];
|
|
211
|
+
for (const item of data.items) {
|
|
212
|
+
const current = ledger.get(item.id);
|
|
213
|
+
if (!current) {
|
|
214
|
+
// Moved or removed in the ledger: only a JSON edit made after that may bring it back.
|
|
215
|
+
const leftAt = ledgerLeftAt(db, item.id, version);
|
|
216
|
+
if (leftAt && !(Date.parse(item.updatedAt) > Date.parse(leftAt))) { leftIds.push(item.id); continue; }
|
|
217
|
+
const feature = db.query('SELECT title FROM features WHERE id = ?').get(item.id) as { title: string } | null;
|
|
218
|
+
putItem(db, version, item);
|
|
219
|
+
if (feature && feature.title !== item.title) db.query('UPDATE assignments SET title_override = ? WHERE feature_id = ? AND version = ?').run(item.title, item.id, version);
|
|
220
|
+
added++;
|
|
221
|
+
changed.push(item.id);
|
|
222
|
+
record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind'] as const;
|
|
226
|
+
const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
|
|
227
|
+
const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
|
|
228
|
+
const differs = fields.some((field) => field === 'evidence'
|
|
229
|
+
? (!preserveRefs && current.evidence !== item.evidence)
|
|
230
|
+
: current[field] !== item[field]);
|
|
231
|
+
if (!differs) continue;
|
|
232
|
+
if (!(Date.parse(item.updatedAt) > Date.parse(current.updatedAt))) {
|
|
233
|
+
ledgerWonIds.push(item.id);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const incoming: ChecklistItem = { ...current, status: item.status, updatedAt: item.updatedAt, updatedBy: item.updatedBy };
|
|
237
|
+
for (const field of fields) {
|
|
238
|
+
if (field === 'status') continue;
|
|
239
|
+
if (field === 'evidence' && preserveRefs) continue;
|
|
240
|
+
if (item[field] === undefined) delete (incoming as unknown as Record<string, unknown>)[field];
|
|
241
|
+
else (incoming as unknown as Record<string, unknown>)[field] = item[field];
|
|
242
|
+
}
|
|
243
|
+
if (!preserveRefs) db.query('DELETE FROM evidence WHERE feature_id = ? AND version = ?').run(item.id, version);
|
|
244
|
+
const persisted = preserveRefs ? { ...incoming, evidence: baseEvidence } : incoming;
|
|
245
|
+
putItem(db, version, persisted);
|
|
246
|
+
if (incoming.title !== current.title) {
|
|
247
|
+
db.query('UPDATE assignments SET title_override = ? WHERE feature_id = ? AND version = ?')
|
|
248
|
+
.run(incoming.title === (db.query('SELECT title FROM features WHERE id = ?').get(item.id) as { title: string }).title ? null : incoming.title, item.id, version);
|
|
249
|
+
}
|
|
250
|
+
changed.push(item.id);
|
|
251
|
+
record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: current, to: incoming, released: data.released, dev: data.dev });
|
|
252
|
+
}
|
|
253
|
+
db.query('UPDATE imported_versions SET json_hash = ?, imported_at = ? WHERE version = ?').run(hash, new Date().toISOString(), version);
|
|
254
|
+
const ledgerWon = ledgerWonIds.length;
|
|
255
|
+
debug.log('release.features', 'reimported', { version, changed: changed.length - added, added, ledgerWon, notReadded: leftIds.length });
|
|
256
|
+
// Two separate lines: what was taken from the old build's JSON, and where the JSON differs but the ledger is newer
|
|
257
|
+
// (a conflict a person should know about even though nothing changed in the ledger).
|
|
258
|
+
if (changed.length) console.error(`⚠ checklist ${version}: 옛 판이 checklist.json 에 쓴 ${changed.length}칸을 다시 들였다(${changed.slice(0, 5).join(', ')})`);
|
|
259
|
+
if (leftIds.length) console.error(`⚠ checklist ${version}: 원장에서 옮기거나 지운 칸 ${leftIds.length}은 옛 판 checklist.json 에 남아 있어도 다시 넣지 않았다(${leftIds.slice(0, 5).join(', ')})`);
|
|
260
|
+
if (ledgerWon) console.error(`⚠ checklist ${version}: 옛 판 checklist.json 과 원장이 다르다 — 원장이 더 새로워 원장 값 유지 ${ledgerWon}칸(${ledgerWonIds.slice(0, 5).join(', ')})`);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
export function importJson(version: string): boolean {
|
|
264
|
+
validateVersion(version);
|
|
265
|
+
if (!existsSync(jsonPath(version))) return false;
|
|
266
|
+
const db = open();
|
|
267
|
+
try {
|
|
268
|
+
const imported = transaction(db, () => importOnDb(db, version));
|
|
269
|
+
if (imported) debug.log('release.features', 'imported', { version });
|
|
270
|
+
return imported;
|
|
271
|
+
} finally { db.close(); }
|
|
272
|
+
}
|
|
273
|
+
function importLegacyAll(): void {
|
|
274
|
+
const db = open();
|
|
275
|
+
try {
|
|
276
|
+
const imported = transaction(db, () => legacyVersions().filter((version) => importOnDb(db, version)));
|
|
277
|
+
for (const version of imported) debug.log('release.features', 'imported', { version });
|
|
278
|
+
} finally { db.close(); }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function list(version: string, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
282
|
+
validateVersion(version);
|
|
283
|
+
importJson(version);
|
|
284
|
+
const db = open();
|
|
285
|
+
try { return readTransaction(db, () => snapshot(db, version, released, dev)); } finally { db.close(); }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The checklist API supplies its existing validation, field ordering and legacy history shape. */
|
|
289
|
+
export function mutate(version: string, released: string, dev: string, apply: (data: Checklist) => boolean): Checklist {
|
|
290
|
+
validateVersion(version);
|
|
291
|
+
const db = open();
|
|
292
|
+
try {
|
|
293
|
+
let imported = false;
|
|
294
|
+
const result = transaction(db, () => {
|
|
295
|
+
imported = importOnDb(db, version);
|
|
296
|
+
const data = snapshot(db, version, released, dev);
|
|
297
|
+
const before = new Map(data.items.map((item) => [item.id, JSON.stringify(item)]));
|
|
298
|
+
const count = data.history.length;
|
|
299
|
+
if (apply(data)) {
|
|
300
|
+
for (const item of data.items) if (before.get(item.id) !== JSON.stringify(item)) {
|
|
301
|
+
const previous = before.get(item.id);
|
|
302
|
+
const evidenceChanged = previous && (JSON.parse(previous) as ChecklistItem).evidence !== item.evidence;
|
|
303
|
+
if (evidenceChanged) db.query('DELETE FROM evidence WHERE feature_id = ? AND version = ?').run(item.id, version);
|
|
304
|
+
const persisted = previous && !evidenceChanged
|
|
305
|
+
? { ...item, evidence: (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined }
|
|
306
|
+
: item;
|
|
307
|
+
putItem(db, version, persisted);
|
|
308
|
+
}
|
|
309
|
+
for (const id of before.keys()) if (!data.items.some((item) => item.id === id)) {
|
|
310
|
+
db.query('DELETE FROM evidence WHERE feature_id = ? AND version = ?').run(id, version);
|
|
311
|
+
db.query('DELETE FROM assignments WHERE feature_id = ? AND version = ?').run(id, version);
|
|
312
|
+
}
|
|
313
|
+
for (const entry of data.history.slice(count)) record(db, version, entry);
|
|
314
|
+
}
|
|
315
|
+
return data;
|
|
316
|
+
});
|
|
317
|
+
if (imported) debug.log('release.features', 'imported', { version });
|
|
318
|
+
return result;
|
|
319
|
+
} finally { db.close(); }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function add(version: string, item: ChecklistItem, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
323
|
+
if (!item.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
324
|
+
if (!item.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
325
|
+
return mutate(version, released, dev, (data) => {
|
|
326
|
+
if (data.items.some((i) => i.id === item.id)) throw new CliUserError(`이미 있는 칸: ${item.id}`, 'set <id> 로 고친다');
|
|
327
|
+
data.items.push(item);
|
|
328
|
+
data.history.push({ at: item.updatedAt, by: item.updatedBy, id: item.id, field: 'add', from: null, to: item, released, dev });
|
|
329
|
+
return true;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
export function set(version: string, id: string, patch: Partial<ChecklistItem>, by: string, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
333
|
+
return mutate(version, released, dev, (data) => {
|
|
334
|
+
const item = data.items.find((i) => i.id === id);
|
|
335
|
+
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
336
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
|
|
337
|
+
if (!fields.length) return false;
|
|
338
|
+
const at = new Date().toISOString();
|
|
339
|
+
for (const field of fields) {
|
|
340
|
+
const from = item[field]; const to = patch[field]!;
|
|
341
|
+
(item as unknown as Record<string, unknown>)[field] = to;
|
|
342
|
+
item.updatedAt = at; item.updatedBy = by;
|
|
343
|
+
data.history.push({ at, by, id, field, from: from ?? null, to, released, dev });
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
export function remove(version: string, id: string, by: string, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
349
|
+
return mutate(version, released, dev, (data) => {
|
|
350
|
+
const index = data.items.findIndex((i) => i.id === id);
|
|
351
|
+
if (index < 0) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
352
|
+
const [item] = data.items.splice(index, 1);
|
|
353
|
+
data.history.push({ at: new Date().toISOString(), by, id, field: 'remove', from: item, to: null, released, dev });
|
|
354
|
+
return true;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
export function move(id: string, from: string, to: string, by: string, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
358
|
+
validateVersion(from); validateVersion(to);
|
|
359
|
+
if (from === to) throw new CliUserError('같은 판으로 옮길 수 없다');
|
|
360
|
+
const db = open();
|
|
361
|
+
try {
|
|
362
|
+
let importedFrom = false; let importedTo = false;
|
|
363
|
+
const result = transaction(db, () => {
|
|
364
|
+
importedFrom = importOnDb(db, from); importedTo = importOnDb(db, to);
|
|
365
|
+
const item = items(db, from).find((i) => i.id === id);
|
|
366
|
+
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
367
|
+
if (db.query('SELECT 1 FROM assignments WHERE feature_id = ? AND version = ?').get(id, to)) throw new CliUserError(`이미 있는 칸: ${id}`);
|
|
368
|
+
const at = new Date().toISOString();
|
|
369
|
+
db.query('UPDATE assignments SET version = ?, updated_at = ?, updated_by = ? WHERE feature_id = ? AND version = ?').run(to, at, by, id, from);
|
|
370
|
+
db.query('UPDATE evidence SET version = ? WHERE feature_id = ? AND version = ?').run(to, id, from);
|
|
371
|
+
record(db, to, { at, by, id, field: 'move', from, to, released, dev });
|
|
372
|
+
return snapshot(db, to, released, dev);
|
|
373
|
+
});
|
|
374
|
+
if (importedFrom) debug.log('release.features', 'imported', { version: from });
|
|
375
|
+
if (importedTo) debug.log('release.features', 'imported', { version: to });
|
|
376
|
+
debug.log('release.features', 'moved', { id, from, to, by });
|
|
377
|
+
return result;
|
|
378
|
+
} finally { db.close(); }
|
|
379
|
+
}
|
|
380
|
+
export function retitle(id: string, title: string, by: string, released = releasedVersion(), dev = devVersion()): void {
|
|
381
|
+
if (!title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
382
|
+
importLegacyAll();
|
|
383
|
+
const db = open();
|
|
384
|
+
try {
|
|
385
|
+
const changed = transaction(db, () => {
|
|
386
|
+
const old = db.query('SELECT title FROM features WHERE id = ?').get(id) as { title: string } | null;
|
|
387
|
+
if (!old) throw new CliUserError(`없는 칸: ${id}`);
|
|
388
|
+
const versions = db.query('SELECT version, COALESCE(title_override, ?) AS title FROM assignments WHERE feature_id = ? ORDER BY version')
|
|
389
|
+
.all(old.title, id) as Array<{ version: string; title: string }>;
|
|
390
|
+
if (!versions.length) throw new CliUserError(`없는 칸: ${id}`);
|
|
391
|
+
const changed = versions.filter((row) => row.title !== title);
|
|
392
|
+
if (!changed.length && old.title === title) return false;
|
|
393
|
+
const at = new Date().toISOString();
|
|
394
|
+
db.query('UPDATE features SET title = ? WHERE id = ?').run(title, id);
|
|
395
|
+
db.query('UPDATE assignments SET title_override = NULL WHERE feature_id = ?').run(id);
|
|
396
|
+
for (const { version, title: previousTitle } of changed) {
|
|
397
|
+
db.query('UPDATE assignments SET updated_at = ?, updated_by = ? WHERE feature_id = ? AND version = ?').run(at, by, id, version);
|
|
398
|
+
record(db, version, { at, by, id, field: 'title', from: previousTitle, to: title, released, dev });
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
});
|
|
402
|
+
if (changed) debug.log('release.features', 'retitled', { id, title, by });
|
|
403
|
+
} finally { db.close(); }
|
|
404
|
+
}
|
|
405
|
+
export function evidenceAdd(id: string, version: string, ref: string, by: string, released = releasedVersion(), dev = devVersion()): void {
|
|
406
|
+
validateVersion(version);
|
|
407
|
+
if (!ref.trim()) throw new CliUserError('근거 ref 가 비었다');
|
|
408
|
+
const db = open();
|
|
409
|
+
try {
|
|
410
|
+
const imported = transaction(db, () => {
|
|
411
|
+
const loaded = importOnDb(db, version);
|
|
412
|
+
if (!db.query('SELECT 1 FROM assignments WHERE feature_id = ? AND version = ?').get(id, version)) throw new CliUserError(`없는 칸: ${id}`);
|
|
413
|
+
const at = new Date().toISOString();
|
|
414
|
+
db.query('INSERT INTO evidence (feature_id, version, ref, at, by) VALUES (?, ?, ?, ?, ?)').run(id, version, ref, at, by);
|
|
415
|
+
record(db, version, { at, by, id, field: 'evidence.add', from: null, to: ref, released, dev });
|
|
416
|
+
return loaded;
|
|
417
|
+
});
|
|
418
|
+
if (imported) debug.log('release.features', 'imported', { version });
|
|
419
|
+
} finally { db.close(); }
|
|
420
|
+
}
|
|
421
|
+
export function history(id: string): Array<ChecklistHistory & { version: string; seq: number }> {
|
|
422
|
+
importLegacyAll();
|
|
423
|
+
const db = open();
|
|
424
|
+
try { return (db.query('SELECT * FROM events WHERE feature_id = ? ORDER BY at, seq').all(id) as EventRow[]).map((row) => ({ ...historyRow(row), version: row.version, seq: row.seq })); }
|
|
425
|
+
finally { db.close(); }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function details(id: string): { id: string; title: string; owner: string | null; kind: string | null; createdAt: string; evidence: Array<{ version: string; ref: string; at: string; by: string }> } | null {
|
|
429
|
+
importLegacyAll();
|
|
430
|
+
const db = open();
|
|
431
|
+
try {
|
|
432
|
+
const row = db.query('SELECT id, title, owner, kind, created_at FROM features WHERE id = ?').get(id) as { id: string; title: string; owner: string | null; kind: string | null; created_at: string } | null;
|
|
433
|
+
if (!row) return null;
|
|
434
|
+
const evidence = db.query('SELECT version, ref, at, by FROM evidence WHERE feature_id = ? ORDER BY at, rowid').all(id) as Array<{ version: string; ref: string; at: string; by: string }>;
|
|
435
|
+
return { id: row.id, title: row.title, owner: row.owner, kind: row.kind, createdAt: row.created_at, evidence };
|
|
436
|
+
} finally { db.close(); }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function exportJson(version: string, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
440
|
+
const data = list(version, released, dev);
|
|
441
|
+
const path = jsonPath(version);
|
|
442
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
443
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
444
|
+
const contents = `${JSON.stringify(data, null, 2)}\n`;
|
|
445
|
+
try {
|
|
446
|
+
writeFileSync(temp, contents, { mode: 0o600 });
|
|
447
|
+
chmodSync(temp, 0o600);
|
|
448
|
+
renameSync(temp, path);
|
|
449
|
+
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
450
|
+
const db = open();
|
|
451
|
+
try { transaction(db, () => {
|
|
452
|
+
// Track only our bytes: a legacy replacement after rename must remain detectable.
|
|
453
|
+
const hash = jsonHash(contents);
|
|
454
|
+
db.query(`INSERT INTO imported_versions (version, json_hash, imported_at) VALUES (?, ?, ?)
|
|
455
|
+
ON CONFLICT(version) DO UPDATE SET json_hash = excluded.json_hash, imported_at = excluded.imported_at`)
|
|
456
|
+
.run(version, hash, new Date().toISOString());
|
|
457
|
+
}); }
|
|
458
|
+
finally { db.close(); }
|
|
459
|
+
return data;
|
|
460
|
+
}
|
|
@@ -15,6 +15,8 @@ export interface ReleaseManifestInput {
|
|
|
15
15
|
cutoff: { sha: string };
|
|
16
16
|
landings: ReleaseLanding[];
|
|
17
17
|
notes?: Map<number, ReleaseNoteFragment>;
|
|
18
|
+
/** Note lines from release/next.md at the cutoff, each tied to the landing that added it when known. */
|
|
19
|
+
nextMd?: NextMdNote[];
|
|
18
20
|
runningGoalPaths: string[];
|
|
19
21
|
publicCommandsBefore: string[];
|
|
20
22
|
publicCommandsAfter: string[];
|
|
@@ -24,9 +26,41 @@ export interface ReleaseManifest {
|
|
|
24
26
|
version: string;
|
|
25
27
|
baseline: ReleaseManifestInput['baseline'];
|
|
26
28
|
cutoff: ReleaseManifestInput['cutoff'];
|
|
27
|
-
in: Array<{ sha: string; title: string; prNumber?: number; docs: 'present' | 'missing' | 'n/a'; line?: string; kind?: ReleaseNoteFragment['kind'] | 'unknown'; note?: 'missing' }>;
|
|
29
|
+
in: Array<{ sha: string; title: string; prNumber?: number; docs: 'present' | 'missing' | 'n/a'; line?: string; kind?: ReleaseNoteFragment['kind'] | NextMdKind | 'unknown'; note?: 'missing' }>;
|
|
28
30
|
deferred: Array<{ sha: string; reason: 'release-target-later' | 'run-in-flight' }>;
|
|
29
|
-
escalate: Array<{ kind: 'command-removed'; command: string }>;
|
|
31
|
+
escalate: Array<{ kind: 'command-removed'; command: string } | { kind: 'notes-empty'; featFixLandings: number; nextMdLines: number }>;
|
|
32
|
+
/** Where each IN line came from — present when next.md was read. */
|
|
33
|
+
fragments?: { byPr: number; byNextMd: number; unlinked: number; unknown: number };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type NextMdKind = 'feat' | 'fix' | 'security' | 'perf' | 'docs' | 'internal';
|
|
37
|
+
export interface NextMdNote { kind: NextMdKind; line: string; sha?: string; /** The list line as written, to find the landing that added it. */ raw?: string }
|
|
38
|
+
|
|
39
|
+
const NEXT_MD_KINDS: readonly NextMdKind[] = ['feat', 'fix', 'security', 'perf', 'docs', 'internal'];
|
|
40
|
+
const USER_KINDS = new Set(['feat', 'fix', 'security']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read `release/next.md` note lines: the `## Feat` / `## Fix` … heading gives the kind, an explicit `- <kind> — ` prefix wins.
|
|
44
|
+
* The trailing `Documentation: … Target: …` fields are dropped from the sentence; `Target: later` lines are skipped.
|
|
45
|
+
*/
|
|
46
|
+
export function parseNextMdNotes(text: string): NextMdNote[] {
|
|
47
|
+
const notes: NextMdNote[] = [];
|
|
48
|
+
let section: NextMdKind | null = null;
|
|
49
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
50
|
+
const heading = /^##\s+(\S+)\s*$/.exec(raw.trim());
|
|
51
|
+
if (heading) { const kind = heading[1]!.toLowerCase(); section = (NEXT_MD_KINDS as readonly string[]).includes(kind) ? kind as NextMdKind : null; continue; }
|
|
52
|
+
const item = /^-\s+(.*\S)\s*$/.exec(raw);
|
|
53
|
+
if (!item) continue;
|
|
54
|
+
let body = item[1]!;
|
|
55
|
+
if (/\bTarget:\s*later\b/i.test(body)) continue;
|
|
56
|
+
const prefix = /^(feat|fix|security|perf|docs|internal)\s+[—-]\s+/i.exec(body);
|
|
57
|
+
const kind = prefix ? prefix[1]!.toLowerCase() as NextMdKind : section;
|
|
58
|
+
if (!kind) continue;
|
|
59
|
+
if (prefix) body = body.slice(prefix[0].length);
|
|
60
|
+
body = body.replace(/\s+Documentation:.*$/s, '').replace(/\s+Target:\s*\S+\.?\s*$/, '').trim();
|
|
61
|
+
if (body) notes.push({ kind, line: body, raw: raw.trim() });
|
|
62
|
+
}
|
|
63
|
+
return notes;
|
|
30
64
|
}
|
|
31
65
|
|
|
32
66
|
/** Classify the already-observed landings without reading git, run ledgers or the filesystem. */
|
|
@@ -41,6 +75,14 @@ export function buildReleaseManifest(input: ReleaseManifestInput): ReleaseManife
|
|
|
41
75
|
deferred: [],
|
|
42
76
|
escalate: [],
|
|
43
77
|
};
|
|
78
|
+
const nextBySha = new Map<string, NextMdNote[]>();
|
|
79
|
+
const landingShas = new Set(input.landings.map((landing) => landing.sha));
|
|
80
|
+
const unlinked: NextMdNote[] = [];
|
|
81
|
+
for (const note of input.nextMd ?? []) {
|
|
82
|
+
if (note.sha && landingShas.has(note.sha)) nextBySha.set(note.sha, [...(nextBySha.get(note.sha) ?? []), note]);
|
|
83
|
+
else unlinked.push(note);
|
|
84
|
+
}
|
|
85
|
+
const counts = { byPr: 0, byNextMd: 0, unlinked: 0, unknown: 0 };
|
|
44
86
|
for (const landing of input.landings) {
|
|
45
87
|
const fragment = landing.prNumber === undefined ? undefined : input.notes?.get(landing.prNumber);
|
|
46
88
|
if (landing.goalPath && running.has(landing.goalPath)) {
|
|
@@ -53,11 +95,23 @@ export function buildReleaseManifest(input: ReleaseManifestInput): ReleaseManife
|
|
|
53
95
|
: landing.changedFiles.some((file) => file.startsWith('release/public/docs/')) ? 'present'
|
|
54
96
|
: landing.changedFiles.some((file) => file.startsWith('src/cli/') || file === 'src/index.ts') ? 'missing'
|
|
55
97
|
: 'n/a';
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
98
|
+
const base: ReleaseManifest['in'][number] = { sha: landing.sha, title: landing.title, ...(landing.prNumber === undefined ? {} : { prNumber: landing.prNumber }), docs };
|
|
99
|
+
// PR fragment first, then the next.md lines this landing added; only with neither is the line unknown.
|
|
100
|
+
const fromNext = fragment ? [] : nextBySha.get(landing.sha) ?? [];
|
|
101
|
+
if (fragment) { counts.byPr++; result.in.push({ ...base, line: fragment.line, kind: fragment.kind }); }
|
|
102
|
+
else if (fromNext.length) for (const note of fromNext) { counts.byNextMd++; result.in.push({ ...base, line: note.line, kind: note.kind }); }
|
|
103
|
+
else { if (input.notes) counts.unknown++; result.in.push({ ...base, ...(input.notes ? { line: landing.title, kind: 'unknown' as const, note: 'missing' as const } : {}) }); }
|
|
59
104
|
}
|
|
60
105
|
}
|
|
106
|
+
// A next.md line no landing in range claims still belongs to this release.
|
|
107
|
+
for (const note of unlinked) { counts.unlinked++; result.in.push({ sha: note.sha ?? '', title: note.line, docs: 'n/a', line: note.line, kind: note.kind }); }
|
|
108
|
+
if (input.nextMd) {
|
|
109
|
+
result.fragments = counts;
|
|
110
|
+
const shown = result.in.filter((entry) => entry.line && entry.kind && USER_KINDS.has(entry.kind)).length;
|
|
111
|
+
const featFixLandings = input.landings.filter((landing) => /^(feat|fix)\b/i.test(landing.title)).length;
|
|
112
|
+
const nextMdLines = input.nextMd.filter((note) => USER_KINDS.has(note.kind)).length;
|
|
113
|
+
if (!shown && (featFixLandings || nextMdLines)) result.escalate.push({ kind: 'notes-empty', featFixLandings, nextMdLines });
|
|
114
|
+
}
|
|
61
115
|
for (const command of input.publicCommandsBefore) {
|
|
62
116
|
if (!after.has(command)) result.escalate.push({ kind: 'command-removed', command });
|
|
63
117
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CliUserError } from '../cli/cli-user-error.js';
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
import { readSchedule, readSchedules, writeSchedule, type ScheduleRow } from './feature-store.js';
|
|
4
|
+
|
|
5
|
+
export type ReleaseSchedule = ScheduleRow;
|
|
6
|
+
|
|
7
|
+
function utcIso(input: string): string {
|
|
8
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(Z|([+-])(\d{2}):(\d{2}))$/.exec(input);
|
|
9
|
+
if (!match) throw new CliUserError(`시각에는 ISO 오프셋 필요: ${input}`);
|
|
10
|
+
const hours = Number(match[6] ?? 0), minutes = Number(match[7] ?? 0);
|
|
11
|
+
const offset = (match[5] === '-' ? -1 : 1) * (hours * 60 + minutes);
|
|
12
|
+
const millis = Date.parse(input);
|
|
13
|
+
if (hours > 23 || minutes > 59 || !Number.isFinite(millis) ||
|
|
14
|
+
new Date(millis + offset * 60_000).toISOString().slice(0, 19) !== `${match[1]}T${match[2]}:${match[3] ?? '00'}`) {
|
|
15
|
+
throw new CliUserError(`잘못된 ISO 시각: ${input}`);
|
|
16
|
+
}
|
|
17
|
+
return new Date(millis).toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatKst(utc: string): string {
|
|
21
|
+
const parts = new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(utc));
|
|
22
|
+
const get = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)!.value;
|
|
23
|
+
return `${get('month')}-${get('day')}(${get('weekday')}) ${get('hour')}:${get('minute')} KST`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getSchedule(version: string, root?: string): ReleaseSchedule | null {
|
|
27
|
+
const schedule = readSchedule(version, root);
|
|
28
|
+
debug.log('release.schedule', 'read', { version, cutAt: schedule?.cutAt ?? null, landBy: schedule?.landBy ?? null });
|
|
29
|
+
return schedule;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listSchedules(root?: string): ReleaseSchedule[] {
|
|
33
|
+
const schedules = readSchedules(root);
|
|
34
|
+
for (const { version, cutAt, landBy } of schedules) debug.log('release.schedule', 'read', { version, cutAt, landBy });
|
|
35
|
+
return schedules;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string }, by: string, root?: string): ReleaseSchedule {
|
|
39
|
+
const schedule = writeSchedule(version, {
|
|
40
|
+
...(patch.cutAt !== undefined ? { cutAt: utcIso(patch.cutAt) } : {}),
|
|
41
|
+
...(patch.landBy !== undefined ? { landBy: utcIso(patch.landBy) } : {}),
|
|
42
|
+
}, by, root);
|
|
43
|
+
debug.log('release.schedule', 'set', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
|
|
44
|
+
return schedule;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatSchedule(schedule: ReleaseSchedule): string {
|
|
48
|
+
const cut = formatKst(schedule.cutAt);
|
|
49
|
+
const deadline = schedule.landBy ? formatKst(schedule.landBy) : null;
|
|
50
|
+
const sameDay = schedule.landBy && new Date(Date.parse(schedule.landBy) + 9 * 60 * 60_000).toISOString().slice(0, 10)
|
|
51
|
+
=== new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
|
|
52
|
+
return `${schedule.version} 컷 ${cut}${deadline ? ` · 착지 마감 ${sameDay ? deadline.split(' ')[1] : deadline}` : ''}`;
|
|
53
|
+
}
|
package/src/repl/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { createInterface, type Interface as ReadlineInterface } from 'node:readl
|
|
|
21
21
|
import { existsSync, readFileSync } from 'node:fs';
|
|
22
22
|
import { resolve as resolvePath, isAbsolute, basename } from 'node:path';
|
|
23
23
|
import * as ui from '../ui.js';
|
|
24
|
+
import { suggestProjectForFolder } from '../project/project-store.js';
|
|
24
25
|
import { runTurn } from '../session/chat.js';
|
|
25
26
|
import {
|
|
26
27
|
ensureCliSession,
|
|
@@ -450,6 +451,12 @@ export async function runRepl(opts: ReplOpts): Promise<number> {
|
|
|
450
451
|
const session = ensureCliSession(cfg, initialId);
|
|
451
452
|
setActiveSessionId(session.id);
|
|
452
453
|
setAmbientSessionId(session.id);
|
|
454
|
+
if (!opts.jsonOutput && (!initialId || session.id !== initialId)) {
|
|
455
|
+
try {
|
|
456
|
+
const project = suggestProjectForFolder(process.cwd());
|
|
457
|
+
if (project) ui.info(`Project suggestion: ${project.name} (${project.id})`);
|
|
458
|
+
} catch { /* suggestions must not interrupt the REPL */ }
|
|
459
|
+
}
|
|
453
460
|
const sessionIdRef = { current: session.id };
|
|
454
461
|
|
|
455
462
|
const tools = opts.enableTools ? buildReplTools() : undefined;
|