elanous 0.2.13 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.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/{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-c952712c87a1f757.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/8060-cc008cc6fedfa818.js +1 -0
- 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/899-46946fe8363e1262.js +1 -0
- 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-0e2c57fe6add4b2a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b8c49f6b4fa2df91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-60c7a8e1a4fe62ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.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-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-c1343f9499d86f3f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-d23b12bb92242558.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-153c2b3bcc89e8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.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-7d4182964407c09c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9817c7f9007466cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.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/1f7c958c41ffa3b9.css +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/ceo/index.html +1 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- 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/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +6 -0
- package/graphs/release/release-loop.yaml +6 -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 +1 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +188 -0
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/coord-post.sh +11 -1
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +169 -0
- package/scripts/heal/run-role.ts +14 -0
- package/scripts/hq/standby-verify.ts +11 -7
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +112 -0
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +89 -14
- 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 +428 -0
- package/scripts/release-story/draft.ts +220 -0
- package/scripts/rhythm/daily.ts +570 -0
- package/scripts/rhythm/weekly.ts +315 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/scripts/test-deterministic.ts +4 -8
- 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/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-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +3 -3
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +73 -10
- package/src/autopilot/build/merge-intent.ts +53 -0
- package/src/briefing/brief-items.ts +121 -0
- package/src/chat/index.ts +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +166 -0
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +44 -3
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/intake-cli.ts +46 -17
- 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/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +29 -1
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +190 -33
- package/src/cli/rfc-status.ts +91 -0
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +15 -20
- package/src/context-bus/context-now-public.ts +96 -0
- package/src/context-bus/context-now-surfaces.ts +55 -2
- package/src/context-bus/context-now.ts +6 -3
- package/src/context-bus/coord-events.ts +6 -4
- package/src/context-bus/long-term-memory.ts +92 -11
- 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 +33 -7
- package/src/decisions/decision-ledger.ts +40 -1
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- 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/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- 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-runner/graph-tick.ts +4 -0
- package/src/graph-runner/runner.ts +30 -5
- package/src/harness/harness-cli-command.ts +259 -38
- package/src/harness/harness-incidents.ts +45 -12
- package/src/harness/harness-pod-dispatch.ts +102 -11
- package/src/harness/harness-queue-child.ts +3 -3
- package/src/harness/harness-queue.ts +583 -65
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +151 -0
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +63 -5
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +332 -0
- package/src/index.ts +159 -27
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/linear-wish.ts +39 -11
- package/src/intake-plane/route.ts +145 -1
- 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/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -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 +63 -0
- package/src/loops/checker.ts +5 -3
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +36 -4
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +10 -0
- package/src/loops/orchestrator/author-depth.ts +220 -0
- package/src/loops/orchestrator/author-input-check.ts +82 -0
- package/src/loops/orchestrator/author-ledger.ts +232 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +51 -12
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +309 -0
- package/src/loops/orchestrator/traffic.ts +134 -13
- package/src/loops/registry.ts +535 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +17 -5
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +9 -5
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/http-server.ts +33 -3
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/schedules-read.ts +18 -1
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +51 -0
- package/src/release-loop/checklist.ts +22 -4
- package/src/release-loop/feature-store.ts +71 -32
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +302 -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 +16 -3
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +531 -40
- package/src/self-dev/dev-pipeline.ts +14 -6
- package/src/self-dev/draft-sweep.ts +162 -6
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +57 -2
- 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 +295 -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-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +197 -0
- package/src/self-implement/host-regate.ts +32 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +299 -48
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +97 -0
- package/src/self-implement/run-ledger.ts +3 -1
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +29 -9
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session/index.ts +2 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-cards/card-cli.ts +10 -2
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +146 -18
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.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-fe5c06d6b31f8b25.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/5250-c78ca295585bbf95.js +0 -1
- 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/6511-ff8360be3d65aaa4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.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/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.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-9da4678f8a310d18.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/9906-e7b8b5310a169dfe.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-e5b4e1b256f41076.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.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-cb2eb0fd809980da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.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-d92e32d51ab5157e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.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/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -13,6 +13,11 @@ 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;
|
|
19
|
+
ceoMinutes?: number;
|
|
20
|
+
ceoDate?: string;
|
|
16
21
|
evidence?: string;
|
|
17
22
|
disposition?: ChecklistDisposition;
|
|
18
23
|
kind?: ChecklistKind;
|
|
@@ -93,11 +98,20 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
93
98
|
debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
export function
|
|
101
|
+
export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }): void {
|
|
102
|
+
if (input.ceoMinutes !== undefined && (!Number.isSafeInteger(input.ceoMinutes) || input.ceoMinutes < 0)) throw new CliUserError('대표 손 분량은 0 이상의 정수 분이어야 한다');
|
|
103
|
+
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 이어야 한다');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
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 }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
97
107
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
98
108
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
99
109
|
if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
|
|
100
110
|
if (input.owner !== undefined) parseOwner(input.owner);
|
|
111
|
+
if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
112
|
+
if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
|
|
113
|
+
if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
|
|
114
|
+
validateCeoLoad(input);
|
|
101
115
|
return mutate(v, (data, otherItems) => {
|
|
102
116
|
if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
|
|
103
117
|
const collisions = otherItems(input.id);
|
|
@@ -108,27 +122,31 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
108
122
|
}
|
|
109
123
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
110
124
|
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 };
|
|
125
|
+
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 } : {}), updatedAt: at, updatedBy: by };
|
|
112
126
|
data.items.push(item);
|
|
113
127
|
change(data, item.id, 'add', null, item, by, at);
|
|
114
128
|
return true;
|
|
115
129
|
});
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind }, by: string): Checklist {
|
|
132
|
+
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 }, by: string): Checklist {
|
|
119
133
|
return mutate(v, (data) => {
|
|
120
134
|
const item = data.items.find((i) => i.id === id);
|
|
121
135
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
122
136
|
if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
|
|
123
137
|
if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
|
|
124
138
|
if (patch.kind !== undefined && patch.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen');
|
|
139
|
+
if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
|
|
140
|
+
if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
|
|
141
|
+
if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
|
|
142
|
+
validateCeoLoad(patch);
|
|
125
143
|
if (patch.owner !== undefined) {
|
|
126
144
|
parseOwner(patch.owner);
|
|
127
145
|
if (item.owner && patch.owner !== item.owner) {
|
|
128
146
|
throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
|
-
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
|
|
149
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
|
|
132
150
|
if (fields.length === 0) return false;
|
|
133
151
|
const at = new Date().toISOString();
|
|
134
152
|
for (const field of fields) {
|
|
@@ -30,6 +30,18 @@ export function releasedVersion(ledgerRoot = releaseLedgerRoot()): string {
|
|
|
30
30
|
})[0] ?? '';
|
|
31
31
|
}
|
|
32
32
|
const walWait = new Int32Array(new SharedArrayBuffer(4));
|
|
33
|
+
const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
|
|
34
|
+
imported_versions: ['json_hash', 'imported_at'],
|
|
35
|
+
events: ['reason'],
|
|
36
|
+
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date'],
|
|
37
|
+
release_schedules: ['freeze_from', 'freeze_until'],
|
|
38
|
+
};
|
|
39
|
+
function schemaCurrent(db: Database): boolean {
|
|
40
|
+
return Object.entries(SCHEMA_COLUMNS).every(([table, names]) => {
|
|
41
|
+
const columns = new Set((db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map((column) => column.name));
|
|
42
|
+
return names.every((name) => columns.has(name));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
33
45
|
function open(root = releaseLedgerRoot()): Database {
|
|
34
46
|
const path = join(root, 'release', 'features.sqlite');
|
|
35
47
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -48,24 +60,35 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
db.exec('PRAGMA foreign_keys = ON');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
// Two processes opening a fresh ledger both saw a missing column and both ran ALTER TABLE ("duplicate column name").
|
|
64
|
+
// BEGIN IMMEDIATE serializes the schema check and the migration, so the second writer re-reads the migrated schema.
|
|
65
|
+
// An up-to-date ledger skips the write lock so readers don't queue behind each other.
|
|
66
|
+
if (!schemaCurrent(db)) db.transaction(() => {
|
|
67
|
+
if (!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'imported_versions'").get()) db.exec(`CREATE TABLE IF NOT EXISTS features (id TEXT PRIMARY KEY, title TEXT NOT NULL, owner TEXT, kind TEXT, created_at TEXT NOT NULL);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS assignments (feature_id TEXT NOT NULL REFERENCES features(id), version TEXT NOT NULL, status TEXT NOT NULL, disposition TEXT, evidence TEXT, title_override TEXT, owner TEXT, kind TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL, PRIMARY KEY(feature_id, version));
|
|
69
|
+
CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, by TEXT NOT NULL, feature_id TEXT NOT NULL, version TEXT NOT NULL, field TEXT NOT NULL, "from" TEXT, "to" TEXT, released TEXT NOT NULL, dev TEXT NOT NULL, reason TEXT);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS evidence (feature_id TEXT NOT NULL, version TEXT NOT NULL, ref TEXT NOT NULL, at TEXT NOT NULL, by TEXT NOT NULL);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS imported_versions (version TEXT PRIMARY KEY, json_hash TEXT, imported_at TEXT);`);
|
|
72
|
+
const columns = db.query('PRAGMA table_info(imported_versions)').all() as Array<{ name: string }>;
|
|
73
|
+
if (!columns.some((column) => column.name === 'json_hash')) db.exec('ALTER TABLE imported_versions ADD COLUMN json_hash TEXT');
|
|
74
|
+
if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
|
|
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
|
+
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
|
+
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']] as const) {
|
|
79
|
+
if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
|
|
80
|
+
}
|
|
81
|
+
const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
|
|
82
|
+
for (const name of ['freeze_from', 'freeze_until']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
|
|
83
|
+
}).immediate();
|
|
61
84
|
return db;
|
|
62
85
|
} catch (error) { db.close(); throw error; }
|
|
63
86
|
}
|
|
64
87
|
|
|
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 };
|
|
88
|
+
export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; updatedAt: string; updatedBy: string }
|
|
89
|
+
type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; updated_at: string; updated_by: string };
|
|
67
90
|
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 };
|
|
91
|
+
return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
export function readSchedule(version: string, root?: string): ScheduleRow | null {
|
|
@@ -84,7 +107,15 @@ export function readSchedules(root?: string): ScheduleRow[] {
|
|
|
84
107
|
finally { db.close(); }
|
|
85
108
|
}
|
|
86
109
|
|
|
87
|
-
|
|
110
|
+
/** Versions with checklist assignments, including SQLite-only releases without a schedule or legacy JSON. */
|
|
111
|
+
export function assignedVersions(root = releaseLedgerRoot()): string[] {
|
|
112
|
+
if (!existsSync(join(root, 'release', 'features.sqlite'))) return [];
|
|
113
|
+
const db = open(root);
|
|
114
|
+
try { return (db.query('SELECT DISTINCT version FROM assignments ORDER BY version').all() as Array<{ version: string }>).map(({ version }) => version); }
|
|
115
|
+
finally { db.close(); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ScheduleRow {
|
|
88
119
|
validateVersion(version);
|
|
89
120
|
const db = open(root);
|
|
90
121
|
try {
|
|
@@ -93,13 +124,17 @@ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?:
|
|
|
93
124
|
const cutAt = patch.cutAt ?? previous?.cut_at;
|
|
94
125
|
if (!cutAt) throw new CliUserError('새 판에는 --cut-at 이 필요하다');
|
|
95
126
|
const landBy = patch.landBy ?? previous?.land_by ?? null;
|
|
96
|
-
|
|
127
|
+
const freezeFrom = patch.freezeFrom ?? previous?.freeze_from ?? null;
|
|
128
|
+
const freezeUntil = patch.freezeUntil ?? previous?.freeze_until ?? null;
|
|
129
|
+
if (Boolean(freezeFrom) !== Boolean(freezeUntil) || (freezeFrom && freezeUntil && Date.parse(freezeFrom) >= Date.parse(freezeUntil))) throw new CliUserError('동결 시작·끝은 함께 주고 시작이 끝보다 앞서야 한다');
|
|
130
|
+
if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil) return scheduleRow(previous);
|
|
97
131
|
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);
|
|
132
|
+
db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
133
|
+
ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
134
|
+
.run(version, cutAt, landBy, freezeFrom, freezeUntil, at, by);
|
|
101
135
|
for (const [field, from, to] of [
|
|
102
136
|
['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
|
|
137
|
+
['freeze_from', previous?.freeze_from ?? null, freezeFrom], ['freeze_until', previous?.freeze_until ?? null, freezeUntil],
|
|
103
138
|
] as const) {
|
|
104
139
|
if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
|
|
105
140
|
}
|
|
@@ -134,14 +169,17 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
|
134
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);
|
|
135
170
|
}
|
|
136
171
|
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
|
|
138
|
-
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>>;
|
|
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.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
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>>;
|
|
139
174
|
return rows.map((r) => {
|
|
140
|
-
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id
|
|
141
|
-
return { id: r.id
|
|
142
|
-
...(r.owner !== null ? { owner: r.owner
|
|
175
|
+
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
|
|
176
|
+
return { id: r.id as string, title: r.title as string, status: r.status as ChecklistItem['status'],
|
|
177
|
+
...(r.owner !== null ? { owner: r.owner as string } : {}), ...(r.kind !== null ? { kind: r.kind as ChecklistItem['kind'] } : {}),
|
|
178
|
+
...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors as string) as string[] } : {}),
|
|
179
|
+
...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
|
|
180
|
+
...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
|
|
143
181
|
...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
|
|
144
|
-
updatedAt: r.updated_at
|
|
182
|
+
updatedAt: r.updated_at as string, updatedBy: r.updated_by as string };
|
|
145
183
|
});
|
|
146
184
|
}
|
|
147
185
|
function snapshot(db: Database, version: string, released: string, dev: string): Checklist {
|
|
@@ -153,9 +191,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
|
|
|
153
191
|
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
192
|
// features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
|
|
155
193
|
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);
|
|
194
|
+
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
195
|
+
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, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
196
|
+
.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.updatedAt, item.updatedBy);
|
|
159
197
|
}
|
|
160
198
|
function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
|
|
161
199
|
|
|
@@ -196,8 +234,8 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
196
234
|
// Keep a legacy per-version title until an explicit retitle unifies the feature identity.
|
|
197
235
|
if (!existing) putItem(db, version, item);
|
|
198
236
|
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);
|
|
237
|
+
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, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
238
|
+
.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.updatedAt, item.updatedBy);
|
|
201
239
|
}
|
|
202
240
|
}
|
|
203
241
|
for (const entry of data.history) record(db, version, entry);
|
|
@@ -224,12 +262,12 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
224
262
|
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
263
|
continue;
|
|
226
264
|
}
|
|
227
|
-
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind'] as const;
|
|
265
|
+
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const;
|
|
228
266
|
const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
|
|
229
267
|
const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
|
|
230
268
|
const differs = fields.some((field) => field === 'evidence'
|
|
231
269
|
? (!preserveRefs && current.evidence !== item.evidence)
|
|
232
|
-
: current[field] !== item[field]);
|
|
270
|
+
: JSON.stringify(current[field]) !== JSON.stringify(item[field]));
|
|
233
271
|
if (!differs) continue;
|
|
234
272
|
if (!(Date.parse(item.updatedAt) > Date.parse(current.updatedAt))) {
|
|
235
273
|
ledgerWonIds.push(item.id);
|
|
@@ -369,7 +407,7 @@ export function remove(version: string, id: string, by: string, released = relea
|
|
|
369
407
|
return true;
|
|
370
408
|
});
|
|
371
409
|
}
|
|
372
|
-
export function move(id: string, from: string, to: string, by: string, released = releasedVersion(), dev = devVersion(), reason?: string): Checklist {
|
|
410
|
+
export function move(id: string, from: string, to: string, by: string, released = releasedVersion(), dev = devVersion(), reason?: string, options: { clearDisposition?: boolean } = {}): Checklist {
|
|
373
411
|
validateVersion(from); validateVersion(to);
|
|
374
412
|
if (from === to) throw new CliUserError('같은 판으로 옮길 수 없다');
|
|
375
413
|
const db = open();
|
|
@@ -383,6 +421,7 @@ export function move(id: string, from: string, to: string, by: string, released
|
|
|
383
421
|
if (collision) throw new CliUserError(`이미 있는 칸: ${JSON.stringify(id).slice(1, -1)} — ${to} · 담당 ${JSON.stringify(collision.owner ?? '-').slice(1, -1)} · ${collision.title.replace(/\s+/g, ' ').slice(0, 40)}`);
|
|
384
422
|
const at = new Date().toISOString();
|
|
385
423
|
db.query('UPDATE assignments SET version = ?, updated_at = ?, updated_by = ? WHERE feature_id = ? AND version = ?').run(to, at, by, id, from);
|
|
424
|
+
if (options.clearDisposition) db.query('UPDATE assignments SET disposition = NULL WHERE feature_id = ? AND version = ?').run(id, to);
|
|
386
425
|
db.query('UPDATE evidence SET version = ? WHERE feature_id = ? AND version = ?').run(to, id, from);
|
|
387
426
|
record(db, to, { at, by, id, field: 'move', from, to, released, dev, ...(reason !== undefined ? { reason } : {}) });
|
|
388
427
|
return snapshot(db, to, released, dev);
|
|
@@ -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,125 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { listChecklist, setItem } from './checklist.js';
|
|
7
|
+
import { evidencePlan, landedButYellow, type MergedChecklistPr } from './landed-but-yellow.js';
|
|
8
|
+
import { assignedVersions, evidenceAdd } from './feature-store.js';
|
|
9
|
+
|
|
10
|
+
export interface MergedPrChecklistDeps {
|
|
11
|
+
readPr?: (number: number, cwd: string) => { state: string; baseRefName: string; title: string; body: string | null; mergedAt: string };
|
|
12
|
+
versions?: () => string[];
|
|
13
|
+
checklist?: typeof listChecklist;
|
|
14
|
+
addEvidence?: typeof evidenceAdd;
|
|
15
|
+
setStatus?: typeof setItem;
|
|
16
|
+
log?: typeof debug.log;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A confirmed main merge is the only entry. The existing checklist matcher owns PR-to-cell decisions. */
|
|
20
|
+
export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: string, deps: MergedPrChecklistDeps = {}): void {
|
|
21
|
+
const log = deps.log ?? debug.log.bind(debug);
|
|
22
|
+
const pr = (deps.readPr ?? ((n, dir) => JSON.parse(execFileSync('gh', ['pr', 'view', String(n), '--json', 'state,baseRefName,title,body,mergedAt'], { cwd: dir, encoding: 'utf8', timeout: 30_000 })) as ReturnType<NonNullable<MergedPrChecklistDeps['readPr']>>))(number, cwd);
|
|
23
|
+
if (pr.state !== 'MERGED' || pr.baseRefName !== 'main') return;
|
|
24
|
+
const merged: MergedChecklistPr = { number, title: pr.title, body: pr.body, mergedAt: pr.mergedAt };
|
|
25
|
+
const versions = deps.versions?.() ?? (() => {
|
|
26
|
+
let recorded: string[] = [];
|
|
27
|
+
try { recorded = readdirSync(join(releaseLedgerRoot(), 'release')).filter((version) => /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(version)); }
|
|
28
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
29
|
+
return [...new Set([...assignedVersions(), ...recorded])];
|
|
30
|
+
})();
|
|
31
|
+
let goal: string | undefined;
|
|
32
|
+
if (goalFile) {
|
|
33
|
+
try { goal = readFileSync(goalFile, 'utf8'); }
|
|
34
|
+
catch (error) { log('release.checklist', 'merged-pr-goal-unreadable', { pr: number, goalFile, error: String(error) }); }
|
|
35
|
+
}
|
|
36
|
+
// The audit matcher owns explicit-label and evidence decisions on both PR surfaces.
|
|
37
|
+
// The shared matcher only accepts a whole `칸: X` line. A designated label in the body (`칸: X — …` at a line head)
|
|
38
|
+
// or in the title is restated as such a line; prose mentions are not (the audit command's contract is unchanged).
|
|
39
|
+
const designated = [...goalCells(pr.body ?? ''), ...titleCells(pr.title)];
|
|
40
|
+
// Only the restated labels go to the matcher — raw body text (quotes, fences, indented code) never does.
|
|
41
|
+
const prForMatch = { ...merged, title: '', body: designated.map((id) => `칸: ${id}`).join('\n') };
|
|
42
|
+
const matches = [...new Set(versions)].flatMap((version) => {
|
|
43
|
+
const snapshot = (deps.checklist ?? listChecklist)(version);
|
|
44
|
+
// The audit excludes green/done; evidence still applies to already-green cells.
|
|
45
|
+
const candidates = snapshot.items.map((item) => ({ ...item, status: 'yellow' as const }));
|
|
46
|
+
return landedButYellow(candidates, [prForMatch])
|
|
47
|
+
.filter((row) => row.prs.some((p) => p.basis === 'cell-line'))
|
|
48
|
+
.map((row) => ({ version, snapshot, row }));
|
|
49
|
+
});
|
|
50
|
+
const counts = new Map<string, number>();
|
|
51
|
+
for (const { row } of matches) counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
|
|
52
|
+
for (const [id, count] of counts) {
|
|
53
|
+
if (count > 1) log('release.checklist', 'merged-pr-cell-ambiguous', { pr: number, id, versions: matches.filter((match) => match.row.id === id).map((match) => match.version) });
|
|
54
|
+
}
|
|
55
|
+
for (const { version, snapshot, row } of matches) {
|
|
56
|
+
if (counts.get(row.id) !== 1) continue;
|
|
57
|
+
for (const { id, ref } of evidencePlan([row])) {
|
|
58
|
+
(deps.addEvidence ?? evidenceAdd)(id, version, ref, 'harness', snapshot.released, snapshot.dev);
|
|
59
|
+
log('release.checklist', 'merged-pr-evidence-added', { version, id, pr: number, ref });
|
|
60
|
+
}
|
|
61
|
+
if (goal && declaresCompletion(goal)) {
|
|
62
|
+
if (!goalCells(goal).has(row.id)) {
|
|
63
|
+
log('release.checklist', 'merged-pr-status-skipped', { version, id: row.id, pr: number, reason: 'goal-does-not-name-cell' });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const from = snapshot.items.find((item) => item.id === row.id)!.status;
|
|
67
|
+
if (from !== 'green' && from !== 'done') {
|
|
68
|
+
(deps.setStatus ?? setItem)(version, row.id, { status: 'green' }, 'harness');
|
|
69
|
+
log('release.checklist', 'merged-pr-status-green', { version, id: row.id, pr: number, from, to: 'green' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const found = new Set(matches.map(({ row }) => row.id));
|
|
74
|
+
const missing = [...new Set(designated)].filter((id) => !found.has(id));
|
|
75
|
+
if (!designated.length) log('release.checklist', 'merged-pr-cell-not-found', { pr: number });
|
|
76
|
+
for (const id of missing) log('release.checklist', 'merged-pr-cell-not-found', { pr: number, id });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const LABEL_IDS = '([A-Za-z0-9_-]+(?:[ \\t]*,[ \\t]*[A-Za-z0-9_-]+)*)';
|
|
80
|
+
const LABEL_END = '(?=[ \\t]*$|[ \\t]+[—–·(-])';
|
|
81
|
+
const DESIGNATION = new RegExp(`^ {0,3}(?:#{1,6}[ \\t]+|[-*][ \\t]+)?칸[ \\t]*:[ \\t]*${LABEL_IDS}${LABEL_END}`);
|
|
82
|
+
|
|
83
|
+
/** Lines of a Markdown document outside fenced code (closed only by the same marker, at least as long), 4-space
|
|
84
|
+
* indented code and `>` quotes — the only lines that may designate a cell or declare completion. */
|
|
85
|
+
function proseLines(text: string): string[] {
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
let fence: { char: string; length: number } | null = null;
|
|
88
|
+
for (const raw of text.split('\n')) {
|
|
89
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
90
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
91
|
+
if (fence) {
|
|
92
|
+
if (marker && marker[1]![0] === fence.char && marker[1]!.length >= fence.length && line.trim() === marker[1]) fence = null;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (marker) { fence = { char: marker[1]![0]!, length: marker[1]!.length }; continue; }
|
|
96
|
+
if (/^( {4}|\t)/.test(line) || /^ {0,3}>/.test(line)) continue;
|
|
97
|
+
lines.push(line);
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Cell ids a document designates on a line head: `칸: X[, Y]`, optionally after a heading or list marker. */
|
|
103
|
+
export function goalCells(goal: string): Set<string> {
|
|
104
|
+
const ids = new Set<string>();
|
|
105
|
+
for (const line of proseLines(goal)) {
|
|
106
|
+
const label = DESIGNATION.exec(line);
|
|
107
|
+
if (!label) continue;
|
|
108
|
+
for (const id of label[1]!.split(',')) ids.add(id.trim());
|
|
109
|
+
}
|
|
110
|
+
return ids;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `이 칸 완료` counts only as a standalone prose line, not inside code or a quote. */
|
|
114
|
+
export function declaresCompletion(goal: string): boolean {
|
|
115
|
+
return proseLines(goal).some((line) => /^\s*이 칸 완료\s*$/.test(line));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A PR title is one authored line: `칸: X — …` may follow a short prefix such as `[TC]` or 「…」. */
|
|
119
|
+
export function titleCells(title: string): Set<string> {
|
|
120
|
+
const ids = new Set<string>();
|
|
121
|
+
for (const label of title.matchAll(new RegExp(`(?<![A-Za-z0-9가-힣])칸[ \\t]*:[ \\t]*${LABEL_IDS}${LABEL_END}`, 'g'))) {
|
|
122
|
+
for (const id of label[1]!.split(',')) ids.add(id.trim());
|
|
123
|
+
}
|
|
124
|
+
return ids;
|
|
125
|
+
}
|