elanous 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +73 -1
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +10 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/helper-scan.ts +10 -3
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +133 -15
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +59 -12
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +39 -6
- package/src/task-agent/judge.ts +31 -5
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +63 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
4
|
+
import { listSeatRequests, type SeatRequestRow } from '../../seat-dispatch/seat-request-ledger.js';
|
|
3
5
|
import { listCoordEvents } from '../../context-bus/coord-events.js';
|
|
4
6
|
import { CardStore, cardIndexPath, type TaskCard } from '../../task-cards/card-store.js';
|
|
5
7
|
import { loadRunLedger, resolveFederatedRunLedgerDirectories, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
|
|
@@ -21,7 +23,9 @@ export interface LoopEdgesDeps {
|
|
|
21
23
|
cardRoot?: string;
|
|
22
24
|
ledgerDir?: string;
|
|
23
25
|
listCards?: () => TaskCard[];
|
|
24
|
-
listRunStarts?: (since: string) => Array<{ runId: string; at: string; seat: string }>;
|
|
26
|
+
listRunStarts?: (since: string) => Array<{ runId: string; at: string; seat: string; mergedAt?: string }>;
|
|
27
|
+
listSeatRequests?: () => SeatRequestRow[];
|
|
28
|
+
listOrchestratorLaunches?: () => Array<{ key: string; runId: string }>;
|
|
25
29
|
listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
|
|
26
30
|
seatIds?: () => string[];
|
|
27
31
|
}
|
|
@@ -47,7 +51,9 @@ function inWindow(at: string, since: number, now: number): boolean {
|
|
|
47
51
|
return Number.isFinite(ms) && ms >= since && ms <= now;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
type PlacedCell = { cardId: string; cellId: string; owner: string };
|
|
55
|
+
|
|
56
|
+
function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySet<string>, placedCells: PlacedCell[]): LoopEdge[] {
|
|
51
57
|
if (!card.goalId.startsWith('wish:') || !ID.test(card.id)) return [];
|
|
52
58
|
const node = `card:${card.id}`;
|
|
53
59
|
const ref = card.id;
|
|
@@ -75,6 +81,7 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
|
|
|
75
81
|
if (!owner || !placement || placement.id !== cellId || !ID.test(cellId)
|
|
76
82
|
|| typeof placement.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(placement.version)) continue;
|
|
77
83
|
placedOwners.add(owner);
|
|
84
|
+
placedCells.push({ cardId: card.id, cellId, owner });
|
|
78
85
|
if (inWindow(section.createdAt, since, now)) {
|
|
79
86
|
const cellNode = `loop:${cellId}`;
|
|
80
87
|
edges.push({ at: section.createdAt, kind: 'card', from: node, to: cellNode, ref });
|
|
@@ -93,9 +100,9 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
|
|
|
93
100
|
return edges;
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string }> {
|
|
103
|
+
function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
|
|
97
104
|
if (!existsSync(dir)) return [];
|
|
98
|
-
const result: Array<{ runId: string; at: string; seat: string }> = [];
|
|
105
|
+
const result: Array<{ runId: string; at: string; seat: string; mergedAt?: string }> = [];
|
|
99
106
|
let names: string[];
|
|
100
107
|
try { names = readdirSync(dir); } catch { return []; }
|
|
101
108
|
for (const name of names) {
|
|
@@ -110,14 +117,42 @@ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, strin
|
|
|
110
117
|
const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
|
|
111
118
|
const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
|
|
112
119
|
?? start?.data.originAgent ?? start?.data.controller;
|
|
113
|
-
if (start?.timestamp &&
|
|
114
|
-
|
|
120
|
+
if (start?.timestamp && seat(launchSeat, seats)) {
|
|
121
|
+
const merged = entries?.find(entry => entry.event === 'merged' && entry.data.merged === true && typeof entry.timestamp === 'string');
|
|
122
|
+
if (start.timestamp >= since || (merged?.timestamp && merged.timestamp >= since)) {
|
|
123
|
+
result.push({ runId, at: start.timestamp, seat: launchSeat, ...(merged?.timestamp ? { mergedAt: merged.timestamp } : {}) });
|
|
124
|
+
}
|
|
115
125
|
}
|
|
116
126
|
}
|
|
117
127
|
return result;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
/**
|
|
130
|
+
/** Project only the key and runId of orchestrator launches, never their card snapshots or prose. */
|
|
131
|
+
function orchestratorLaunches(root: string): Array<{ key: string; runId: string }> {
|
|
132
|
+
const dir = join(root, 'loop', 'orchestrator');
|
|
133
|
+
if (!existsSync(dir)) return [];
|
|
134
|
+
const launches: Array<{ key: string; runId: string }> = [];
|
|
135
|
+
let names: string[];
|
|
136
|
+
try { names = readdirSync(dir); } catch { return []; }
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (!/^[a-zA-Z0-9._-]+\.json$/.test(name)) continue;
|
|
139
|
+
const path = join(dir, name);
|
|
140
|
+
try {
|
|
141
|
+
if (!statSync(path).isFile() || statSync(path).size > 5 * 1024 * 1024) continue;
|
|
142
|
+
const state = parse(readFileSync(path, 'utf8'));
|
|
143
|
+
if (state?.mode !== 'live' || !Array.isArray(state.launched)) continue;
|
|
144
|
+
for (const item of state.launched) {
|
|
145
|
+
const launch = object(item);
|
|
146
|
+
if (typeof launch?.key === 'string' && typeof launch.runId === 'string') {
|
|
147
|
+
launches.push({ key: launch.key, runId: launch.runId });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch { /* An unreadable tick cannot establish a launch. */ }
|
|
151
|
+
}
|
|
152
|
+
return launches;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** GET only; project an allowlist of fields from the ledgers. Never serialize ledger records. */
|
|
121
156
|
export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
|
|
122
157
|
const params = new URL(req.url).searchParams;
|
|
123
158
|
const rawSince = params.get('since');
|
|
@@ -143,13 +178,14 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
|
|
|
143
178
|
if (seat(recipient, seats)) edges.push({ at: event.at, kind, from: event.refs.seat, to: recipient, ref });
|
|
144
179
|
}
|
|
145
180
|
}
|
|
181
|
+
const placedCells: PlacedCell[] = [];
|
|
146
182
|
if (deps.listCards) {
|
|
147
|
-
for (const card of deps.listCards()) edges.push(...cardEdges(card, since, now, seats));
|
|
183
|
+
for (const card of deps.listCards()) edges.push(...cardEdges(card, since, now, seats, placedCells));
|
|
148
184
|
} else {
|
|
149
185
|
const root = deps.cardRoot;
|
|
150
186
|
if (existsSync(cardIndexPath(root))) {
|
|
151
187
|
const store = new CardStore(root, true);
|
|
152
|
-
try { for (const card of store.listCards()) edges.push(...cardEdges(card, since, now, seats)); }
|
|
188
|
+
try { for (const card of store.listCards()) edges.push(...cardEdges(card, since, now, seats, placedCells)); }
|
|
153
189
|
finally { store.close(); }
|
|
154
190
|
}
|
|
155
191
|
}
|
|
@@ -163,9 +199,29 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
|
|
|
163
199
|
return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
|
|
164
200
|
.flatMap(dir => runStarts(from, dir, owners, seats));
|
|
165
201
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
202
|
+
const runs = readStarts(sinceIso).filter(run => seat(run.seat, seats) && ID.test(run.runId)
|
|
203
|
+
&& (inWindow(run.at, since, now) || (run.mergedAt !== undefined && inWindow(run.mergedAt, since, now))));
|
|
204
|
+
for (const run of runs) {
|
|
205
|
+
if (inWindow(run.at, since, now)) edges.push({ at: run.at, kind: 'run', from: run.seat, to: `loop:${run.runId}`, ref: run.runId });
|
|
206
|
+
}
|
|
207
|
+
if (placedCells.length && runs.length) {
|
|
208
|
+
const root = deps.cardRoot ?? effectiveInstanceRoot();
|
|
209
|
+
let requests: SeatRequestRow[] = [];
|
|
210
|
+
let launches: Array<{ key: string; runId: string }> = [];
|
|
211
|
+
try { requests = (deps.listSeatRequests ?? (() => listSeatRequests(root)))(); } catch { /* Missing request evidence is not a match. */ }
|
|
212
|
+
try { launches = (deps.listOrchestratorLaunches ?? (() => orchestratorLaunches(root)))(); } catch { /* Missing launch evidence is not a match. */ }
|
|
213
|
+
const confirmed = new Set(launches.filter(item => ID.test(item.runId) && runs.some(run => run.runId === item.runId))
|
|
214
|
+
.map(item => JSON.stringify([item.key, item.runId])));
|
|
215
|
+
for (const cell of placedCells) {
|
|
216
|
+
const key = `orch:${cell.cardId}:${cell.cellId}`;
|
|
217
|
+
if (!requests.some(row => row.key === key && row.cell === cell.cellId && row.source === 'orchestrator' && row.seat === cell.owner)) continue;
|
|
218
|
+
for (const run of runs) {
|
|
219
|
+
if (run.seat !== cell.owner || !confirmed.has(JSON.stringify([key, run.runId]))) continue;
|
|
220
|
+
if (inWindow(run.at, since, now)) edges.push({ at: run.at, kind: 'card', from: cell.owner, to: `loop:${run.runId}`, ref: cell.cardId });
|
|
221
|
+
if (run.mergedAt && inWindow(run.mergedAt, since, now)) {
|
|
222
|
+
edges.push({ at: run.mergedAt, kind: 'card', from: `loop:${run.runId}`, to: `landed:${cell.cellId}`, ref: cell.cardId });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
169
225
|
}
|
|
170
226
|
}
|
|
171
227
|
edges.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// removed — same rule as decision replies), the release checklist, and the decision ledger. Each failure becomes null upstream.
|
|
3
3
|
import { DecisionLedger } from '../../decisions/decision-ledger.js';
|
|
4
4
|
import { listCoordEvents, type CoordEvent } from '../../context-bus/coord-events.js';
|
|
5
|
-
import {
|
|
5
|
+
import { checklistDevVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
6
6
|
import { getUserConfig } from '../../user-config.js';
|
|
7
7
|
import { SEATS, kstDayRange, type ChannelComment, type ChannelSource, type MergedPr, type SeatsSources } from './ops-seats.js';
|
|
8
8
|
|
|
@@ -82,7 +82,7 @@ export function liveSeatsSources(deps: { listEvents?: typeof listCoordEvents; gi
|
|
|
82
82
|
return lines?.map((line) => JSON.parse(line) as MergedPr) ?? null;
|
|
83
83
|
},
|
|
84
84
|
checklist() {
|
|
85
|
-
const dev =
|
|
85
|
+
const dev = checklistDevVersion();
|
|
86
86
|
const next = listChecklist(nextPatch(dev)).items;
|
|
87
87
|
const current = listChecklist(dev).items;
|
|
88
88
|
const all: ChecklistItem[] = [...current, ...next];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { readdirSync, readFileSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
+
import { debug } from '../../debug/log.js';
|
|
7
8
|
import { jsonResponse } from './http-server.js';
|
|
8
9
|
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
9
10
|
|
|
@@ -47,6 +48,8 @@ export function listTemplateCatalog(dir = findTemplateDir()): WorkflowTemplateEn
|
|
|
47
48
|
try {
|
|
48
49
|
files = readdirSync(dir).filter((f) => f.endsWith('.yaml'));
|
|
49
50
|
} catch {
|
|
51
|
+
// An installed package without this folder used to answer «No templates available» silently (10-06 · files list).
|
|
52
|
+
debug.log('nexus.workflow-templates', 'template-dir-missing', { dir });
|
|
50
53
|
return [];
|
|
51
54
|
}
|
|
52
55
|
const out: WorkflowTemplateEntry[] = [];
|
package/src/oauth/codex.ts
CHANGED
|
@@ -554,6 +554,44 @@ export function codexAccessHoursLeft(accessToken: unknown, now: number = Date.no
|
|
|
554
554
|
return typeof exp === 'number' ? Math.round(((exp * 1000 - now) / 3_600_000) * 10) / 10 : null;
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
/** PREREFRESH-LOCK (10-06 · post-review of #24449) — the per-account host refresh lock path.
|
|
558
|
+
* 🩸 Two Pod launches on one host picked the same expiring account at once: both refreshed, the server
|
|
559
|
+
* rotated the refresh token under the second, its refresh failed and that run dropped the account.
|
|
560
|
+
* The lock file sits NEXT TO the account's credential state (`<codexHome>/.elanous-prerefresh.lock`) —
|
|
561
|
+
* ⛔ never inside a token file, and it carries only the holder's pid/nonce. null = no named home to lock. */
|
|
562
|
+
export function codexAccountRefreshLockPath(name: string, storePath: string = authStorePath()): string | null {
|
|
563
|
+
if (name === DEFAULT_CODEX_ACCOUNT || !isValidAccountName(name)) return null;
|
|
564
|
+
const codexHome = loadTokens(codexStoreKey(name), storePath)?.codexHome;
|
|
565
|
+
return codexHome ? join(codexHome, '.elanous-prerefresh.lock') : null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** A refresh (network + rotate + atomic write + import) can take tens of seconds — wait up to ~90 s,
|
|
569
|
+
* and treat a lock as stale only after 2 min (the async holder heartbeats its mtime meanwhile). */
|
|
570
|
+
export const CODEX_REFRESH_LOCK_OPTS = { staleMs: 120_000, retryBusyMs: 100, maxTries: 900 } as const;
|
|
571
|
+
|
|
572
|
+
/** The refresh lock could not be acquired (timeout · ENOENT · EACCES …) — `fn` never ran, so nothing was refreshed. */
|
|
573
|
+
export class CodexRefreshLockUnavailableError extends Error {
|
|
574
|
+
constructor(readonly reason: 'lock-timeout' | 'lock-error', cause: unknown) {
|
|
575
|
+
super(`codex refresh lock unavailable (${reason}): ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
576
|
+
this.name = 'CodexRefreshLockUnavailableError';
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Runs `fn` while holding the account's refresh lock (cross-process · same-process). null path = no lock.
|
|
581
|
+
* Any failure to acquire the lock throws `CodexRefreshLockUnavailableError` (fn not run); fn's own errors pass through. */
|
|
582
|
+
export async function withCodexAccountRefreshLock<T>(lockPath: string | null, fn: () => Promise<T>, opts: { staleMs?: number; retryBusyMs?: number; maxTries?: number } = CODEX_REFRESH_LOCK_OPTS): Promise<T> {
|
|
583
|
+
if (!lockPath) return fn();
|
|
584
|
+
const { acquireLockAsync } = await import('../storage/file-lock.js');
|
|
585
|
+
let handle: Awaited<ReturnType<typeof acquireLockAsync>>;
|
|
586
|
+
try { handle = await acquireLockAsync(lockPath, opts); }
|
|
587
|
+
catch (error) {
|
|
588
|
+
const timedOut = error instanceof Error && error.message.startsWith('acquireLockAsync: timed out');
|
|
589
|
+
throw new CodexRefreshLockUnavailableError(timedOut ? 'lock-timeout' : 'lock-error', error);
|
|
590
|
+
}
|
|
591
|
+
try { return await fn(); }
|
|
592
|
+
finally { handle.release(); }
|
|
593
|
+
}
|
|
594
|
+
|
|
557
595
|
export async function refreshCodexAccountHome(
|
|
558
596
|
name: string,
|
|
559
597
|
opts: {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { needsFirstRun, needsOnboarding } from '../onboarding.js';
|
|
3
|
+
import type { UserConfig } from '../user-config.js';
|
|
4
|
+
import type { FirstRunDeps, runFirstRun } from './first-run.js';
|
|
5
|
+
import { UNATTENDED_SETUP_COMMAND } from './entry-hints.js';
|
|
6
|
+
|
|
7
|
+
export interface FirstChatGateDeps {
|
|
8
|
+
isTTY: boolean;
|
|
9
|
+
runFirstRun: (deps: FirstRunDeps) => ReturnType<typeof runFirstRun>;
|
|
10
|
+
runInteractive: () => Promise<boolean>;
|
|
11
|
+
print: (line: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function firstChatGate(cfg: UserConfig, deps: FirstChatGateDeps): Promise<'continue' | 'stop'> {
|
|
15
|
+
const decide = (decision: 'continue' | 'stop', outcome: string): 'continue' | 'stop' => {
|
|
16
|
+
debug.log('onboarding.first-chat', decision, { isTTY: deps.isTTY, outcome });
|
|
17
|
+
return decision;
|
|
18
|
+
};
|
|
19
|
+
if (!needsOnboarding(cfg)) return decide('continue', 'completed');
|
|
20
|
+
if (deps.isTTY) {
|
|
21
|
+
return decide(await deps.runInteractive() ? 'continue' : 'stop', 'interactive');
|
|
22
|
+
}
|
|
23
|
+
if (!needsFirstRun(cfg)) return decide('continue', 'already-ready');
|
|
24
|
+
const result = await deps.runFirstRun({ config: cfg, isTTY: false, print: () => {} });
|
|
25
|
+
if (result.outcome === 'ready') return decide('continue', result.outcome);
|
|
26
|
+
deps.print(`다음: elanous setup llm (또는 ${UNATTENDED_SETUP_COMMAND})`);
|
|
27
|
+
process.exitCode = 2;
|
|
28
|
+
return decide('stop', result.outcome);
|
|
29
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
4
|
import * as store from './feature-store.js';
|
|
4
5
|
import { debug } from '../debug/log.js';
|
|
5
6
|
import { CliUserError } from '../cli/cli-user-error.js';
|
|
@@ -20,6 +21,8 @@ export interface ChecklistItem {
|
|
|
20
21
|
ceoDate?: string;
|
|
21
22
|
/** 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 없으면 가속 등급이 아니다. */
|
|
22
23
|
accelerator?: true;
|
|
24
|
+
/** 이 칸이 근거로 삼는 문서(저장소 상대 경로, 선택적으로 `#절`). 없으면 칸에 실리지 않는다. */
|
|
25
|
+
refs?: string[];
|
|
23
26
|
evidence?: string;
|
|
24
27
|
disposition?: ChecklistDisposition;
|
|
25
28
|
kind?: ChecklistKind;
|
|
@@ -50,6 +53,12 @@ export function devVersion(): string {
|
|
|
50
53
|
return (JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as { version: string }).version;
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/** The checklist version of the development tree: package.json says `0.2.18-dev.0` once a release branch is cut,
|
|
57
|
+
* but the checklist only knows `0.2.18` (10-07: every server caller that passed devVersion() raw got null/throw). */
|
|
58
|
+
export function checklistDevVersion(version = devVersion()): string {
|
|
59
|
+
return version.replace(/-dev\.\d+$/, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
export function parseOwner(value: string): { seat: string; sub?: string } {
|
|
54
63
|
const match = /^([A-Z]{2,8})(?:\/([a-z][a-z0-9-]{0,31}))?$/.exec(value);
|
|
55
64
|
if (!match || match[0] !== value) throw new CliUserError(`잘못된 담당: ${value}`, 'TC 또는 TC/rel');
|
|
@@ -100,12 +109,82 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
100
109
|
debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
const REPO_ROOT = join(import.meta.dir, '..', '..');
|
|
113
|
+
|
|
114
|
+
/** `--ref` 문서를 찾는 뿌리들 — 이 코드의 트리 ⊕ 지금 작업 디렉토리의 git 뿌리.
|
|
115
|
+
* ⭐ 설치본(`~/.local/share/elanous/current`)에는 `docs/` 가 없다 — 저장소 안에서 부른 설치본도 저장소 문서를 인용할 수 있게. */
|
|
116
|
+
export function refRoots(cwd = process.cwd()): string[] {
|
|
117
|
+
const roots = [REPO_ROOT];
|
|
118
|
+
try {
|
|
119
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', timeout: 5_000 });
|
|
120
|
+
const path = top.status === 0 ? top.stdout.trim() : '';
|
|
121
|
+
if (path && !roots.includes(path)) roots.push(path);
|
|
122
|
+
} catch { /* git 없음 — 코드 트리만 */ }
|
|
123
|
+
return roots;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `--ref` 값 검사: 저장소 상대 경로(⊕ `#절`)이고 그 파일이 뿌리들 중 하나에 있어야 한다. 중복은 순서를 지키며 지운다. */
|
|
127
|
+
export function normalizeRefs(refs: readonly string[], root: string | readonly string[] = refRoots()): string[] {
|
|
128
|
+
const roots = typeof root === 'string' ? [root] : root;
|
|
129
|
+
const out: string[] = [];
|
|
130
|
+
for (const raw of refs) {
|
|
131
|
+
const ref = raw.trim();
|
|
132
|
+
const path = ref.split('#')[0] ?? '';
|
|
133
|
+
if (!path || isAbsolute(path) || path.split(/[\\/]/).some((part) => part === '..')) throw new CliUserError(`잘못된 문서 참조: ${raw}`, '저장소 상대 경로[#절] — 예: docs/manual/MANUAL-x.md#§1');
|
|
134
|
+
let isFile = false;
|
|
135
|
+
for (const base of roots) {
|
|
136
|
+
try { if (statSync(join(base, path)).isFile()) { isFile = true; break; } } catch { /* 없음 */ }
|
|
137
|
+
}
|
|
138
|
+
if (!isFile) throw new CliUserError(`없는 문서: ${path}`, '저장소 상대 «파일» 경로로 준다(폴더는 안 된다)');
|
|
139
|
+
if (!out.includes(ref)) out.push(ref);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 한 문서를 인용하는 칸 한 줄 — `release checklist refs <doc>` 의 «구현 현황» 행. */
|
|
145
|
+
export interface DocRefRow { version: string; id: string; title: string; status: ChecklistStatus; owner: string | null; section: string | null }
|
|
146
|
+
|
|
147
|
+
function splitRef(ref: string): { path: string; section: string | null } {
|
|
148
|
+
const at = ref.indexOf('#');
|
|
149
|
+
const path = (at < 0 ? ref : ref.slice(0, at)).trim().replace(/^\.\//, '');
|
|
150
|
+
return { path, section: at < 0 ? null : ref.slice(at + 1) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 읽기 전용: 칸의 refs 중 경로(`#` 앞)가 doc 의 경로와 같은 것을 행으로 낸다. doc 에 `#절`이 붙으면 그 절과 정확히 같은 ref 만. */
|
|
154
|
+
export function cellsReferencingDoc(doc: string, checklists: readonly Checklist[]): DocRefRow[] {
|
|
155
|
+
const want = splitRef(doc.trim());
|
|
156
|
+
const rows: DocRefRow[] = [];
|
|
157
|
+
for (const data of checklists) {
|
|
158
|
+
for (const item of data.items) {
|
|
159
|
+
// 칸마다 한 행 — 같은 문서의 절을 여럿 인용해도 절을 모아 한 줄로(절 없는 인용이 섞이면 문서 전체로 본다).
|
|
160
|
+
const sections: string[] = [];
|
|
161
|
+
let whole = false;
|
|
162
|
+
for (const ref of item.refs ?? []) {
|
|
163
|
+
const got = splitRef(ref);
|
|
164
|
+
if (got.path !== want.path) continue;
|
|
165
|
+
if (want.section !== null && got.section !== want.section) continue;
|
|
166
|
+
if (got.section === null) whole = true;
|
|
167
|
+
else if (!sections.includes(got.section)) sections.push(got.section);
|
|
168
|
+
}
|
|
169
|
+
if (!whole && sections.length === 0) continue;
|
|
170
|
+
rows.push({ version: data.version, id: item.id, title: item.title, status: item.status, owner: item.owner ?? null, section: whole ? null : sections.join(' · ') });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return rows;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** `| 판 | 칸 | 상태 | 절 | 제목 |` 마크다운 표 — 행이 없으면 머리줄 둘만. */
|
|
177
|
+
export function renderRefsStatus(rows: readonly DocRefRow[]): string {
|
|
178
|
+
const cell = (value: string) => value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
179
|
+
return ['| 판 | 칸 | 상태 | 절 | 제목 |', '| --- | --- | --- | --- | --- |', ...rows.map((row) => `| ${cell(row.version)} | ${cell(row.id)} | ${row.status} | ${cell(row.section ?? '-')} | ${cell(row.title)} |`)].join('\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
103
182
|
export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }): void {
|
|
104
183
|
if (input.ceoMinutes !== undefined && (!Number.isSafeInteger(input.ceoMinutes) || input.ceoMinutes < 0)) throw new CliUserError('대표 손 분량은 0 이상의 정수 분이어야 한다');
|
|
105
184
|
if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
|
|
106
185
|
}
|
|
107
186
|
|
|
108
|
-
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
187
|
+
export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
109
188
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
110
189
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
111
190
|
if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
|
|
@@ -115,6 +194,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
115
194
|
if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
|
|
116
195
|
if (input.accelerator !== undefined && input.accelerator !== true) throw new CliUserError('가속 등급은 true 이거나 생략한다');
|
|
117
196
|
validateCeoLoad(input);
|
|
197
|
+
const refs = input.refs !== undefined && input.refs.length ? normalizeRefs(input.refs) : undefined;
|
|
118
198
|
return mutate(v, (data, otherItems) => {
|
|
119
199
|
if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
|
|
120
200
|
const collisions = otherItems(input.id);
|
|
@@ -125,14 +205,14 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
125
205
|
}
|
|
126
206
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
127
207
|
const at = new Date().toISOString();
|
|
128
|
-
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), updatedAt: at, updatedBy: by };
|
|
208
|
+
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
|
|
129
209
|
data.items.push(item);
|
|
130
210
|
change(data, item.id, 'add', null, item, by, at);
|
|
131
211
|
return true;
|
|
132
212
|
});
|
|
133
213
|
}
|
|
134
214
|
|
|
135
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
|
|
215
|
+
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
|
|
136
216
|
return mutate(v, (data) => {
|
|
137
217
|
const item = data.items.find((i) => i.id === id);
|
|
138
218
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
@@ -144,13 +224,15 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
|
|
|
144
224
|
if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
|
|
145
225
|
if (patch.accelerator !== undefined && patch.accelerator !== true && patch.accelerator !== null) throw new CliUserError('가속 등급은 true(부여) 또는 null(해제)이다');
|
|
146
226
|
validateCeoLoad(patch);
|
|
227
|
+
// --ref 는 «더하기»다 — 기존 참조 뒤에 붙이고 중복을 지운다.
|
|
228
|
+
if (patch.refs !== undefined) patch = { ...patch, refs: normalizeRefs([...(item.refs ?? []), ...patch.refs]) };
|
|
147
229
|
if (patch.owner !== undefined) {
|
|
148
230
|
parseOwner(patch.owner);
|
|
149
231
|
if (item.owner && patch.owner !== item.owner) {
|
|
150
232
|
throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
|
|
151
233
|
}
|
|
152
234
|
}
|
|
153
|
-
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
|
|
235
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
|
|
154
236
|
if (fields.length === 0) return false;
|
|
155
237
|
const at = new Date().toISOString();
|
|
156
238
|
for (const field of fields) {
|
|
@@ -269,6 +351,22 @@ export function checklistGate(v: string): ChecklistGate {
|
|
|
269
351
|
return result;
|
|
270
352
|
}
|
|
271
353
|
|
|
354
|
+
/** GATE-ENTRY-ALIGN: the cut-time judgement shared by `release run`'s entry check and the checklist-gate node —
|
|
355
|
+
* past the landing deadline a non-P0 yellow without a disposition is carried (moved), and a P0 yellow always blocks. */
|
|
356
|
+
export function cutChecklistGate(v: string, landBy: string | null | undefined, now: Date = new Date()): ChecklistGate & { autoMoved: string[] } {
|
|
357
|
+
const items = listChecklist(v).items;
|
|
358
|
+
const overdue = landBy !== null && landBy !== undefined && now.getTime() > Date.parse(landBy);
|
|
359
|
+
const autoMoved = overdue ? items.filter((item) => item.status === 'yellow' && item.priority !== 'P0' && item.disposition === undefined).map((item) => item.id) : [];
|
|
360
|
+
const p0 = items.filter((item) => item.status === 'yellow' && item.priority === 'P0').map((item) => item.id);
|
|
361
|
+
const gate = checklistGate(v);
|
|
362
|
+
gate.undecided = gate.undecided.filter((id) => !autoMoved.includes(id) && !p0.includes(id));
|
|
363
|
+
gate.moved = gate.moved.filter((id) => !p0.includes(id)).concat(autoMoved);
|
|
364
|
+
gate.knownIssues = gate.knownIssues.filter((item) => !p0.includes(item.id));
|
|
365
|
+
gate.blocked.push(...p0.filter((id) => !gate.blocked.includes(id)));
|
|
366
|
+
gate.ok = gate.red.length === 0 && gate.undecided.length === 0 && gate.blocked.length === 0;
|
|
367
|
+
return { ...gate, autoMoved };
|
|
368
|
+
}
|
|
369
|
+
|
|
272
370
|
export function seedFromRoadmap(v: string, markdown: string): Checklist {
|
|
273
371
|
return mutate(v, (data) => {
|
|
274
372
|
let added = false;
|
|
@@ -33,7 +33,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
|
|
|
33
33
|
const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
|
|
34
34
|
imported_versions: ['json_hash', 'imported_at'],
|
|
35
35
|
events: ['reason'],
|
|
36
|
-
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator'],
|
|
36
|
+
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
|
|
37
37
|
release_schedules: ['freeze_from', 'freeze_until'],
|
|
38
38
|
};
|
|
39
39
|
function schemaCurrent(db: Database): boolean {
|
|
@@ -75,7 +75,7 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
75
75
|
if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
|
|
76
76
|
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
77
|
const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
|
|
78
|
-
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER']] as const) {
|
|
78
|
+
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
79
|
if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
|
|
80
80
|
}
|
|
81
81
|
const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
|
|
@@ -169,7 +169,7 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
|
169
169
|
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);
|
|
170
170
|
}
|
|
171
171
|
function items(db: Database, version: string): ChecklistItem[] {
|
|
172
|
-
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
172
|
+
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.refs, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
173
173
|
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 | number | null>>;
|
|
174
174
|
return rows.map((r) => {
|
|
175
175
|
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
|
|
@@ -179,6 +179,7 @@ function items(db: Database, version: string): ChecklistItem[] {
|
|
|
179
179
|
...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
|
|
180
180
|
...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
|
|
181
181
|
...(r.accelerator === 1 ? { accelerator: true as const } : {}),
|
|
182
|
+
...(r.refs !== null && r.refs !== undefined ? { refs: JSON.parse(r.refs as string) as string[] } : {}),
|
|
182
183
|
...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
|
|
183
184
|
updatedAt: r.updated_at as string, updatedBy: r.updated_by as string };
|
|
184
185
|
});
|
|
@@ -192,9 +193,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
|
|
|
192
193
|
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);
|
|
193
194
|
// features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
|
|
194
195
|
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);
|
|
195
|
-
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
196
|
-
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
197
|
-
.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.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
|
|
196
|
+
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
197
|
+
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, refs=excluded.refs, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
198
|
+
.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.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
|
|
198
199
|
}
|
|
199
200
|
function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
|
|
200
201
|
|
|
@@ -235,8 +236,8 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
235
236
|
// Keep a legacy per-version title until an explicit retitle unifies the feature identity.
|
|
236
237
|
if (!existing) putItem(db, version, item);
|
|
237
238
|
else {
|
|
238
|
-
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
239
|
-
.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.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
|
|
239
|
+
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
240
|
+
.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.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
for (const entry of data.history) record(db, version, entry);
|
|
@@ -263,7 +264,7 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
263
264
|
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 });
|
|
264
265
|
continue;
|
|
265
266
|
}
|
|
266
|
-
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const;
|
|
267
|
+
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const;
|
|
267
268
|
const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
|
|
268
269
|
const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
|
|
269
270
|
const differs = fields.some((field) => field === 'evidence'
|
|
@@ -8,6 +8,7 @@ export interface LandingFreeze {
|
|
|
8
8
|
startedAt: string;
|
|
9
9
|
until: string | null;
|
|
10
10
|
by: string;
|
|
11
|
+
holdLaunches?: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function landingFreezePath(root = effectiveInstanceRoot()): string {
|
|
@@ -25,21 +26,22 @@ export function readLandingFreeze(root = effectiveInstanceRoot(), now = new Date
|
|
|
25
26
|
const freeze = record as Partial<LandingFreeze>;
|
|
26
27
|
if (typeof freeze.reason !== 'string' || typeof freeze.by !== 'string' ||
|
|
27
28
|
typeof freeze.startedAt !== 'string' || !Number.isFinite(Date.parse(freeze.startedAt)) ||
|
|
28
|
-
!(freeze.until === null || (typeof freeze.until === 'string' && Number.isFinite(Date.parse(freeze.until))))
|
|
29
|
+
!(freeze.until === null || (typeof freeze.until === 'string' && Number.isFinite(Date.parse(freeze.until)))) ||
|
|
30
|
+
(freeze.holdLaunches !== undefined && typeof freeze.holdLaunches !== 'boolean')) {
|
|
29
31
|
throw new Error(`invalid landing freeze: ${path}`);
|
|
30
32
|
}
|
|
31
33
|
// An expired freeze reads as «off». The file is left alone: deleting it here could race a concurrent `freeze on`
|
|
32
34
|
// that just wrote a new one (only `freeze off` removes it; the next `freeze on` overwrites it).
|
|
33
35
|
if (freeze.until && Date.parse(freeze.until) <= now.getTime()) return null;
|
|
34
|
-
return freeze as LandingFreeze;
|
|
36
|
+
return { ...freeze, holdLaunches: freeze.holdLaunches ?? false } as LandingFreeze;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
|
|
39
|
+
export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string; holdLaunches?: boolean }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
|
|
38
40
|
const until = opts.until === undefined ? null : opts.until;
|
|
39
41
|
if (until !== null && (!/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?(?:Z|[+-]\d\d:\d\d)$/.test(until) || !Number.isFinite(Date.parse(until)) || Date.parse(until) <= now.getTime())) {
|
|
40
42
|
throw new Error('--until must be a future ISO timestamp with timezone');
|
|
41
43
|
}
|
|
42
|
-
const freeze: LandingFreeze = { reason: opts.reason?.trim() || 'operator freeze', startedAt: now.toISOString(), until: until === null ? null : new Date(until).toISOString(), by: opts.by?.trim() || process.env.ELANOUS_TRACK || process.env.USER || 'cli' };
|
|
44
|
+
const freeze: LandingFreeze = { reason: opts.reason?.trim() || 'operator freeze', startedAt: now.toISOString(), until: until === null ? null : new Date(until).toISOString(), by: opts.by?.trim() || process.env.ELANOUS_TRACK || process.env.USER || 'cli', holdLaunches: opts.holdLaunches ?? false };
|
|
43
45
|
const path = landingFreezePath(root);
|
|
44
46
|
mkdirSync(root, { recursive: true });
|
|
45
47
|
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -8,6 +8,7 @@ import { userConfigPath } from '../user-config.js';
|
|
|
8
8
|
import { addItem, listChecklist, ownerMatches, parseOwner, setItem, validateCeoLoad, type ChecklistItem } from './checklist.js';
|
|
9
9
|
import { move, releasedVersion } from './feature-store.js';
|
|
10
10
|
import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
|
|
11
|
+
import { parseRubric, rubricScore } from './rubric.js';
|
|
11
12
|
|
|
12
13
|
export type PlacementPriority = 'P0' | 'P1' | 'P2';
|
|
13
14
|
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
|
|
@@ -271,20 +272,29 @@ export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceR
|
|
|
271
272
|
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
272
273
|
const next = rows.find((row) => versionOrder(row.version, version) > 0 && !backlog.includes(row.version));
|
|
273
274
|
const snapshots = new Map(rows.map((row) => [row.version, [...(deps.checklist ?? listChecklist)(row.version).items]]));
|
|
274
|
-
const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence)
|
|
275
|
+
const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence)
|
|
276
|
+
.map((item, index) => {
|
|
277
|
+
const rubric = parseRubric(`${item.title}\n${item.evidence ?? ''}`);
|
|
278
|
+
return { item, index, score: rubric ? rubricScore(rubric) : null };
|
|
279
|
+
})
|
|
280
|
+
.sort((a, b) => {
|
|
281
|
+
if (a.score === null) return b.score === null ? a.index - b.index : 1;
|
|
282
|
+
if (b.score === null) return -1;
|
|
283
|
+
return a.score - b.score || a.index - b.index;
|
|
284
|
+
});
|
|
275
285
|
if (unstarted.length && !next) throw new CliUserError(`${version} 다음 판이 없다`);
|
|
276
286
|
if (!unstarted.length) return { decisions: [], blocked: [] };
|
|
277
287
|
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
278
288
|
const caps = deps.seatCap ?? placementSeatCap();
|
|
279
289
|
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
280
290
|
const result: RebalanceResult = { decisions: [], blocked: [] };
|
|
281
|
-
for (const item of unstarted) {
|
|
291
|
+
for (const { item, score } of unstarted) {
|
|
282
292
|
const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
|
|
283
293
|
if (violation) {
|
|
284
294
|
result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
|
|
285
295
|
continue;
|
|
286
296
|
}
|
|
287
|
-
const reason = `${version} 착지 마감 2시간 전 미시작 칸
|
|
297
|
+
const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월${score === null ? '' : ` · 루브릭 ${score}`}`;
|
|
288
298
|
result.decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
|
|
289
299
|
snapshots.set(version, snapshots.get(version)!.filter((cell) => cell.id !== item.id));
|
|
290
300
|
snapshots.get(next!.version)!.push(item);
|