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
|
@@ -0,0 +1,302 @@
|
|
|
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, validateCeoLoad, 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; ceoMinutes?: number; ceoDate?: 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 RebalanceResult {
|
|
16
|
+
decisions: PlacementDecision[];
|
|
17
|
+
blocked: Array<{ id: string; from: string; to: string; reason: string }>;
|
|
18
|
+
}
|
|
19
|
+
export interface PlacementDeps {
|
|
20
|
+
now?: Date;
|
|
21
|
+
schedules?: ReleaseSchedule[];
|
|
22
|
+
merged24h?: number;
|
|
23
|
+
seatCap?: Record<string, number>;
|
|
24
|
+
ceoDailyCap?: number;
|
|
25
|
+
checklist?: typeof listChecklist;
|
|
26
|
+
released?: string;
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
by?: string;
|
|
29
|
+
/** Backlog releases are a source of cells, never a placement target (default {@link BACKLOG_VERSIONS}). */
|
|
30
|
+
backlog?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 0.9.0 is the backlog bucket: cells are pulled out of it, never placed into it. */
|
|
34
|
+
export const BACKLOG_VERSIONS: readonly string[] = ['0.9.0'];
|
|
35
|
+
|
|
36
|
+
function versionOrder(a: string, b: string): number {
|
|
37
|
+
const left = a.split('.').map(Number), right = b.split('.').map(Number);
|
|
38
|
+
for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i]! - right[i]!;
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The merged PR list is a first-class GitHub query, not a count inferred from checklist status. */
|
|
43
|
+
export function mergedPrsLast24h(now = new Date()): number {
|
|
44
|
+
const since = new Date(now.getTime() - 86_400_000).toISOString();
|
|
45
|
+
const r = spawnSync('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since.slice(0, 10)}`, '--limit', '1000', '--json', 'number,mergedAt'], { encoding: 'utf8' });
|
|
46
|
+
if (r.status !== 0) throw new CliUserError(`병합 PR 속도 조회 실패: ${(r.stderr || r.error || '').toString().trim()}`);
|
|
47
|
+
let rows: Array<{ number: number; mergedAt: string }>;
|
|
48
|
+
try { rows = JSON.parse(r.stdout) as typeof rows; }
|
|
49
|
+
catch { throw new CliUserError('병합 PR 속도 응답이 JSON 이 아니다'); }
|
|
50
|
+
if (!Array.isArray(rows) || rows.length >= 1000 || rows.some((row) => typeof row.number !== 'number' || typeof row.mergedAt !== 'string' || !Number.isFinite(Date.parse(row.mergedAt))))
|
|
51
|
+
throw new CliUserError('병합 PR 속도 조회가 잘렸거나 잘못됐다');
|
|
52
|
+
return rows.filter((row) => Date.parse(row.mergedAt) >= Date.parse(since) && Date.parse(row.mergedAt) <= now.getTime()).length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function placementConfig(): { seatCap?: Record<string, number>; ceoDailyCap?: number } {
|
|
56
|
+
const path = getElanousConfigDirOverride() ? join(effectiveInstanceRoot(), 'config.json') : userConfigPath();
|
|
57
|
+
if (!existsSync(path)) return {};
|
|
58
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as { release?: { placement?: { seatCap?: Record<string, number>; ceoDailyCap?: number } } };
|
|
59
|
+
return raw.release?.placement ?? {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function placementSeatCap(): Record<string, number> { return placementConfig().seatCap ?? {}; }
|
|
63
|
+
function validCeoCap(cap: number): number {
|
|
64
|
+
if (!Number.isSafeInteger(cap) || cap < 0) throw new CliUserError('잘못된 대표 하루 상한: release.placement.ceoDailyCap');
|
|
65
|
+
return cap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function placementCeoDailyCap(): number { return validCeoCap(placementConfig().ceoDailyCap ?? 30); }
|
|
69
|
+
|
|
70
|
+
/** Explicit day wins; otherwise a cell uses the target release's landing day in KST. */
|
|
71
|
+
function ceoDay(item: { ceoDate?: string }, row: ReleaseSchedule): string {
|
|
72
|
+
return item.ceoDate ?? new Date(Date.parse(row.landBy!) + 9 * 3_600_000).toISOString().slice(0, 10);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type CeoWork = Pick<ChecklistItem, 'id' | 'ceoMinutes' | 'ceoDate'>;
|
|
76
|
+
function ceoOverload(item: CeoWork, row: ReleaseSchedule, snapshots: Map<string, { items: CeoWork[] }>, schedules: ReleaseSchedule[], cap: number): string | null {
|
|
77
|
+
if (!item.ceoMinutes) return null;
|
|
78
|
+
const day = ceoDay(item, row);
|
|
79
|
+
const peers = [...snapshots].flatMap(([version, snapshot]) => {
|
|
80
|
+
const schedule = schedules.find((candidate) => candidate.version === version);
|
|
81
|
+
return snapshot.items.filter((other) => other.id !== item.id && other.ceoMinutes
|
|
82
|
+
&& (other.ceoDate === day || (!other.ceoDate && schedule?.landBy && ceoDay(other, schedule) === day)));
|
|
83
|
+
});
|
|
84
|
+
const total = item.ceoMinutes + peers.reduce((sum, other) => sum + other.ceoMinutes!, 0);
|
|
85
|
+
if (total <= cap) return null;
|
|
86
|
+
return `대표 손 과부하 ${day} KST: ${[...peers.map((peer) => `${peer.id} ${peer.ceoMinutes}분`), `${item.id} ${item.ceoMinutes}분`].join(' + ')} = ${total}분 > 하루 상한 ${cap}분 — 늦추기(다음 날짜/판) · 자리 대행(대표 분량 축소) · 묶기(촬영·승인 합산 분량 축소)를 제안`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function dependencyViolation(
|
|
90
|
+
snapshots: Map<string, { items: ChecklistItem[] }>,
|
|
91
|
+
locations: Map<string, string>,
|
|
92
|
+
overrides: Map<string, string>,
|
|
93
|
+
input: PlacementCell,
|
|
94
|
+
): string | null {
|
|
95
|
+
for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
|
|
96
|
+
const dependentVersion = overrides.get(item.id) ?? version;
|
|
97
|
+
const predecessors = item.id === input.id ? input.predecessors : item.predecessors ?? [];
|
|
98
|
+
for (const predecessor of predecessors) {
|
|
99
|
+
const predecessorVersion = overrides.get(predecessor) ?? locations.get(predecessor);
|
|
100
|
+
if (!predecessorVersion || versionOrder(dependentVersion, predecessorVersion) <= 0)
|
|
101
|
+
return `의존 칸 ${item.id}은 선행 칸 ${predecessor} 이후 판이어야 한다`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!locations.has(input.id)) for (const predecessor of input.predecessors) {
|
|
105
|
+
const predecessorVersion = overrides.get(predecessor) ?? locations.get(predecessor);
|
|
106
|
+
const dependentVersion = overrides.get(input.id);
|
|
107
|
+
if (!predecessorVersion || !dependentVersion || versionOrder(dependentVersion, predecessorVersion) <= 0)
|
|
108
|
+
return `의존 칸 ${input.id}은 선행 칸 ${predecessor} 이후 판이어야 한다`;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A freeze window holds operational rollout only; the release still exists and takes cells (OP 10-04 19:39). */
|
|
114
|
+
function available(row: ReleaseSchedule, now: number, backlog: readonly string[]): boolean {
|
|
115
|
+
if (backlog.includes(row.version)) return false;
|
|
116
|
+
return Date.parse(row.landBy ?? row.cutAt) > now;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): PlacementDecision {
|
|
120
|
+
if (!input.id.trim() || !input.title.trim()) throw new CliUserError('칸 id 와 제목이 필요하다');
|
|
121
|
+
if (!Array.isArray(input.predecessors) || input.predecessors.some((id) => typeof id !== 'string' || !id.trim() || id === input.id)) throw new CliUserError('선행 칸 id 가 잘못됐다');
|
|
122
|
+
validateCeoLoad(input);
|
|
123
|
+
const seat = parseOwner(input.owner).seat;
|
|
124
|
+
if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
125
|
+
const now = (deps.now ?? new Date()).getTime();
|
|
126
|
+
const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
127
|
+
const released = deps.released ?? releasedVersion();
|
|
128
|
+
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
129
|
+
const open = schedules.filter((row) => (!released || versionOrder(row.version, released) > 0) && row.landBy && available(row, now, backlog));
|
|
130
|
+
const sources = [...new Set([...schedules.map((row) => row.version), ...backlog])];
|
|
131
|
+
const snapshots = new Map(sources.map((version) => [version, (deps.checklist ?? listChecklist)(version)]));
|
|
132
|
+
const locations = new Map<string, string>();
|
|
133
|
+
for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
|
|
134
|
+
if (backlog.includes(version)) continue;
|
|
135
|
+
if (locations.has(item.id)) throw new CliUserError(`여러 판의 같은 칸: ${item.id}`);
|
|
136
|
+
locations.set(item.id, version);
|
|
137
|
+
}
|
|
138
|
+
// A backlog copy never outranks a scheduled cell with the same id.
|
|
139
|
+
for (const version of backlog) for (const item of snapshots.get(version)?.items ?? []) if (!locations.has(item.id)) locations.set(item.id, version);
|
|
140
|
+
const from = locations.get(input.id) ?? null;
|
|
141
|
+
const existing = from ? snapshots.get(from)!.items.find((item) => item.id === input.id)! : null;
|
|
142
|
+
if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
|
|
143
|
+
if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
|
|
144
|
+
if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
|
|
145
|
+
const load = { id: input.id, ceoMinutes: input.ceoMinutes ?? existing?.ceoMinutes, ceoDate: input.ceoDate ?? existing?.ceoDate };
|
|
146
|
+
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
147
|
+
const predecessors = input.predecessors.map((id) => {
|
|
148
|
+
const version = locations.get(id);
|
|
149
|
+
if (!version) throw new CliUserError(`없는 선행 칸: ${id}`);
|
|
150
|
+
if (backlog.includes(version)) throw new CliUserError(`선행 칸 ${id} 이 백로그 ${version} 에 있다 — 먼저 배치하라`);
|
|
151
|
+
return version;
|
|
152
|
+
});
|
|
153
|
+
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
154
|
+
if (!Number.isInteger(rate) || rate < 0) throw new CliUserError('24시간 병합 PR 수가 잘못됐다');
|
|
155
|
+
const caps = deps.seatCap ?? placementSeatCap();
|
|
156
|
+
for (const [configuredSeat, limit] of Object.entries(caps)) if (!Number.isInteger(limit) || limit < 0) throw new CliUserError(`잘못된 자리 용량: ${configuredSeat}`);
|
|
157
|
+
const seatLimit = caps[seat] ?? Infinity;
|
|
158
|
+
const first = open[0];
|
|
159
|
+
const deadlineVersion = input.priority === 'P0' ? undefined : input.deadlineVersion ?? existing?.deadlineVersion;
|
|
160
|
+
if (input.priority === 'P1' && !deadlineVersion) throw new CliUserError(`P1 칸의 마감 판이 없다: ${input.id}`);
|
|
161
|
+
if (input.priority !== 'P0' && deadlineVersion && !schedules.some((row) => row.version === deadlineVersion)) throw new CliUserError(`없는 마감 판: ${deadlineVersion}`);
|
|
162
|
+
const eligible = open.filter((row) => predecessors.every((v) => versionOrder(row.version, v) > 0));
|
|
163
|
+
const candidates = input.priority === 'P0' ? eligible.filter((row) => row.version === first?.version)
|
|
164
|
+
: input.priority === 'P1' ? eligible.filter((row) => row.version === deadlineVersion)
|
|
165
|
+
: eligible.filter((row) => !deadlineVersion || versionOrder(row.version, deadlineVersion) <= 0);
|
|
166
|
+
|
|
167
|
+
const occupied = (version: string) => snapshots.get(version)!.items.filter((item) => item.status !== 'done' && item.id !== input.id);
|
|
168
|
+
const capacity = (row: ReleaseSchedule) => Math.floor(rate * (Date.parse(row.landBy!) - now) / 86_400_000 * 0.7);
|
|
169
|
+
const displaced: PlacementDecision['displaced'] = [];
|
|
170
|
+
let chosen: ReleaseSchedule | undefined;
|
|
171
|
+
let loadIssue: string | null = null;
|
|
172
|
+
const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
|
|
173
|
+
for (const row of feasible) {
|
|
174
|
+
const placement = new Map([[input.id, row.version]]);
|
|
175
|
+
const items = occupied(row.version);
|
|
176
|
+
const seatItems = items.filter((item) => ownerMatches(item.owner, seat));
|
|
177
|
+
const directIssue = ceoOverload(load, row, snapshots, schedules, ceoCap);
|
|
178
|
+
if (items.length < capacity(row) && seatItems.length < seatLimit) {
|
|
179
|
+
if (directIssue) { loadIssue ??= directIssue; continue; }
|
|
180
|
+
chosen = row;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (input.priority !== 'P1' || from === row.version) { loadIssue ??= directIssue; continue; }
|
|
184
|
+
const next = open.find((later) => versionOrder(later.version, row.version) > 0);
|
|
185
|
+
if (!next || items.length - 1 >= capacity(row)) { loadIssue ??= directIssue; continue; }
|
|
186
|
+
const p2 = [...items].reverse().find((item) => {
|
|
187
|
+
if (item.priority !== 'P2' || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
|
|
188
|
+
if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
|
|
189
|
+
if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
|
|
190
|
+
if (!item.owner) return false;
|
|
191
|
+
const shiftedSeat = parseOwner(item.owner).seat;
|
|
192
|
+
if (occupied(next.version).length >= capacity(next) || occupied(next.version).filter((other) => ownerMatches(other.owner, shiftedSeat)).length >= (caps[shiftedSeat] ?? Infinity)) return false;
|
|
193
|
+
if (!(seatItems.length < seatLimit || (ownerMatches(item.owner, seat) && seatItems.length - 1 < seatLimit))) return false;
|
|
194
|
+
|
|
195
|
+
// Evaluate both changed cells against the same post-placement state, before either ledger write.
|
|
196
|
+
const proposed = new Map<string, { items: CeoWork[] }>([...snapshots].map(([version, snapshot]) => [version, { items: snapshot.items.filter((cell) => cell.id !== input.id && cell.id !== item.id) }]));
|
|
197
|
+
proposed.get(row.version)!.items.push(load);
|
|
198
|
+
proposed.get(next.version)!.items.push(item);
|
|
199
|
+
const issue = ceoOverload(load, row, proposed, schedules, ceoCap)
|
|
200
|
+
?? ceoOverload(item, next, proposed, schedules, ceoCap);
|
|
201
|
+
if (issue) { loadIssue ??= issue; return false; }
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
|
+
if (p2) {
|
|
205
|
+
displaced.push({ id: p2.id, from: row.version, to: next.version, reason: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
|
|
206
|
+
chosen = row;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
loadIssue ??= directIssue;
|
|
210
|
+
}
|
|
211
|
+
if (!chosen) throw new CliUserError(loadIssue ?? `${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
|
|
212
|
+
const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
213
|
+
const decision = { id: input.id, from, version: chosen.version, reason, displaced };
|
|
214
|
+
if (!deps.dryRun) {
|
|
215
|
+
const by = deps.by ?? 'OP';
|
|
216
|
+
for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
|
|
217
|
+
if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
|
|
218
|
+
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate)))
|
|
219
|
+
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}) }, by);
|
|
220
|
+
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}) });
|
|
221
|
+
}
|
|
222
|
+
return decision;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function moveConstraint(
|
|
226
|
+
item: ChecklistItem, to: ReleaseSchedule, backlog: readonly string[],
|
|
227
|
+
snapshots: Map<string, ChecklistItem[]>, now: number, rate: number, caps: Record<string, number>, rows: ReleaseSchedule[], ceoCap: number,
|
|
228
|
+
): string | null {
|
|
229
|
+
if (!to.landBy || !available(to, now, backlog)) return '착지 마감이 지났거나 백로그 판이라 이동 불가';
|
|
230
|
+
if (item.deadlineVersion && versionOrder(to.version, item.deadlineVersion) > 0) return '마감 판 뒤로 이월 불가';
|
|
231
|
+
if (!item.owner) return '담당 없는 칸은 이동 불가';
|
|
232
|
+
const seat = parseOwner(item.owner).seat;
|
|
233
|
+
const destination = snapshots.get(to.version) ?? [];
|
|
234
|
+
if (destination.filter((cell) => cell.status !== 'done').length >= Math.floor(rate * (Date.parse(to.landBy) - now) / 86_400_000 * 0.7)) return '다음 판 PR 용량 초과';
|
|
235
|
+
if (destination.filter((cell) => cell.status !== 'done' && ownerMatches(cell.owner, seat)).length >= (caps[seat] ?? Infinity)) return '다음 판 자리 용량 초과';
|
|
236
|
+
const loadIssue = ceoOverload(item, to, new Map([...snapshots].map(([version, items]) => [version, { items }])),
|
|
237
|
+
rows, ceoCap);
|
|
238
|
+
if (loadIssue) return loadIssue;
|
|
239
|
+
const locations = new Map<string, string>();
|
|
240
|
+
for (const [version, items] of snapshots) for (const cell of items) {
|
|
241
|
+
if (locations.has(cell.id)) return `여러 판의 같은 칸: ${cell.id}`;
|
|
242
|
+
locations.set(cell.id, version);
|
|
243
|
+
}
|
|
244
|
+
for (const predecessor of item.predecessors ?? []) {
|
|
245
|
+
const location = locations.get(predecessor);
|
|
246
|
+
if (!location || versionOrder(to.version, location) <= 0) return `선행 칸 ${predecessor} 이후 판으로만 이동 가능`;
|
|
247
|
+
}
|
|
248
|
+
for (const [version, items] of snapshots) for (const dependent of items) {
|
|
249
|
+
if (dependent.id !== item.id && dependent.predecessors?.includes(item.id) && versionOrder(version, to.version) <= 0)
|
|
250
|
+
return `의존 칸 ${dependent.id} 이전 판으로만 이동 가능`;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Only unstarted cells inside the two-hour pre-deadline window move; a dry run never writes. */
|
|
256
|
+
export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceResult {
|
|
257
|
+
const rows = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
258
|
+
const current = rows.find((row) => row.version === version);
|
|
259
|
+
if (!current?.landBy) throw new CliUserError(`착지 마감이 없는 판: ${version}`);
|
|
260
|
+
const now = (deps.now ?? new Date()).getTime();
|
|
261
|
+
if (Date.parse(current.landBy) - now > 7_200_000 || Date.parse(current.landBy) <= now) return { decisions: [], blocked: [] };
|
|
262
|
+
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
263
|
+
const next = rows.find((row) => versionOrder(row.version, version) > 0 && !backlog.includes(row.version));
|
|
264
|
+
const snapshots = new Map(rows.map((row) => [row.version, [...(deps.checklist ?? listChecklist)(row.version).items]]));
|
|
265
|
+
const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence);
|
|
266
|
+
if (unstarted.length && !next) throw new CliUserError(`${version} 다음 판이 없다`);
|
|
267
|
+
if (!unstarted.length) return { decisions: [], blocked: [] };
|
|
268
|
+
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
269
|
+
const caps = deps.seatCap ?? placementSeatCap();
|
|
270
|
+
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
271
|
+
const result: RebalanceResult = { decisions: [], blocked: [] };
|
|
272
|
+
for (const item of unstarted) {
|
|
273
|
+
const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
|
|
274
|
+
if (violation) {
|
|
275
|
+
result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월`;
|
|
279
|
+
result.decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
|
|
280
|
+
snapshots.set(version, snapshots.get(version)!.filter((cell) => cell.id !== item.id));
|
|
281
|
+
snapshots.get(next!.version)!.push(item);
|
|
282
|
+
}
|
|
283
|
+
if (!deps.dryRun) for (const decision of result.decisions) move(decision.id, version, decision.version, deps.by ?? 'OP', undefined, undefined, decision.reason);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** A seat may defer its own cell one release, never pull another owner's cell forward. */
|
|
288
|
+
export function seatMove(id: string, from: string, to: string, by: string, reason: string, deps: PlacementDeps = {}): void {
|
|
289
|
+
if (!reason.trim()) throw new CliUserError('이동 이유가 필요하다');
|
|
290
|
+
const rows = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
291
|
+
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
292
|
+
const next = rows.find((row) => versionOrder(row.version, from) > 0 && !backlog.includes(row.version));
|
|
293
|
+
const snapshots = new Map(rows.map((row) => [row.version, (deps.checklist ?? listChecklist)(row.version).items]));
|
|
294
|
+
const item = snapshots.get(from)?.find((cell) => cell.id === id);
|
|
295
|
+
if (!item) throw new CliUserError(`없는 칸: ${id}`);
|
|
296
|
+
if (!ownerMatches(item.owner, parseOwner(by).seat) || next?.version !== to) throw new CliUserError('남의 칸 당기기 거부 — COO 에 요청');
|
|
297
|
+
const now = (deps.now ?? new Date()).getTime();
|
|
298
|
+
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
299
|
+
const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap(), rows, validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap()));
|
|
300
|
+
if (violation) throw new CliUserError(`${id}: ${violation}`);
|
|
301
|
+
if (!deps.dryRun) move(id, from, to, by, undefined, undefined, reason);
|
|
302
|
+
}
|
|
@@ -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;
|
|
@@ -52,7 +52,7 @@ export interface CeoTaskExtra {
|
|
|
52
52
|
attachments?: Array<{ name: string; path: string }>;
|
|
53
53
|
/** Paths supplied by callers that do not have attachment names. */
|
|
54
54
|
attachmentPaths?: readonly string[];
|
|
55
|
-
via?: 'telegram' | 'discord' | 'pwa';
|
|
55
|
+
via?: 'telegram' | 'discord' | 'pwa' | 'tui';
|
|
56
56
|
/** Request ref written into the message body; with `deps.hasMessage` it makes delivery idempotent per request. */
|
|
57
57
|
ref?: string;
|
|
58
58
|
}
|
|
@@ -90,7 +90,7 @@ async function postToChannel(seat: string, task: string, deps: CeoCommandDeps, e
|
|
|
90
90
|
if (!target) return deps.replyTarget ? 'decisions.replyGhPr 형식이 owner/repo#N 이 아님' : 'decisions.replyGhPr 미설정';
|
|
91
91
|
try {
|
|
92
92
|
const attachmentCount = (extra.attachments?.length ?? 0) + (extra.attachmentPaths?.length ?? 0);
|
|
93
|
-
const code = await deps.runGh(['pr', 'comment', target[2]!, '--repo', target[1]!, '--body-file', '-'], `${ceoTaskLine(seat, task, deps.now?.() ?? new Date())}${attachmentCount ? ` · 첨부 ${attachmentCount}` : ''}${extra.via === 'pwa' ? ' (PWA)' : ''}`);
|
|
93
|
+
const code = await deps.runGh(['pr', 'comment', target[2]!, '--repo', target[1]!, '--body-file', '-'], `${ceoTaskLine(seat, task, deps.now?.() ?? new Date())}${attachmentCount ? ` · 첨부 ${attachmentCount}` : ''}${extra.via === 'pwa' ? ' (PWA)' : extra.via === 'tui' ? ' (TUI)' : ''}`);
|
|
94
94
|
return code === 0 ? null : `gh 종료 코드 ${code}`;
|
|
95
95
|
} catch (error) {
|
|
96
96
|
return error instanceof Error ? error.message.slice(0, 80) : 'gh 실행 실패';
|
|
@@ -8,10 +8,11 @@ import type { SeatId } from './seat-questions.js';
|
|
|
8
8
|
export type AskOrigin =
|
|
9
9
|
| { channel: 'telegram'; chatId: number | string; messageId: number; threadId?: number; botId?: string }
|
|
10
10
|
| { channel: 'discord'; channelId: string; messageId: string; threadId?: string }
|
|
11
|
-
| { channel: 'pwa'; clientId: string }
|
|
11
|
+
| { channel: 'pwa'; clientId: string }
|
|
12
|
+
| { channel: 'tui'; clientId: string };
|
|
12
13
|
|
|
13
14
|
export const DEFAULT_ASK_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
14
|
-
export const parseSeatAsk = (text: string): string | null => /^\s*CTO\s*에게\s
|
|
15
|
+
export const parseSeatAsk = (text: string): string | null => /^\s*CTO\s*에게\s*물어봐(?:\s*[::]\s*|\s+)(\S[\s\S]*)$/i.exec(text)?.[1]?.trim() ?? null;
|
|
15
16
|
|
|
16
17
|
interface AskRow { id: string; seat: string; origin: string; deadline: number; timeout_minutes: number; answer: string | null; answered_at: number | null }
|
|
17
18
|
|
|
@@ -53,6 +54,17 @@ export function recordSeatAskShadow(root: string, ask: SeatAskShadow, now: numbe
|
|
|
53
54
|
} finally { store.close(); }
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** A TUI restart must keep the same return address as its prior questions. The instance's
|
|
58
|
+
* message database owns this identity, so test and production instances cannot share it. */
|
|
59
|
+
export function getTuiSeatAskClientId(open: () => MsgStore = openMsgStore): string {
|
|
60
|
+
const store = open();
|
|
61
|
+
try {
|
|
62
|
+
store.db.exec('CREATE TABLE IF NOT EXISTS seat_ask_clients (channel TEXT PRIMARY KEY, client_id TEXT NOT NULL)');
|
|
63
|
+
store.db.query('INSERT OR IGNORE INTO seat_ask_clients (channel, client_id) VALUES (?, ?)').run('tui', randomUUID());
|
|
64
|
+
return (store.db.query('SELECT client_id FROM seat_ask_clients WHERE channel = ?').get('tui') as { client_id: string }).client_id;
|
|
65
|
+
} finally { store.close(); }
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
export interface SeatAskDeps {
|
|
57
69
|
open?: () => MsgStore;
|
|
58
70
|
now?: () => number;
|
|
@@ -133,7 +145,7 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
133
145
|
for (const ask of pending) {
|
|
134
146
|
const origin = JSON.parse(ask.origin) as AskOrigin;
|
|
135
147
|
if (deps.channel && origin.channel !== deps.channel) continue;
|
|
136
|
-
if (origin.channel === 'pwa' && deps.clientId !== origin.clientId) continue;
|
|
148
|
+
if ((origin.channel === 'pwa' || origin.channel === 'tui') && deps.clientId !== origin.clientId) continue;
|
|
137
149
|
if (origin.channel === 'telegram' && origin.botId && origin.botId !== deps.botId) continue;
|
|
138
150
|
const status = decide(ask.id, (deps.now ?? Date.now)());
|
|
139
151
|
if (status) debug.log('seat.ask', status, { seat: ask.seat, id: ask.id, via: origin.channel });
|
|
@@ -147,6 +159,7 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
147
159
|
if (deps.clientId === origin.clientId) await deps.send(origin, item.text);
|
|
148
160
|
continue; // A browser poll is not a receipt; only its explicit acknowledgement consumes the answer.
|
|
149
161
|
}
|
|
162
|
+
if (origin.channel === 'tui' && deps.clientId !== origin.clientId) continue;
|
|
150
163
|
const token = randomUUID();
|
|
151
164
|
const now = Date.now();
|
|
152
165
|
// SQLite's conditional write is the claim across concurrent bot loops and processes.
|
|
@@ -73,6 +73,64 @@ export function seatQuestions(root: string, seat: SeatId): Message[] {
|
|
|
73
73
|
} finally { store.close(); }
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export type CrossCheckJudgment = { agree: boolean; note: string; resolves: boolean };
|
|
77
|
+
|
|
78
|
+
export function askCrossCheck(root: string, from: SeatId, to: SeatId, draft: string, key: string): Message {
|
|
79
|
+
if (from === to || !draft.trim() || !key.trim()) throw new Error('cross-check requires another seat, a draft and a key');
|
|
80
|
+
return deliverOnce(root, key, { from, to, body: draft.trim(), kind: 'seat-xcheck' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function seatCrossChecks(root: string, seat: SeatId): Message[] {
|
|
84
|
+
const store = storeAt(root);
|
|
85
|
+
try {
|
|
86
|
+
const found: Message[] = [];
|
|
87
|
+
let cursor = 0;
|
|
88
|
+
while (true) {
|
|
89
|
+
const batch = store.listByRecipient(seat, cursor, 1000);
|
|
90
|
+
found.push(...batch.filter((message) => message.kind === 'seat-xcheck' && isSeatId(message.from) && message.from !== seat));
|
|
91
|
+
if (batch.length < 1000) return found;
|
|
92
|
+
cursor = batch[batch.length - 1]!.id;
|
|
93
|
+
}
|
|
94
|
+
} finally { store.close(); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function answerCrossCheck(root: string, question: Message, judgment: CrossCheckJudgment): Message {
|
|
98
|
+
if (question.kind !== 'seat-xcheck' || !isSeatId(question.to) || !isSeatId(question.from)
|
|
99
|
+
|| typeof judgment.agree !== 'boolean' || typeof judgment.resolves !== 'boolean'
|
|
100
|
+
|| typeof judgment.note !== 'string' || !judgment.note.trim()) throw new Error('invalid cross-check answer');
|
|
101
|
+
return deliverOnce(root, `xcheck-answer:${question.id}`, {
|
|
102
|
+
from: question.to, to: question.from,
|
|
103
|
+
body: JSON.stringify({ questionId: question.id, agree: judgment.agree, note: judgment.note.trim(), resolves: judgment.resolves }),
|
|
104
|
+
kind: 'seat-xcheck-answer',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function crossCheckAnswer(root: string, question: Message): CrossCheckJudgment | null {
|
|
109
|
+
if (question.kind !== 'seat-xcheck' || !isSeatId(question.to) || !isSeatId(question.from)) return null;
|
|
110
|
+
const store = storeAt(root);
|
|
111
|
+
try {
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
while (true) {
|
|
114
|
+
const batch = store.listByRecipient(question.from, cursor, 1000);
|
|
115
|
+
for (const message of batch) {
|
|
116
|
+
if (message.kind !== 'seat-xcheck-answer' || message.from !== question.to) continue;
|
|
117
|
+
try {
|
|
118
|
+
const value: unknown = JSON.parse(message.body);
|
|
119
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
120
|
+
const answer = value as Record<string, unknown>;
|
|
121
|
+
if (answer.questionId === question.id && typeof answer.agree === 'boolean'
|
|
122
|
+
&& typeof answer.resolves === 'boolean' && typeof answer.note === 'string' && answer.note.trim()) {
|
|
123
|
+
return { agree: answer.agree, note: answer.note, resolves: answer.resolves };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch { /* Ignore unrelated malformed messages. */ }
|
|
127
|
+
}
|
|
128
|
+
if (batch.length < 1000) return null;
|
|
129
|
+
cursor = batch[batch.length - 1]!.id;
|
|
130
|
+
}
|
|
131
|
+
} finally { store.close(); }
|
|
132
|
+
}
|
|
133
|
+
|
|
76
134
|
export function answerSeat(root: string, question: Message, answer: string): Message {
|
|
77
135
|
if (question.kind !== 'seat-question' || !isSeatId(question.to) || !isSeatId(question.from) || !answer.trim()) {
|
|
78
136
|
throw new Error('invalid seat answer');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DEFAULT_MAX_REQUEST_AGE_HOURS = 48;
|
|
2
|
+
|
|
3
|
+
export type SeatRequestEligibility =
|
|
4
|
+
| { eligible: true }
|
|
5
|
+
| { eligible: false; reason: 'empty-body' | 'too-old' | 'slot-closed' };
|
|
6
|
+
|
|
7
|
+
/** Unknown/unreadable slot status does not establish that a slot has closed. */
|
|
8
|
+
export function seatRequestEligibility(
|
|
9
|
+
request: { text: string; queuedAt: string },
|
|
10
|
+
options: { now: Date; maxRequestAgeHours?: number; slotStatus?: string | null },
|
|
11
|
+
): SeatRequestEligibility {
|
|
12
|
+
if (!request.text.trim()) return { eligible: false, reason: 'empty-body' };
|
|
13
|
+
const maxAgeHours = options.maxRequestAgeHours ?? DEFAULT_MAX_REQUEST_AGE_HOURS;
|
|
14
|
+
if (!Number.isFinite(maxAgeHours) || maxAgeHours < 0) throw new Error('invalid seat request max age hours');
|
|
15
|
+
const age = options.now.getTime() - Date.parse(request.queuedAt);
|
|
16
|
+
if (!Number.isFinite(age) || age > maxAgeHours * 60 * 60_000) return { eligible: false, reason: 'too-old' };
|
|
17
|
+
if (options.slotStatus === 'green' || options.slotStatus === 'done') return { eligible: false, reason: 'slot-closed' };
|
|
18
|
+
return { eligible: true };
|
|
19
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { Database } from 'bun:sqlite';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export type SeatRequestStatus = 'pending' | 'queued' | 'rejected' | 'done';
|
|
7
|
+
export type SeatRequestRow = {
|
|
8
|
+
key: string;
|
|
9
|
+
seat: string;
|
|
10
|
+
status: SeatRequestStatus;
|
|
11
|
+
text: string;
|
|
12
|
+
queuedAt: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
closedAt?: string;
|
|
15
|
+
[field: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SeatRequestFilters = { seat?: string; status?: SeatRequestStatus };
|
|
19
|
+
export type CloseSeatRequestsOptions = {
|
|
20
|
+
reason: string;
|
|
21
|
+
status?: 'rejected' | 'done';
|
|
22
|
+
/** Minimum age in milliseconds, measured from queuedAt; only strictly older requests qualify. */
|
|
23
|
+
olderThan?: number;
|
|
24
|
+
now?: Date;
|
|
25
|
+
dryRun?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ledgerPath = (root: string): string => join(root, 'seat-requests', 'requests.jsonl');
|
|
29
|
+
|
|
30
|
+
/** SQLite releases its transaction lock on process death, including SIGKILL. */
|
|
31
|
+
export function withSeatRequestLedgerLock<T>(path: string, work: () => T): T {
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
const db = new Database(`${path}.lock.sqlite`);
|
|
34
|
+
try {
|
|
35
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
36
|
+
db.exec('BEGIN IMMEDIATE');
|
|
37
|
+
try {
|
|
38
|
+
const result = work();
|
|
39
|
+
db.exec('COMMIT');
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
db.exec('ROLLBACK');
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
} finally { db.close(); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Called under the journal transaction lock; never expose a partial JSON line. */
|
|
49
|
+
export function appendSeatRequestRows<T>(path: string, rows: readonly T[],
|
|
50
|
+
write: (fd: number, data: Buffer, offset: number, length: number) => number = writeSync): void {
|
|
51
|
+
if (!rows.length) return;
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
const prior = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
54
|
+
if (prior && !prior.endsWith('\n')) throw new Error('invalid seat request journal terminator');
|
|
55
|
+
const data = Buffer.from(prior + rows.map((row) => `${JSON.stringify(row)}\n`).join(''));
|
|
56
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
57
|
+
let fd: number | undefined;
|
|
58
|
+
try {
|
|
59
|
+
fd = openSync(temp, 'wx', 0o600);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (offset < data.length) {
|
|
62
|
+
const count = write(fd, data, offset, data.length - offset);
|
|
63
|
+
if (count <= 0) throw new Error('seat request journal write stalled');
|
|
64
|
+
offset += count;
|
|
65
|
+
}
|
|
66
|
+
fsyncSync(fd);
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
fd = undefined;
|
|
69
|
+
renameSync(temp, path);
|
|
70
|
+
const directory = openSync(dirname(path), 'r');
|
|
71
|
+
try { fsyncSync(directory); } finally { closeSync(directory); }
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (fd !== undefined) closeSync(fd);
|
|
74
|
+
try { unlinkSync(temp); } catch { /* temp was not created or already renamed */ }
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function latestRows(text: string): Map<string, SeatRequestRow> {
|
|
80
|
+
const latest = new Map<string, SeatRequestRow>();
|
|
81
|
+
for (const line of text.split('\n')) {
|
|
82
|
+
if (!line.trim()) continue;
|
|
83
|
+
const row: unknown = JSON.parse(line);
|
|
84
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('invalid seat request row');
|
|
85
|
+
const request = row as Record<string, unknown>;
|
|
86
|
+
if (typeof request.key !== 'string' || !request.key || typeof request.seat !== 'string' || !request.seat
|
|
87
|
+
|| !['pending', 'queued', 'rejected', 'done'].includes(String(request.status))
|
|
88
|
+
|| typeof request.text !== 'string' || typeof request.queuedAt !== 'string') {
|
|
89
|
+
throw new Error('invalid seat request row');
|
|
90
|
+
}
|
|
91
|
+
latest.set(request.key, request as SeatRequestRow);
|
|
92
|
+
}
|
|
93
|
+
return latest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Filters apply to the latest row, never to a historical row superseded by a closing event. */
|
|
97
|
+
export function listSeatRequests(root: string, filters: SeatRequestFilters = {}): SeatRequestRow[] {
|
|
98
|
+
const path = ledgerPath(root);
|
|
99
|
+
return [...latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '').values()].filter((row) =>
|
|
100
|
+
(filters.seat === undefined || row.seat === filters.seat)
|
|
101
|
+
&& (filters.status === undefined || row.status === filters.status));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Append at most one terminal event for each distinct, currently open request key. */
|
|
105
|
+
export function closeSeatRequests(root: string, keys: readonly string[], options: CloseSeatRequestsOptions): SeatRequestRow[] {
|
|
106
|
+
const reason = options.reason?.trim();
|
|
107
|
+
if (!reason) throw new Error('seat request close requires a reason');
|
|
108
|
+
const status = options.status ?? 'rejected';
|
|
109
|
+
if (status !== 'rejected' && status !== 'done') throw new Error('invalid seat request close status');
|
|
110
|
+
if (options.olderThan !== undefined && (!Number.isSafeInteger(options.olderThan) || options.olderThan < 0)) {
|
|
111
|
+
throw new Error('invalid seat request older-than duration');
|
|
112
|
+
}
|
|
113
|
+
const now = options.now ?? new Date();
|
|
114
|
+
if (!Number.isFinite(now.getTime())) throw new Error('invalid seat request close time');
|
|
115
|
+
const path = ledgerPath(root);
|
|
116
|
+
const close = (): SeatRequestRow[] => {
|
|
117
|
+
const latest = latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '');
|
|
118
|
+
const closed = [...new Set(keys)].flatMap((key) => {
|
|
119
|
+
const row = latest.get(key);
|
|
120
|
+
if (!row || (row.status !== 'pending' && row.status !== 'queued')) return [];
|
|
121
|
+
if (options.olderThan !== undefined) {
|
|
122
|
+
const queuedAt = Date.parse(row.queuedAt);
|
|
123
|
+
if (!Number.isFinite(queuedAt) || now.getTime() - queuedAt <= options.olderThan) return [];
|
|
124
|
+
}
|
|
125
|
+
return [{ ...row, status, reason, closedAt: now.toISOString(),
|
|
126
|
+
...(row.status === 'queued' && typeof row.ref !== 'string' && typeof row.receiptId === 'string'
|
|
127
|
+
? { ref: row.receiptId } : {}) } satisfies SeatRequestRow];
|
|
128
|
+
});
|
|
129
|
+
if (!options.dryRun) appendSeatRequestRows(path, closed);
|
|
130
|
+
return closed;
|
|
131
|
+
};
|
|
132
|
+
return options.dryRun ? close() : withSeatRequestLedgerLock(path, close);
|
|
133
|
+
}
|