elanous 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +2 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +186 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/coord-post.sh +11 -1
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +163 -0
- package/scripts/heal/run-role.ts +14 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +223 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lessons/handbook-page.ts +85 -0
- package/scripts/release-loop/gate-node.ts +62 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +196 -0
- package/scripts/release-story/draft.ts +172 -0
- package/scripts/rhythm/daily.ts +383 -0
- package/scripts/rhythm/weekly.ts +294 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/test-deterministic.ts +57 -11
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +15 -10
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/build/llm-conflict-merge.ts +45 -8
- package/src/autopilot/build/merge-intent.ts +53 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +157 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +84 -20
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +44 -2
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +248 -45
- package/src/cli/rfc-status.ts +91 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +189 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-public.ts +86 -0
- package/src/context-bus/context-now-surfaces.ts +70 -4
- package/src/context-bus/context-now.ts +4 -3
- package/src/context-bus/long-term-memory.ts +201 -24
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +83 -18
- package/src/discord/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- package/src/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +13 -2
- package/src/graph-runner/runner.ts +111 -22
- package/src/harness/harness-cli-command.ts +178 -27
- package/src/harness/harness-incidents.ts +43 -12
- package/src/harness/harness-pod-dispatch.ts +2 -0
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +494 -0
- package/src/harness/helper-scan.ts +139 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/hq/promote.ts +301 -0
- package/src/index.ts +281 -27
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +72 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +32 -0
- package/src/loops/checker.ts +180 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +72 -6
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +59 -0
- package/src/loops/orchestrator/author-depth.ts +164 -0
- package/src/loops/orchestrator/author-input-check.ts +77 -0
- package/src/loops/orchestrator/author-ledger.ts +230 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +180 -0
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/tick.ts +302 -0
- package/src/loops/orchestrator/traffic.ts +165 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/registry.ts +454 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +34 -0
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +65 -2
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/schedules-read.ts +5 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/auto-start.ts +48 -0
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +28 -16
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/placement.ts +242 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +73 -15
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-loop/seat-loop.ts +495 -46
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/dev-pipeline.ts +6 -2
- package/src/self-dev/draft-sweep.ts +76 -0
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +8 -0
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +294 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +79 -0
- package/src/self-implement/host-regate.ts +25 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +114 -10
- package/src/self-implement/revert-guard.ts +62 -0
- package/src/self-implement/run-ledger.ts +1 -1
- package/src/self-implement/seams.ts +28 -8
- package/src/session/index.ts +2 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +86 -0
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +204 -16
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
2
|
+
import { releaseReadiness, type ReleaseReadiness } from '../../scripts/release-loop/release-readiness.js';
|
|
3
|
+
import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
|
|
4
|
+
|
|
5
|
+
/** Cuts become eligible at cutAt and remain eligible through the end of the window. */
|
|
6
|
+
export function selectScheduledCut(schedules: readonly ReleaseSchedule[], now: Date, windowMinutes: number): ReleaseSchedule | null {
|
|
7
|
+
if (!Number.isFinite(windowMinutes) || windowMinutes <= 0) throw new Error('windowMinutes must be positive and finite');
|
|
8
|
+
const current = now.getTime();
|
|
9
|
+
if (!Number.isFinite(current)) throw new Error('now must be a valid date');
|
|
10
|
+
return schedules
|
|
11
|
+
.filter((schedule) => {
|
|
12
|
+
const cut = Date.parse(schedule.cutAt);
|
|
13
|
+
return Number.isFinite(cut) && cut <= current && current - cut <= windowMinutes * 60_000;
|
|
14
|
+
})
|
|
15
|
+
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt) || a.version.localeCompare(b.version))[0] ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AutoStartResult =
|
|
19
|
+
| { status: 'no-cut' }
|
|
20
|
+
| { status: 'planned'; version: string; cutAt: string }
|
|
21
|
+
| { status: 'skipped'; version: string; reason: Exclude<ReleaseReadiness, { ready: true }>['reason'] }
|
|
22
|
+
| { status: 'started'; version: string; cutAt: string };
|
|
23
|
+
|
|
24
|
+
export interface AutoStartOptions {
|
|
25
|
+
windowMinutes: number;
|
|
26
|
+
apply?: boolean;
|
|
27
|
+
now?: Date;
|
|
28
|
+
ledgerRoot?: string;
|
|
29
|
+
schedules?: (root: string) => ReleaseSchedule[];
|
|
30
|
+
readiness?: (version: string, ledgerRoot: string, now: Date) => ReleaseReadiness;
|
|
31
|
+
launch?: (version: string) => void | Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Select from the release schedule; the existing `release run --if-ready` owns the launch-time lock and guards. */
|
|
35
|
+
export async function autoStartScheduledRelease(options: AutoStartOptions): Promise<AutoStartResult> {
|
|
36
|
+
const root = options.ledgerRoot ?? releaseLedgerRoot();
|
|
37
|
+
const now = options.now ?? new Date();
|
|
38
|
+
// Validate the window even when the ledger has no rows.
|
|
39
|
+
const schedule = selectScheduledCut((options.schedules ?? listSchedules)(root), now, options.windowMinutes);
|
|
40
|
+
if (!schedule) return { status: 'no-cut' };
|
|
41
|
+
const { version, cutAt } = schedule;
|
|
42
|
+
if (!options.apply) return { status: 'planned', version, cutAt };
|
|
43
|
+
const readiness = (options.readiness ?? ((v, ledgerRoot, at) => releaseReadiness(v, { ledgerRoot, now: () => at })))(version, root, now);
|
|
44
|
+
if (!readiness.ready) return { status: 'skipped', version, reason: readiness.reason };
|
|
45
|
+
if (!options.launch) throw new Error('auto-start apply requires a launch callback');
|
|
46
|
+
await options.launch(version);
|
|
47
|
+
return { status: 'started', version, cutAt };
|
|
48
|
+
}
|
|
@@ -13,6 +13,9 @@ export interface ChecklistItem {
|
|
|
13
13
|
title: string;
|
|
14
14
|
status: ChecklistStatus;
|
|
15
15
|
owner?: string;
|
|
16
|
+
priority?: 'P0' | 'P1' | 'P2';
|
|
17
|
+
predecessors?: string[];
|
|
18
|
+
deadlineVersion?: string;
|
|
16
19
|
evidence?: string;
|
|
17
20
|
disposition?: ChecklistDisposition;
|
|
18
21
|
kind?: ChecklistKind;
|
|
@@ -93,11 +96,14 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
93
96
|
debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
99
|
+
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
97
100
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
98
101
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
99
102
|
if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
|
|
100
103
|
if (input.owner !== undefined) parseOwner(input.owner);
|
|
104
|
+
if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
105
|
+
if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
|
|
106
|
+
if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
|
|
101
107
|
return mutate(v, (data, otherItems) => {
|
|
102
108
|
if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
|
|
103
109
|
const collisions = otherItems(input.id);
|
|
@@ -108,27 +114,30 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
108
114
|
}
|
|
109
115
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
110
116
|
const at = new Date().toISOString();
|
|
111
|
-
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), updatedAt: at, updatedBy: by };
|
|
117
|
+
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 } : {}), updatedAt: at, updatedBy: by };
|
|
112
118
|
data.items.push(item);
|
|
113
119
|
change(data, item.id, 'add', null, item, by, at);
|
|
114
120
|
return true;
|
|
115
121
|
});
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind }, by: string): Checklist {
|
|
124
|
+
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 }, by: string): Checklist {
|
|
119
125
|
return mutate(v, (data) => {
|
|
120
126
|
const item = data.items.find((i) => i.id === id);
|
|
121
127
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
122
128
|
if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
|
|
123
129
|
if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
|
|
124
130
|
if (patch.kind !== undefined && patch.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen');
|
|
131
|
+
if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
|
|
132
|
+
if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
|
|
133
|
+
if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
|
|
125
134
|
if (patch.owner !== undefined) {
|
|
126
135
|
parseOwner(patch.owner);
|
|
127
136
|
if (item.owner && patch.owner !== item.owner) {
|
|
128
137
|
throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
|
-
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
|
|
140
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
|
|
132
141
|
if (fields.length === 0) return false;
|
|
133
142
|
const at = new Date().toISOString();
|
|
134
143
|
for (const field of fields) {
|
|
@@ -58,14 +58,20 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
58
58
|
if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
|
|
59
59
|
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');
|
|
60
60
|
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)');
|
|
61
|
+
const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
|
|
62
|
+
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT']] as const) {
|
|
63
|
+
if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
|
|
64
|
+
}
|
|
65
|
+
const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
|
|
66
|
+
for (const name of ['freeze_from', 'freeze_until']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
|
|
61
67
|
return db;
|
|
62
68
|
} catch (error) { db.close(); throw error; }
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; updatedAt: string; updatedBy: string }
|
|
66
|
-
type StoredSchedule = { version: string; cut_at: string; land_by: string | null; updated_at: string; updated_by: string };
|
|
71
|
+
export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; updatedAt: string; updatedBy: string }
|
|
72
|
+
type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; updated_at: string; updated_by: string };
|
|
67
73
|
function scheduleRow(row: StoredSchedule): ScheduleRow {
|
|
68
|
-
return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, updatedAt: row.updated_at, updatedBy: row.updated_by };
|
|
74
|
+
return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export function readSchedule(version: string, root?: string): ScheduleRow | null {
|
|
@@ -84,7 +90,7 @@ export function readSchedules(root?: string): ScheduleRow[] {
|
|
|
84
90
|
finally { db.close(); }
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string }, by: string, root?: string): ScheduleRow {
|
|
93
|
+
export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ScheduleRow {
|
|
88
94
|
validateVersion(version);
|
|
89
95
|
const db = open(root);
|
|
90
96
|
try {
|
|
@@ -93,13 +99,17 @@ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?:
|
|
|
93
99
|
const cutAt = patch.cutAt ?? previous?.cut_at;
|
|
94
100
|
if (!cutAt) throw new CliUserError('새 판에는 --cut-at 이 필요하다');
|
|
95
101
|
const landBy = patch.landBy ?? previous?.land_by ?? null;
|
|
96
|
-
|
|
102
|
+
const freezeFrom = patch.freezeFrom ?? previous?.freeze_from ?? null;
|
|
103
|
+
const freezeUntil = patch.freezeUntil ?? previous?.freeze_until ?? null;
|
|
104
|
+
if (Boolean(freezeFrom) !== Boolean(freezeUntil) || (freezeFrom && freezeUntil && Date.parse(freezeFrom) >= Date.parse(freezeUntil))) throw new CliUserError('동결 시작·끝은 함께 주고 시작이 끝보다 앞서야 한다');
|
|
105
|
+
if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil) return scheduleRow(previous);
|
|
97
106
|
const at = new Date().toISOString();
|
|
98
|
-
db.query(`INSERT INTO release_schedules (version, cut_at, land_by, updated_at, updated_by) VALUES (?, ?, ?, ?, ?)
|
|
99
|
-
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`)
|
|
100
|
-
.run(version, cutAt, landBy, at, by);
|
|
107
|
+
db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
108
|
+
ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
109
|
+
.run(version, cutAt, landBy, freezeFrom, freezeUntil, at, by);
|
|
101
110
|
for (const [field, from, to] of [
|
|
102
111
|
['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
|
|
112
|
+
['freeze_from', previous?.freeze_from ?? null, freezeFrom], ['freeze_until', previous?.freeze_until ?? null, freezeUntil],
|
|
103
113
|
] as const) {
|
|
104
114
|
if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
|
|
105
115
|
}
|
|
@@ -134,12 +144,14 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
|
134
144
|
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);
|
|
135
145
|
}
|
|
136
146
|
function items(db: Database, version: string): ChecklistItem[] {
|
|
137
|
-
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
|
|
147
|
+
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.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
138
148
|
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>>;
|
|
139
149
|
return rows.map((r) => {
|
|
140
150
|
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id!)].filter((value) => value !== null).join('\n');
|
|
141
151
|
return { id: r.id!, title: r.title!, status: r.status as ChecklistItem['status'],
|
|
142
152
|
...(r.owner !== null ? { owner: r.owner! } : {}), ...(r.kind !== null ? { kind: r.kind as ChecklistItem['kind'] } : {}),
|
|
153
|
+
...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors!) as string[] } : {}),
|
|
154
|
+
...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version! } : {}),
|
|
143
155
|
...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
|
|
144
156
|
updatedAt: r.updated_at!, updatedBy: r.updated_by! };
|
|
145
157
|
});
|
|
@@ -153,9 +165,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
|
|
|
153
165
|
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);
|
|
154
166
|
// features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
|
|
155
167
|
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);
|
|
156
|
-
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
157
|
-
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`)
|
|
158
|
-
.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);
|
|
168
|
+
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
169
|
+
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, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
170
|
+
.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.updatedAt, item.updatedBy);
|
|
159
171
|
}
|
|
160
172
|
function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
|
|
161
173
|
|
|
@@ -196,8 +208,8 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
196
208
|
// Keep a legacy per-version title until an explicit retitle unifies the feature identity.
|
|
197
209
|
if (!existing) putItem(db, version, item);
|
|
198
210
|
else {
|
|
199
|
-
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
200
|
-
.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);
|
|
211
|
+
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
212
|
+
.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.updatedAt, item.updatedBy);
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
for (const entry of data.history) record(db, version, entry);
|
|
@@ -224,12 +236,12 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
224
236
|
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 });
|
|
225
237
|
continue;
|
|
226
238
|
}
|
|
227
|
-
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind'] as const;
|
|
239
|
+
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion'] as const;
|
|
228
240
|
const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
|
|
229
241
|
const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
|
|
230
242
|
const differs = fields.some((field) => field === 'evidence'
|
|
231
243
|
? (!preserveRefs && current.evidence !== item.evidence)
|
|
232
|
-
: current[field] !== item[field]);
|
|
244
|
+
: JSON.stringify(current[field]) !== JSON.stringify(item[field]));
|
|
233
245
|
if (!differs) continue;
|
|
234
246
|
if (!(Date.parse(item.updatedAt) > Date.parse(current.updatedAt))) {
|
|
235
247
|
ledgerWonIds.push(item.id);
|
|
@@ -11,6 +11,18 @@ export interface LandedButYellowRow {
|
|
|
11
11
|
newer: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function evidencePlan(rows: readonly LandedButYellowRow[]): Array<{ id: string; ref: string }> {
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
return rows.flatMap((row) => row.prs.flatMap((pr, index) => {
|
|
17
|
+
if (pr.basis === 'mention' || row.alreadyInEvidence[index]) return [];
|
|
18
|
+
const ref = `#${pr.number}`;
|
|
19
|
+
const key = JSON.stringify([row.id, ref]);
|
|
20
|
+
if (seen.has(key)) return [];
|
|
21
|
+
seen.add(key);
|
|
22
|
+
return [{ id: row.id, ref }];
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/** Prefer a dedicated body cell line over title evidence; incidental mentions only qualify for long ids. */
|
|
15
27
|
export function landedButYellow(items: readonly ChecklistItem[], prs: readonly MergedChecklistPr[], { owner }: { owner?: string } = {}): LandedButYellowRow[] {
|
|
16
28
|
const rows: LandedButYellowRow[] = [];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
5
|
+
|
|
6
|
+
export interface LandingFreeze {
|
|
7
|
+
reason: string;
|
|
8
|
+
startedAt: string;
|
|
9
|
+
until: string | null;
|
|
10
|
+
by: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function landingFreezePath(root = effectiveInstanceRoot()): string {
|
|
14
|
+
return join(root, 'landing-freeze.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readLandingFreeze(root = effectiveInstanceRoot(), now = new Date()): LandingFreeze | null {
|
|
18
|
+
const path = landingFreezePath(root);
|
|
19
|
+
let text: string;
|
|
20
|
+
// Read without an existence pre-check: a concurrent `freeze off` between the two would surface as ENOENT.
|
|
21
|
+
try { text = readFileSync(path, 'utf8'); }
|
|
22
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error; }
|
|
23
|
+
const record: unknown = JSON.parse(text);
|
|
24
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) throw new Error(`invalid landing freeze: ${path}`);
|
|
25
|
+
const freeze = record as Partial<LandingFreeze>;
|
|
26
|
+
if (typeof freeze.reason !== 'string' || typeof freeze.by !== 'string' ||
|
|
27
|
+
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
|
+
throw new Error(`invalid landing freeze: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
// An expired freeze reads as «off». The file is left alone: deleting it here could race a concurrent `freeze on`
|
|
32
|
+
// that just wrote a new one (only `freeze off` removes it; the next `freeze on` overwrites it).
|
|
33
|
+
if (freeze.until && Date.parse(freeze.until) <= now.getTime()) return null;
|
|
34
|
+
return freeze as LandingFreeze;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
|
|
38
|
+
const until = opts.until === undefined ? null : opts.until;
|
|
39
|
+
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
|
+
throw new Error('--until must be a future ISO timestamp with timezone');
|
|
41
|
+
}
|
|
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' };
|
|
43
|
+
const path = landingFreezePath(root);
|
|
44
|
+
mkdirSync(root, { recursive: true });
|
|
45
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(tmp, `${JSON.stringify(freeze, null, 2)}\n`, { mode: 0o600 });
|
|
48
|
+
renameSync(tmp, path);
|
|
49
|
+
} finally { rmSync(tmp, { force: true }); }
|
|
50
|
+
return freeze;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function disableLandingFreeze(root = effectiveInstanceRoot()): void {
|
|
54
|
+
rmSync(landingFreezePath(root), { force: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** In-flight merge markers: a merger writes its marker «before» reading the freeze file, and `freeze on` writes the
|
|
58
|
+
* freeze file «before» counting markers — so either the merger sees the freeze or the freeze waits for the merger. */
|
|
59
|
+
function mergesDir(root: string): string { return join(root, 'landing-merges'); }
|
|
60
|
+
|
|
61
|
+
export interface LandingMergeGuard { frozen: LandingFreeze | null; end: () => void }
|
|
62
|
+
|
|
63
|
+
export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date()): LandingMergeGuard {
|
|
64
|
+
const dir = mergesDir(root);
|
|
65
|
+
mkdirSync(dir, { recursive: true });
|
|
66
|
+
const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
|
|
67
|
+
writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
|
|
68
|
+
const end = () => { rmSync(marker, { force: true }); };
|
|
69
|
+
let frozen: LandingFreeze | null;
|
|
70
|
+
try { frozen = readLandingFreeze(root, now); } catch (error) { end(); throw error; }
|
|
71
|
+
if (frozen) { end(); return { frozen, end: () => {} }; }
|
|
72
|
+
return { frozen: null, end };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Live in-flight merges; markers of dead processes are removed. */
|
|
76
|
+
export function inFlightLandingMerges(root = effectiveInstanceRoot()): number {
|
|
77
|
+
const dir = mergesDir(root);
|
|
78
|
+
let names: string[];
|
|
79
|
+
try { names = readdirSync(dir); }
|
|
80
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 0; throw error; }
|
|
81
|
+
let live = 0;
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const pid = Number(name.split('-')[0]);
|
|
84
|
+
let alive = false;
|
|
85
|
+
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
86
|
+
try { process.kill(pid, 0); alive = true; } catch (error) { alive = (error as NodeJS.ErrnoException).code === 'EPERM'; }
|
|
87
|
+
}
|
|
88
|
+
if (alive) live++; else rmSync(join(dir, name), { force: true });
|
|
89
|
+
}
|
|
90
|
+
return live;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `freeze on` completes only after merges that passed their freeze check have finished (or the wait runs out). */
|
|
94
|
+
export async function awaitLandingMergesDrained(
|
|
95
|
+
root = effectiveInstanceRoot(),
|
|
96
|
+
opts: { timeoutMs?: number; pollMs?: number; sleep?: (ms: number) => Promise<void> } = {},
|
|
97
|
+
): Promise<{ drained: boolean; pending: number }> {
|
|
98
|
+
const timeoutMs = opts.timeoutMs ?? 600_000, pollMs = opts.pollMs ?? 1_000;
|
|
99
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const pending = inFlightLandingMerges(root);
|
|
103
|
+
if (pending === 0) return { drained: true, pending: 0 };
|
|
104
|
+
if (Date.now() >= deadline) return { drained: false, pending };
|
|
105
|
+
await sleep(pollMs);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Thrown where a release run stops for a freeze, so the scheduled `--if-ready` path can defer instead of failing. */
|
|
110
|
+
export class LandingFrozenError extends Error {
|
|
111
|
+
constructor(readonly freeze: LandingFreeze) { super(landingFreezeMessage(freeze)); this.name = 'LandingFrozenError'; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const FREEZE_MESSAGE_HEAD = '동결 중 · ';
|
|
115
|
+
|
|
116
|
+
/** True when a node's recorded output or error is the freeze refusal written by {@link landingFreezeMessage}. */
|
|
117
|
+
export function isLandingFreezeRefusal(text: string): boolean { return text.includes(FREEZE_MESSAGE_HEAD); }
|
|
118
|
+
|
|
119
|
+
export function landingFreezeMessage(freeze: LandingFreeze): string {
|
|
120
|
+
return `${FREEZE_MESSAGE_HEAD}${freeze.reason} · 끝 시각 ${freeze.until ?? '미지정'}`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CliUserError } from '../cli/cli-user-error.js';
|
|
5
|
+
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { userConfigPath } from '../user-config.js';
|
|
8
|
+
import { addItem, listChecklist, ownerMatches, parseOwner, setItem, type ChecklistItem } from './checklist.js';
|
|
9
|
+
import { move, releasedVersion } from './feature-store.js';
|
|
10
|
+
import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
|
|
11
|
+
|
|
12
|
+
export type PlacementPriority = 'P0' | 'P1' | 'P2';
|
|
13
|
+
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string }
|
|
14
|
+
export interface PlacementDecision { id: string; from: string | null; version: string; reason: string; displaced: Array<{ id: string; from: string; to: string; reason: string }> }
|
|
15
|
+
export interface PlacementDeps {
|
|
16
|
+
now?: Date;
|
|
17
|
+
schedules?: ReleaseSchedule[];
|
|
18
|
+
merged24h?: number;
|
|
19
|
+
seatCap?: Record<string, number>;
|
|
20
|
+
checklist?: typeof listChecklist;
|
|
21
|
+
released?: string;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
by?: string;
|
|
24
|
+
/** Backlog releases are a source of cells, never a placement target (default {@link BACKLOG_VERSIONS}). */
|
|
25
|
+
backlog?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 0.9.0 is the backlog bucket: cells are pulled out of it, never placed into it. */
|
|
29
|
+
export const BACKLOG_VERSIONS: readonly string[] = ['0.9.0'];
|
|
30
|
+
|
|
31
|
+
function versionOrder(a: string, b: string): number {
|
|
32
|
+
const left = a.split('.').map(Number), right = b.split('.').map(Number);
|
|
33
|
+
for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i]! - right[i]!;
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The merged PR list is a first-class GitHub query, not a count inferred from checklist status. */
|
|
38
|
+
export function mergedPrsLast24h(now = new Date()): number {
|
|
39
|
+
const since = new Date(now.getTime() - 86_400_000).toISOString();
|
|
40
|
+
const r = spawnSync('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since.slice(0, 10)}`, '--limit', '1000', '--json', 'number,mergedAt'], { encoding: 'utf8' });
|
|
41
|
+
if (r.status !== 0) throw new CliUserError(`병합 PR 속도 조회 실패: ${(r.stderr || r.error || '').toString().trim()}`);
|
|
42
|
+
let rows: Array<{ number: number; mergedAt: string }>;
|
|
43
|
+
try { rows = JSON.parse(r.stdout) as typeof rows; }
|
|
44
|
+
catch { throw new CliUserError('병합 PR 속도 응답이 JSON 이 아니다'); }
|
|
45
|
+
if (!Array.isArray(rows) || rows.length >= 1000 || rows.some((row) => typeof row.number !== 'number' || typeof row.mergedAt !== 'string' || !Number.isFinite(Date.parse(row.mergedAt))))
|
|
46
|
+
throw new CliUserError('병합 PR 속도 조회가 잘렸거나 잘못됐다');
|
|
47
|
+
return rows.filter((row) => Date.parse(row.mergedAt) >= Date.parse(since) && Date.parse(row.mergedAt) <= now.getTime()).length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function placementSeatCap(): Record<string, number> {
|
|
51
|
+
const path = getElanousConfigDirOverride() ? join(effectiveInstanceRoot(), 'config.json') : userConfigPath();
|
|
52
|
+
if (!existsSync(path)) return {};
|
|
53
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as { release?: { placement?: { seatCap?: Record<string, number> } } };
|
|
54
|
+
return raw.release?.placement?.seatCap ?? {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dependencyViolation(
|
|
58
|
+
snapshots: Map<string, { items: ChecklistItem[] }>,
|
|
59
|
+
locations: Map<string, string>,
|
|
60
|
+
overrides: Map<string, string>,
|
|
61
|
+
input: PlacementCell,
|
|
62
|
+
): string | null {
|
|
63
|
+
for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
|
|
64
|
+
const dependentVersion = overrides.get(item.id) ?? version;
|
|
65
|
+
const predecessors = item.id === input.id ? input.predecessors : item.predecessors ?? [];
|
|
66
|
+
for (const predecessor of predecessors) {
|
|
67
|
+
const predecessorVersion = overrides.get(predecessor) ?? locations.get(predecessor);
|
|
68
|
+
if (!predecessorVersion || versionOrder(dependentVersion, predecessorVersion) <= 0)
|
|
69
|
+
return `의존 칸 ${item.id}은 선행 칸 ${predecessor} 이후 판이어야 한다`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!locations.has(input.id)) for (const predecessor of input.predecessors) {
|
|
73
|
+
const predecessorVersion = overrides.get(predecessor) ?? locations.get(predecessor);
|
|
74
|
+
const dependentVersion = overrides.get(input.id);
|
|
75
|
+
if (!predecessorVersion || !dependentVersion || versionOrder(dependentVersion, predecessorVersion) <= 0)
|
|
76
|
+
return `의존 칸 ${input.id}은 선행 칸 ${predecessor} 이후 판이어야 한다`;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A freeze window holds operational rollout only; the release still exists and takes cells (OP 10-04 19:39). */
|
|
82
|
+
function available(row: ReleaseSchedule, now: number, backlog: readonly string[]): boolean {
|
|
83
|
+
if (backlog.includes(row.version)) return false;
|
|
84
|
+
return Date.parse(row.landBy ?? row.cutAt) > now;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): PlacementDecision {
|
|
88
|
+
if (!input.id.trim() || !input.title.trim()) throw new CliUserError('칸 id 와 제목이 필요하다');
|
|
89
|
+
if (!Array.isArray(input.predecessors) || input.predecessors.some((id) => typeof id !== 'string' || !id.trim() || id === input.id)) throw new CliUserError('선행 칸 id 가 잘못됐다');
|
|
90
|
+
const seat = parseOwner(input.owner).seat;
|
|
91
|
+
if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
92
|
+
const now = (deps.now ?? new Date()).getTime();
|
|
93
|
+
const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
94
|
+
const released = deps.released ?? releasedVersion();
|
|
95
|
+
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
96
|
+
const open = schedules.filter((row) => (!released || versionOrder(row.version, released) > 0) && row.landBy && available(row, now, backlog));
|
|
97
|
+
const sources = [...new Set([...schedules.map((row) => row.version), ...backlog])];
|
|
98
|
+
const snapshots = new Map(sources.map((version) => [version, (deps.checklist ?? listChecklist)(version)]));
|
|
99
|
+
const locations = new Map<string, string>();
|
|
100
|
+
for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
|
|
101
|
+
if (backlog.includes(version)) continue;
|
|
102
|
+
if (locations.has(item.id)) throw new CliUserError(`여러 판의 같은 칸: ${item.id}`);
|
|
103
|
+
locations.set(item.id, version);
|
|
104
|
+
}
|
|
105
|
+
// A backlog copy never outranks a scheduled cell with the same id.
|
|
106
|
+
for (const version of backlog) for (const item of snapshots.get(version)?.items ?? []) if (!locations.has(item.id)) locations.set(item.id, version);
|
|
107
|
+
const from = locations.get(input.id) ?? null;
|
|
108
|
+
const existing = from ? snapshots.get(from)!.items.find((item) => item.id === input.id)! : null;
|
|
109
|
+
if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
|
|
110
|
+
if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
|
|
111
|
+
if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
|
|
112
|
+
const predecessors = input.predecessors.map((id) => {
|
|
113
|
+
const version = locations.get(id);
|
|
114
|
+
if (!version) throw new CliUserError(`없는 선행 칸: ${id}`);
|
|
115
|
+
if (backlog.includes(version)) throw new CliUserError(`선행 칸 ${id} 이 백로그 ${version} 에 있다 — 먼저 배치하라`);
|
|
116
|
+
return version;
|
|
117
|
+
});
|
|
118
|
+
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
119
|
+
if (!Number.isInteger(rate) || rate < 0) throw new CliUserError('24시간 병합 PR 수가 잘못됐다');
|
|
120
|
+
const caps = deps.seatCap ?? placementSeatCap();
|
|
121
|
+
for (const [configuredSeat, limit] of Object.entries(caps)) if (!Number.isInteger(limit) || limit < 0) throw new CliUserError(`잘못된 자리 용량: ${configuredSeat}`);
|
|
122
|
+
const seatLimit = caps[seat] ?? Infinity;
|
|
123
|
+
const first = open[0];
|
|
124
|
+
const deadlineVersion = input.priority === 'P0' ? undefined : input.deadlineVersion ?? existing?.deadlineVersion;
|
|
125
|
+
if (input.priority === 'P1' && !deadlineVersion) throw new CliUserError(`P1 칸의 마감 판이 없다: ${input.id}`);
|
|
126
|
+
if (input.priority !== 'P0' && deadlineVersion && !schedules.some((row) => row.version === deadlineVersion)) throw new CliUserError(`없는 마감 판: ${deadlineVersion}`);
|
|
127
|
+
const eligible = open.filter((row) => predecessors.every((v) => versionOrder(row.version, v) > 0));
|
|
128
|
+
const candidates = input.priority === 'P0' ? eligible.filter((row) => row.version === first?.version)
|
|
129
|
+
: input.priority === 'P1' ? eligible.filter((row) => row.version === deadlineVersion)
|
|
130
|
+
: eligible.filter((row) => !deadlineVersion || versionOrder(row.version, deadlineVersion) <= 0);
|
|
131
|
+
|
|
132
|
+
const occupied = (version: string) => snapshots.get(version)!.items.filter((item) => item.status !== 'done' && item.id !== input.id);
|
|
133
|
+
const capacity = (row: ReleaseSchedule) => Math.floor(rate * (Date.parse(row.landBy!) - now) / 86_400_000 * 0.7);
|
|
134
|
+
const displaced: PlacementDecision['displaced'] = [];
|
|
135
|
+
let chosen: ReleaseSchedule | undefined;
|
|
136
|
+
for (const row of candidates) {
|
|
137
|
+
const placement = new Map([[input.id, row.version]]);
|
|
138
|
+
if (dependencyViolation(snapshots, locations, placement, input)) continue;
|
|
139
|
+
const items = occupied(row.version);
|
|
140
|
+
const seatItems = items.filter((item) => ownerMatches(item.owner, seat));
|
|
141
|
+
if (items.length < capacity(row) && seatItems.length < seatLimit) { chosen = row; break; }
|
|
142
|
+
if (input.priority !== 'P1' || from === row.version) continue;
|
|
143
|
+
const next = open.find((later) => versionOrder(later.version, row.version) > 0);
|
|
144
|
+
if (!next || items.length - 1 >= capacity(row)) continue;
|
|
145
|
+
const p2 = [...items].reverse().find((item) => {
|
|
146
|
+
if (item.priority !== 'P2' || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
|
|
147
|
+
if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
|
|
148
|
+
if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
|
|
149
|
+
if (!item.owner) return false;
|
|
150
|
+
const shiftedSeat = parseOwner(item.owner).seat;
|
|
151
|
+
return occupied(next.version).length < capacity(next) && occupied(next.version).filter((other) => ownerMatches(other.owner, shiftedSeat)).length < (caps[shiftedSeat] ?? Infinity);
|
|
152
|
+
});
|
|
153
|
+
if (p2 && (seatItems.length < seatLimit || (ownerMatches(p2.owner, seat) && seatItems.length - 1 < seatLimit))) {
|
|
154
|
+
displaced.push({ id: p2.id, from: row.version, to: next.version, reason: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
|
|
155
|
+
chosen = row;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!chosen) throw new CliUserError(`${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
|
|
160
|
+
const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
161
|
+
const decision = { id: input.id, from, version: chosen.version, reason, displaced };
|
|
162
|
+
if (!deps.dryRun) {
|
|
163
|
+
const by = deps.by ?? 'OP';
|
|
164
|
+
for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
|
|
165
|
+
if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
|
|
166
|
+
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion)))
|
|
167
|
+
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}) }, by);
|
|
168
|
+
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion });
|
|
169
|
+
}
|
|
170
|
+
return decision;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function moveConstraint(
|
|
174
|
+
item: ChecklistItem, to: ReleaseSchedule, backlog: readonly string[],
|
|
175
|
+
snapshots: Map<string, ChecklistItem[]>, now: number, rate: number, caps: Record<string, number>,
|
|
176
|
+
): string | null {
|
|
177
|
+
if (!to.landBy || !available(to, now, backlog)) return '착지 마감이 지났거나 백로그 판이라 이동 불가';
|
|
178
|
+
if (item.deadlineVersion && versionOrder(to.version, item.deadlineVersion) > 0) return '마감 판 뒤로 이월 불가';
|
|
179
|
+
if (!item.owner) return '담당 없는 칸은 이동 불가';
|
|
180
|
+
const seat = parseOwner(item.owner).seat;
|
|
181
|
+
const destination = snapshots.get(to.version) ?? [];
|
|
182
|
+
if (destination.filter((cell) => cell.status !== 'done').length >= Math.floor(rate * (Date.parse(to.landBy) - now) / 86_400_000 * 0.7)) return '다음 판 PR 용량 초과';
|
|
183
|
+
if (destination.filter((cell) => cell.status !== 'done' && ownerMatches(cell.owner, seat)).length >= (caps[seat] ?? Infinity)) return '다음 판 자리 용량 초과';
|
|
184
|
+
const locations = new Map<string, string>();
|
|
185
|
+
for (const [version, items] of snapshots) for (const cell of items) {
|
|
186
|
+
if (locations.has(cell.id)) return `여러 판의 같은 칸: ${cell.id}`;
|
|
187
|
+
locations.set(cell.id, version);
|
|
188
|
+
}
|
|
189
|
+
for (const predecessor of item.predecessors ?? []) {
|
|
190
|
+
const location = locations.get(predecessor);
|
|
191
|
+
if (!location || versionOrder(to.version, location) <= 0) return `선행 칸 ${predecessor} 이후 판으로만 이동 가능`;
|
|
192
|
+
}
|
|
193
|
+
for (const [version, items] of snapshots) for (const dependent of items) {
|
|
194
|
+
if (dependent.id !== item.id && dependent.predecessors?.includes(item.id) && versionOrder(version, to.version) <= 0)
|
|
195
|
+
return `의존 칸 ${dependent.id} 이전 판으로만 이동 가능`;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Only unstarted cells inside the two-hour pre-deadline window move; a dry run never writes. */
|
|
201
|
+
export function rebalance(version: string, deps: PlacementDeps = {}): PlacementDecision[] {
|
|
202
|
+
const rows = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
203
|
+
const current = rows.find((row) => row.version === version);
|
|
204
|
+
if (!current?.landBy) throw new CliUserError(`착지 마감이 없는 판: ${version}`);
|
|
205
|
+
const now = (deps.now ?? new Date()).getTime();
|
|
206
|
+
if (Date.parse(current.landBy) - now > 7_200_000 || Date.parse(current.landBy) <= now) return [];
|
|
207
|
+
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
208
|
+
const next = rows.find((row) => versionOrder(row.version, version) > 0 && !backlog.includes(row.version));
|
|
209
|
+
const snapshots = new Map(rows.map((row) => [row.version, [...(deps.checklist ?? listChecklist)(row.version).items]]));
|
|
210
|
+
const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence);
|
|
211
|
+
if (unstarted.length && !next) throw new CliUserError(`${version} 다음 판이 없다`);
|
|
212
|
+
if (!unstarted.length) return [];
|
|
213
|
+
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
214
|
+
const caps = deps.seatCap ?? placementSeatCap();
|
|
215
|
+
const decisions: PlacementDecision[] = [];
|
|
216
|
+
for (const item of unstarted) {
|
|
217
|
+
if (moveConstraint(item, next!, backlog, snapshots, now, rate, caps)) continue;
|
|
218
|
+
const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월`;
|
|
219
|
+
decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
|
|
220
|
+
snapshots.set(version, snapshots.get(version)!.filter((cell) => cell.id !== item.id));
|
|
221
|
+
snapshots.get(next!.version)!.push(item);
|
|
222
|
+
}
|
|
223
|
+
if (!deps.dryRun) for (const decision of decisions) move(decision.id, version, decision.version, deps.by ?? 'OP', undefined, undefined, decision.reason);
|
|
224
|
+
return decisions;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A seat may defer its own cell one release, never pull another owner's cell forward. */
|
|
228
|
+
export function seatMove(id: string, from: string, to: string, by: string, reason: string, deps: PlacementDeps = {}): void {
|
|
229
|
+
if (!reason.trim()) throw new CliUserError('이동 이유가 필요하다');
|
|
230
|
+
const rows = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
231
|
+
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
232
|
+
const next = rows.find((row) => versionOrder(row.version, from) > 0 && !backlog.includes(row.version));
|
|
233
|
+
const snapshots = new Map(rows.map((row) => [row.version, (deps.checklist ?? listChecklist)(row.version).items]));
|
|
234
|
+
const item = snapshots.get(from)?.find((cell) => cell.id === id);
|
|
235
|
+
if (!item) throw new CliUserError(`없는 칸: ${id}`);
|
|
236
|
+
if (!ownerMatches(item.owner, parseOwner(by).seat) || next?.version !== to) throw new CliUserError('남의 칸 당기기 거부 — COO 에 요청');
|
|
237
|
+
const now = (deps.now ?? new Date()).getTime();
|
|
238
|
+
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
239
|
+
const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap());
|
|
240
|
+
if (violation) throw new CliUserError(`${id}: ${violation}`);
|
|
241
|
+
if (!deps.dryRun) move(id, from, to, by, undefined, undefined, reason);
|
|
242
|
+
}
|
|
@@ -35,10 +35,12 @@ export function listSchedules(root?: string): ReleaseSchedule[] {
|
|
|
35
35
|
return schedules;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string }, by: string, root?: string): ReleaseSchedule {
|
|
38
|
+
export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ReleaseSchedule {
|
|
39
39
|
const schedule = writeSchedule(version, {
|
|
40
40
|
...(patch.cutAt !== undefined ? { cutAt: utcIso(patch.cutAt) } : {}),
|
|
41
41
|
...(patch.landBy !== undefined ? { landBy: utcIso(patch.landBy) } : {}),
|
|
42
|
+
...(patch.freezeFrom !== undefined ? { freezeFrom: utcIso(patch.freezeFrom) } : {}),
|
|
43
|
+
...(patch.freezeUntil !== undefined ? { freezeUntil: utcIso(patch.freezeUntil) } : {}),
|
|
42
44
|
}, by, root);
|
|
43
45
|
debug.log('release.schedule', 'set', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
|
|
44
46
|
return schedule;
|