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
|
@@ -6,6 +6,10 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
|
|
8
8
|
import { debug } from '../debug/log.js';
|
|
9
|
+
import { surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
10
|
+
import { judgeNeighbor } from '../loops/neighbors.js';
|
|
11
|
+
import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
|
|
12
|
+
import { isRescueAllowedAction } from '../steward/rescue.js';
|
|
9
13
|
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
10
14
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
11
15
|
import { loopEvent } from '../loops/observe.js';
|
|
@@ -17,7 +21,8 @@ import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePerson
|
|
|
17
21
|
import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
|
|
18
22
|
import { PersonaRegistry } from '../persona/registry.js';
|
|
19
23
|
import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
|
|
20
|
-
import {
|
|
24
|
+
import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
|
|
25
|
+
import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
|
|
21
26
|
import { queryRunningRuns } from '../self-implement/running-runs.js';
|
|
22
27
|
import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
23
28
|
import { readSchedules, releasedVersion } from '../release-loop/feature-store.js';
|
|
@@ -38,8 +43,14 @@ export type TcCandidate =
|
|
|
38
43
|
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
39
44
|
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
40
45
|
export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
|
|
41
|
-
export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean };
|
|
42
|
-
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
|
|
46
|
+
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
47
|
+
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
|
|
48
|
+
|
|
49
|
+
export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
50
|
+
const suffix = (result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason
|
|
51
|
+
? ` (live-safe 요청 · 이유: ${result.modeDowngradeReason})` : '';
|
|
52
|
+
return `seat loop ${result.seat}: ${result.status}${'runId' in result && result.runId ? ` ${result.runId}` : ''}${suffix}`;
|
|
53
|
+
}
|
|
43
54
|
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
|
|
44
55
|
export type SeatDeps = {
|
|
45
56
|
personaConfig?: PersonaLoopConfig;
|
|
@@ -55,6 +66,10 @@ export type SeatDeps = {
|
|
|
55
66
|
now?: () => Date;
|
|
56
67
|
read?: (path: string) => string;
|
|
57
68
|
run?: (args: string[]) => Promise<string>;
|
|
69
|
+
enqueue?: (seat: string, text: string, root: string, idempotencyKey: string) => Promise<{ id: string }>;
|
|
70
|
+
queueOutcome?: (id: string, root: string) => ReturnType<typeof harnessQueueOutcome>;
|
|
71
|
+
queueItems?: (root: string) => ReturnType<typeof listHarnessQueue>;
|
|
72
|
+
queueIdForKey?: (key: string, root: string) => string | undefined;
|
|
58
73
|
append?: (path: string, entry: SeatEntry) => void;
|
|
59
74
|
versions?: () => string[];
|
|
60
75
|
/** Release schedules (version · cutAt) — a version is open while its cut is still ahead. Default reads the release ledger; unreadable ⇒ no checklist candidates (fail closed). */
|
|
@@ -69,6 +84,8 @@ export type SeatDeps = {
|
|
|
69
84
|
stallDelivery?: (message: MessageEnvelope, key: string, root: string) => boolean;
|
|
70
85
|
/** Open cards from the decision ledger; injected only for isolated ledger tests. */
|
|
71
86
|
pendingDecisions?: () => Array<Pick<DecisionEntry, 'id' | 'title' | 'category' | 'dueAt'>>;
|
|
87
|
+
/** Running-run observation for OP cutoff; incomplete observations cannot establish that a run is absent. */
|
|
88
|
+
cutoffRuns?: (runIds: readonly string[]) => Pick<ReturnType<typeof queryRunningRuns>, 'completeness' | 'entries' | 'pty'>;
|
|
72
89
|
/** Read-only PR snapshot for TC judgment; defaults to GitHub CLI reads. */
|
|
73
90
|
pullRequests?: () => Promise<TcPullRequest[]>;
|
|
74
91
|
ledgerFiles?: (directory: string) => string[];
|
|
@@ -77,6 +94,9 @@ export type SeatDeps = {
|
|
|
77
94
|
inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
|
|
78
95
|
/** Supply the receiving seat's answer; absent evidence must not invent a reply. */
|
|
79
96
|
reply?: (seat: SeatId, question: string, from: SeatId) => Promise<SeatReply | null> | SeatReply | null;
|
|
97
|
+
/** Judge a decision draft against the receiving seat's evidence; null leaves the request pending. */
|
|
98
|
+
crossCheck?: (seat: SeatId, draft: string, from: SeatId) => Promise<CrossCheckJudgment | null> | CrossCheckJudgment | null;
|
|
99
|
+
decisionsConfig?: UserConfig['decisions'];
|
|
80
100
|
};
|
|
81
101
|
|
|
82
102
|
const repoRoot = resolve(import.meta.dir, '../..');
|
|
@@ -189,6 +209,10 @@ async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<Person
|
|
|
189
209
|
|
|
190
210
|
export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
191
211
|
const result = await runSeatLoopOnce(seat, deps);
|
|
212
|
+
if ((result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason) {
|
|
213
|
+
try { debug.log('seat-loop', 'mode-downgraded', { seat, requested: 'live-safe', effective: result.status, reason: result.modeDowngradeReason }); }
|
|
214
|
+
catch { /* logging cannot change execution */ }
|
|
215
|
+
}
|
|
192
216
|
try { await runPersonaShadowOnce(deps); }
|
|
193
217
|
catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
|
|
194
218
|
try {
|
|
@@ -224,6 +248,64 @@ function versionOrder(a: string, b: string): number {
|
|
|
224
248
|
|
|
225
249
|
function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
|
|
226
250
|
|
|
251
|
+
function handledItemKey(item: SeatItem): string {
|
|
252
|
+
return `${itemKey(item)}:${JSON.stringify([item.evidenceHash ?? null, itemSnapshot(item)])}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function queueKey(seat: string, item: SeatItem): string {
|
|
256
|
+
return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** A crash (or a failed write) between AUTOQ enqueue and the `queued` row leaves an `attempting`/`outcome-unknown` row with no queue id. Find its queue row by the
|
|
260
|
+
* seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
|
|
261
|
+
function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
|
|
262
|
+
const unsettled = (entry: SeatEntry) => (entry.status === 'attempting' || entry.status === 'outcome-unknown') && entry.action === 'harness' && !entry.queueId && !!entry.item;
|
|
263
|
+
const attempting = ledger.filter(unsettled);
|
|
264
|
+
if (attempting.length === 0) return [...ledger];
|
|
265
|
+
let rows: ReturnType<typeof listHarnessQueue>;
|
|
266
|
+
try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
|
|
267
|
+
catch (error) { observe('queue-recovery-unreadable', { error: String(error).slice(0, 200) }); return [...ledger]; }
|
|
268
|
+
return ledger.map((entry, index) => {
|
|
269
|
+
const item = entry.item;
|
|
270
|
+
if (!unsettled(entry) || !item) return entry;
|
|
271
|
+
const key = handledItemKey(item);
|
|
272
|
+
// A later row for the same item already settled this attempt.
|
|
273
|
+
if (ledger.slice(index + 1).some((later) => later.seat === entry.seat && later.item && handledItemKey(later.item) === key && later.status !== 'attempting' && later.status !== 'outcome-unknown')) return entry;
|
|
274
|
+
const row = rows.find((candidate) => candidate.seat === entry.seat && candidate.idempotencyKey === queueKey(entry.seat, item)
|
|
275
|
+
&& candidate.input === seatTaskText(entry.seat, item));
|
|
276
|
+
// The row may already have left the queue (finished · cancelled); its settled id is kept by key.
|
|
277
|
+
const id = row?.id ?? (deps.queueIdForKey ?? ((key: string, stateRoot: string) => harnessQueueIdForKey(key, { root: stateRoot })))(queueKey(entry.seat, item), root);
|
|
278
|
+
if (!id) return entry;
|
|
279
|
+
observe('queue-recovered', { seat: entry.seat, item: item.id, queueId: id, from: row ? 'queue' : 'key-marker' });
|
|
280
|
+
return { ...entry, status: 'queued' as const, queueId: id };
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function reconciledQueueLedger(recorded: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
|
|
285
|
+
const ledger = recoverAttemptingQueueIds(recorded, root, deps);
|
|
286
|
+
const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
|
|
287
|
+
const retryable = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
|
|
288
|
+
&& outcome(entry.queueId, root) === 'retryable' && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
|
|
289
|
+
const pending = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
|
|
290
|
+
&& ['pending', 'unknown'].includes(outcome(entry.queueId, root)) && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
|
|
291
|
+
const succeeded = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
|
|
292
|
+
&& outcome(entry.queueId, root) === 'succeeded' && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
|
|
293
|
+
return ledger.map((entry) => {
|
|
294
|
+
if (entry.status === 'attempting' && entry.item) {
|
|
295
|
+
const key = `${entry.seat}:${handledItemKey(entry.item)}`;
|
|
296
|
+
if (retryable.has(key) && !pending.has(key) && !succeeded.has(key)) return { ...entry, status: 'refused' as const };
|
|
297
|
+
}
|
|
298
|
+
if (entry.status !== 'queued' || !entry.queueId) return entry;
|
|
299
|
+
const state = outcome(entry.queueId, root);
|
|
300
|
+
if (state === 'retryable') {
|
|
301
|
+
observe('queue-retryable', { seat: entry.seat, item: entry.item?.id, queueId: entry.queueId });
|
|
302
|
+
return { ...entry, status: entry.item && (pending.has(`${entry.seat}:${handledItemKey(entry.item)}`) || succeeded.has(`${entry.seat}:${handledItemKey(entry.item)}`)) ? 'queued' as const : 'refused' as const,
|
|
303
|
+
reason: 'AUTOQ failed or cancelled; eligible for retry' };
|
|
304
|
+
}
|
|
305
|
+
return { ...entry, status: state === 'succeeded' ? 'launched' as const : entry.status };
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
227
309
|
function itemSnapshot(item: SeatItem): string {
|
|
228
310
|
return JSON.stringify([item.title, item.text, item.evidence ?? null, item.status ?? null]);
|
|
229
311
|
}
|
|
@@ -232,20 +314,31 @@ function decisionRef(seat: string, item: SeatItem): string {
|
|
|
232
314
|
return `seat-loop:${seat}:${encodeURIComponent(itemKey(item))}`;
|
|
233
315
|
}
|
|
234
316
|
|
|
317
|
+
// The decision ledger counts Unicode characters and sentence boundaries before it stores a card.
|
|
318
|
+
function decisionSummary(text: string): string {
|
|
319
|
+
const sentences = text.replace(/\s+/g, ' ').trim().split(/(?<=[.!?。!?])\s+/u).slice(0, 2).join(' ');
|
|
320
|
+
return Array.from(sentences).slice(0, 240).join('').trim();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function decisionSourceRef(seat: string, item: SeatItem): string {
|
|
324
|
+
return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
|
|
325
|
+
}
|
|
326
|
+
|
|
235
327
|
function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEntry | undefined {
|
|
236
|
-
const ref =
|
|
328
|
+
const ref = decisionSourceRef(seat, item);
|
|
237
329
|
return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).find((card) => card.refs?.includes(ref));
|
|
238
330
|
}
|
|
239
331
|
|
|
240
332
|
// RM3 (asked/answered · decision-card receipts for seat inquiries) ⊕ main (checklist cells re-picked by evidence, not by a shadow row).
|
|
241
|
-
function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string): Set<string> {
|
|
333
|
+
function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string, liveSafe = false): Set<string> {
|
|
242
334
|
const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
|
|
243
335
|
return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
|
|
336
|
+
|| entry.status === 'resolved-by-neighbor'
|
|
244
337
|
// RM3a: only TC's other-seat defect ask-shadows are deduplicated here — an ordinary inquiry recorded in shadow must still be delivered once questions turn on.
|
|
245
338
|
|| (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
|
|
246
339
|
|| (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
|
|
247
340
|
|| (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
|
|
248
|
-
|| (!inquiryRow(entry) && entry.status === 'outcome-unknown')
|
|
341
|
+
|| (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
|
|
249
342
|
|| (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
|
|
250
343
|
later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
|
|
251
344
|
&& later.item.version === entry.item?.version && later.item.id === entry.item?.id
|
|
@@ -253,12 +346,14 @@ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: strin
|
|
|
253
346
|
|| (shadow && entry.status === 'shadow' && entry.action !== 'wait' && entry.item?.source !== 'checklist')
|
|
254
347
|
|| (entry.status === 'launched' && entry.item?.source !== 'checklist'))
|
|
255
348
|
.filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string')
|
|
256
|
-
.map((entry) => itemKey(entry.item)));
|
|
349
|
+
.map((entry) => !liveSafe || inquiryRow(entry) ? itemKey(entry.item) : handledItemKey(entry.item)));
|
|
257
350
|
}
|
|
258
351
|
|
|
259
352
|
function eligibleItem(item: SeatItem, ledger: readonly SeatEntry[], handled: ReadonlySet<string>, shadow: boolean): boolean {
|
|
260
353
|
const key = itemKey(item);
|
|
261
|
-
if (handled.has(key)) return false;
|
|
354
|
+
if (handled.has(key) || handled.has(handledItemKey(item))) return false;
|
|
355
|
+
const rejected = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key && entry.status === 'rejected-no-evidence');
|
|
356
|
+
if (rejected?.item && itemSnapshot(rejected.item) === itemSnapshot(item)) return false;
|
|
262
357
|
const lastWait = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key
|
|
263
358
|
&& (entry.status === 'wait' || (shadow && entry.status === 'shadow' && entry.action === 'wait')));
|
|
264
359
|
return !lastWait?.item || itemSnapshot(lastWait.item) !== itemSnapshot(item);
|
|
@@ -275,7 +370,7 @@ export function alreadyHandled(item: SeatItem, ledger: readonly SeatEntry[]): bo
|
|
|
275
370
|
const at = Date.parse(entry.at || entry.ts || '');
|
|
276
371
|
return entry.seat === item.seat && at >= now - CHECKLIST_HANDLED_WINDOW_MS && at <= now
|
|
277
372
|
// A shadowed seat question was never delivered — it must be picked again once delivery is on (RM3 · TC merge 10-03).
|
|
278
|
-
&& (entry.status === 'launched' || (entry.status === 'shadow' && !entry.inquiry))
|
|
373
|
+
&& (entry.status === 'launched' || entry.status === 'queued' || (entry.status === 'shadow' && !entry.inquiry))
|
|
279
374
|
&& entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version === item.version
|
|
280
375
|
&& entry.item.title === item.title && entry.item.evidenceHash === item.evidenceHash;
|
|
281
376
|
});
|
|
@@ -292,7 +387,8 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
292
387
|
const read = deps.read ?? defaultRead;
|
|
293
388
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
294
389
|
const activeLedger = ledger ?? readSeatLedger(seat, deps, (deps.now ?? (() => new Date()))());
|
|
295
|
-
const
|
|
390
|
+
const mode = (deps.config ?? getUserConfig().loops?.seat)?.mode;
|
|
391
|
+
const consumed = handledKeys(activeLedger, mode === 'shadow', root, mode === 'live-safe');
|
|
296
392
|
const requests = new Map<string, Record<string, unknown>>();
|
|
297
393
|
for (const row of rows(read(join(root, 'seat-requests', 'requests.jsonl')))) {
|
|
298
394
|
if (typeof row.key === 'string') requests.set(row.key, row);
|
|
@@ -330,7 +426,7 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
330
426
|
try { fromSchedule = readSchedules().map((row) => row.version); } catch { /* ledger unreadable — directories only */ }
|
|
331
427
|
return [...new Set([...fromDirs, ...fromSchedule])].filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
332
428
|
});
|
|
333
|
-
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
|
|
429
|
+
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items);
|
|
334
430
|
let shipped: string | undefined;
|
|
335
431
|
try { shipped = releasedVersion(releaseRoot); }
|
|
336
432
|
catch (error) {
|
|
@@ -375,9 +471,70 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
375
471
|
debug.log('seat.loop', 'skip-closed-versions', { seat, count: closed.length, newest: closed.at(-1)!, oldest: closed[0]! });
|
|
376
472
|
}
|
|
377
473
|
checklist.sort((a, b) => versionOrder(a.version!, b.version!) || a.id.localeCompare(b.id));
|
|
474
|
+
const ownedEvidence = checklist.filter((item) => item.evidence?.trim())
|
|
475
|
+
.map((item) => `${item.id} · ${item.title}: ${item.evidence!.trim()}`).join('\n').slice(0, 8_000);
|
|
476
|
+
for (const request of pending) {
|
|
477
|
+
if (request.source === 'seat-question' && ownedEvidence) request.evidence = ownedEvidence;
|
|
478
|
+
}
|
|
378
479
|
return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
|
|
379
480
|
}
|
|
380
481
|
|
|
482
|
+
const OP_CUTOFF_WINDOW_MS = 2 * 60 * 60_000;
|
|
483
|
+
const RUN_REFERENCE = /\brun-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
484
|
+
|
|
485
|
+
function checkOpLandingCutoff(deps: SeatDeps, now: Date, path: string, stateRoot: string): SeatEntry | undefined {
|
|
486
|
+
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
487
|
+
const shipped = releasedVersion(releaseRoot);
|
|
488
|
+
const schedule = (deps.schedules ?? (() => readSchedules(releaseRoot)))()
|
|
489
|
+
.filter((row) => /^\d+\.\d+\.\d+$/.test(row.version) && (!shipped || versionOrder(row.version, shipped) > 0))
|
|
490
|
+
.sort((a, b) => versionOrder(a.version, b.version))[0];
|
|
491
|
+
if (!schedule?.landBy) return;
|
|
492
|
+
const untilLanding = Date.parse(schedule.landBy) - now.getTime();
|
|
493
|
+
if (!Number.isFinite(untilLanding) || untilLanding < 0 || untilLanding > OP_CUTOFF_WINDOW_MS) return;
|
|
494
|
+
const append = deps.append ?? defaultAppend;
|
|
495
|
+
let items: ReturnType<NonNullable<SeatDeps['checklistItems']>>;
|
|
496
|
+
try { items = (deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items))(schedule.version); }
|
|
497
|
+
catch (error) {
|
|
498
|
+
const entry: SeatEntry = { seat: 'OP', ts: now.toISOString(), at: now.toISOString(), status: 'shadow',
|
|
499
|
+
reason: `체크리스트 못 읽음: ${schedule.version} (${String(error).slice(0, 200)})`,
|
|
500
|
+
modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' };
|
|
501
|
+
append(path, entry);
|
|
502
|
+
return entry;
|
|
503
|
+
}
|
|
504
|
+
const yellow = items.filter((item) => item.status === 'yellow');
|
|
505
|
+
const runIds = [...new Set(yellow.flatMap((item) => [...(item.evidence ?? '').matchAll(RUN_REFERENCE)].map(([id]) => id.toLowerCase())))];
|
|
506
|
+
let running = new Set<string>();
|
|
507
|
+
if (runIds.length) {
|
|
508
|
+
try {
|
|
509
|
+
const observed = (deps.cutoffRuns ?? ((ids: readonly string[]) => queryRunningRuns({ runIds: ids, caller: 'seat-loop.op-cutoff' })))(runIds);
|
|
510
|
+
if (observed.completeness !== 'complete' || observed.pty.unreadable.length) {
|
|
511
|
+
observe('op-cutoff-runs-unreadable', { version: schedule.version });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
running = new Set(observed.entries.filter((row) => row.status === 'running' || row.status === 'probable-running' || row.status === 'unknown')
|
|
515
|
+
.map((row) => row.runId.toLowerCase()));
|
|
516
|
+
} catch (error) {
|
|
517
|
+
observe('op-cutoff-runs-unreadable', { version: schedule.version, error: String(error).slice(0, 200) });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const candidates = yellow.filter((item) => ![...(item.evidence ?? '').matchAll(RUN_REFERENCE)]
|
|
522
|
+
.some(([id]) => running.has(id.toLowerCase())))
|
|
523
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
524
|
+
.map(({ id, title, owner }) => ({ id, title, owner: owner ?? '미배정' }));
|
|
525
|
+
try { debug.log('seat-loop', 'op-cutoff-check', { version: schedule.version, landBy: schedule.landBy, candidates }); }
|
|
526
|
+
catch { /* Observation cannot change a request. */ }
|
|
527
|
+
if (!candidates.length) return;
|
|
528
|
+
const key = `seat-loop:op-cutoff:${schedule.version}:${seatDay(now)}`;
|
|
529
|
+
const requestPath = join(stateRoot, 'seat-requests', 'requests.jsonl');
|
|
530
|
+
withSeatRequestLedgerLock(requestPath, () => {
|
|
531
|
+
if (listSeatRequests(stateRoot).some((row) => row.key === key)) return;
|
|
532
|
+
appendSeatRequestRows(requestPath, [{ key, seat: 'OP', status: 'pending' as const, queuedAt: now.toISOString(),
|
|
533
|
+
text: `${schedule.version} 착지 마감 ${schedule.landBy} 전 노랑 칸 점검 — 다음 판으로 옮길지 OP 판단:\n${candidates.map((item) => `${item.id} · ${item.title} · 주인 ${item.owner}`).join('\n')}`,
|
|
534
|
+
version: schedule.version, landBy: schedule.landBy, candidates }]);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
381
538
|
function opCandidates(deps: SeatDeps, now: Date): OpCandidate[] {
|
|
382
539
|
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
383
540
|
const shipped = releasedVersion(releaseRoot);
|
|
@@ -583,6 +740,10 @@ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, no
|
|
|
583
740
|
debug.log('org.stall', 'escalate', { ...data, mode: 'shadow' });
|
|
584
741
|
continue;
|
|
585
742
|
}
|
|
743
|
+
if (config.mode === 'live-safe' && !isRescueAllowedAction('alert')) {
|
|
744
|
+
observe('refused', { seat, item: item.id, action: 'alert', reason: 'policy denied internal stall alert' });
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
586
747
|
const key = JSON.stringify([schedule.version, item.id, from, to, reason, startedAt]);
|
|
587
748
|
const message: MessageEnvelope = { from, to, kind: 'stall-escalation',
|
|
588
749
|
body: `[stall:${key}] ${schedule.version} 체크리스트 ${item.id} · ${item.title} · ${reason} · ${stalledMin}분 정체 (${from} → ${to})` };
|
|
@@ -601,8 +762,8 @@ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, no
|
|
|
601
762
|
}
|
|
602
763
|
|
|
603
764
|
// In shadow mode a shadowed item counts as handled, so a rehearsal day walks the queue like the seat would.
|
|
604
|
-
export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
|
|
605
|
-
const handled = handledKeys(ledger, opts.shadow === true, opts.root);
|
|
765
|
+
export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; liveSafe?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
|
|
766
|
+
const handled = handledKeys(ledger, opts.shadow === true, opts.root, opts.liveSafe === true);
|
|
606
767
|
return [...inputs.requests, ...inputs.checklist].find((item) => {
|
|
607
768
|
if (alreadyHandled(item, ledger)) {
|
|
608
769
|
debug.log('seat.loop', 'skip-handled', { id: item.id });
|
|
@@ -666,21 +827,193 @@ export function seatTaskText(seat: string, item: SeatItem): string {
|
|
|
666
827
|
return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
|
|
667
828
|
}
|
|
668
829
|
|
|
830
|
+
function xcheckEvent(event: string, seat: string, neighbor: string, item: string, outcome: string): void {
|
|
831
|
+
try { debug.log('decisions.xcheck', event, { seat, neighbor, item, outcome }); }
|
|
832
|
+
catch { /* Observation does not alter delivery. */ }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function neighborLastSeen(root: string, isolated: boolean, now: Date, wanted: ReadonlySet<string>): Map<string, string> {
|
|
836
|
+
const path = isolated ? join(root, 'surface_events.db') : surfaceEventsDbPath();
|
|
837
|
+
const last = new Map<string, string>();
|
|
838
|
+
if (!existsSync(path)) return last;
|
|
839
|
+
const db = new Database(path, { readonly: true });
|
|
840
|
+
try {
|
|
841
|
+
// Same outbound seat events and refs.seat as context day; unlike its 24h display window,
|
|
842
|
+
// look back across the whole journal so a genuinely old ACK can be judged absent.
|
|
843
|
+
// Newest first and stop once every wanted seat has an ACK, so a long journal is not read and sorted in full each turn.
|
|
844
|
+
const statement = db.prepare(`SELECT ts, refs FROM events
|
|
845
|
+
WHERE surface IN ('coord:channel', 'context:session') AND direction='outbound' AND ts<=? ORDER BY ts DESC`);
|
|
846
|
+
try {
|
|
847
|
+
const events = statement.iterate(now.toISOString()) as Iterable<{ ts: string; refs: string | null }>;
|
|
848
|
+
for (const event of events) {
|
|
849
|
+
let seat: unknown;
|
|
850
|
+
try { seat = (JSON.parse(event.refs ?? '{}') as { seat?: unknown }).seat; } catch { continue; }
|
|
851
|
+
if (typeof seat === 'string' && wanted.has(seat) && !last.has(seat)
|
|
852
|
+
&& Number.isFinite(Date.parse(event.ts)) && Date.parse(event.ts) <= now.getTime()) last.set(seat, event.ts);
|
|
853
|
+
if (last.size >= wanted.size) break;
|
|
854
|
+
}
|
|
855
|
+
} finally { statement.finalize(); }
|
|
856
|
+
return last;
|
|
857
|
+
} finally { db.close(); }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function neighborSeat(id: string): string | null {
|
|
861
|
+
const match = /^(op|tc|mk|ux)-seat$/i.exec(id);
|
|
862
|
+
return match ? match[1]!.toUpperCase() : null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps, root: string, now: Date): void {
|
|
866
|
+
const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
|
|
867
|
+
if (!neighbors.length) return;
|
|
868
|
+
let seen = new Map<string, string>();
|
|
869
|
+
let unreadable = false;
|
|
870
|
+
const wanted = new Set(neighbors.flatMap((neighbor) => { const source = neighborSeat(neighbor.id); return source ? [source] : []; }));
|
|
871
|
+
try { seen = wanted.size ? neighborLastSeen(root, !!deps.root, now, wanted) : seen; }
|
|
872
|
+
catch (error) { unreadable = true; observe('neighbor-source-unreadable', { seat, error: String(error).slice(0, 200) }); }
|
|
873
|
+
for (const neighbor of neighbors) {
|
|
874
|
+
try {
|
|
875
|
+
const source = neighborSeat(neighbor.id);
|
|
876
|
+
const lastSeenAt = source ? seen.get(source) : undefined;
|
|
877
|
+
const judgment = judgeNeighbor({ neighbor, lastSeenAt: unreadable ? null : lastSeenAt, now });
|
|
878
|
+
// action = what this turn actually did; plannedAction = what onAbsent asks for (shadow only records it).
|
|
879
|
+
let action: string | null = null;
|
|
880
|
+
const plannedAction = judgment.state === 'absent' ? judgment.action?.action ?? null : null;
|
|
881
|
+
if (judgment.state === 'absent') {
|
|
882
|
+
if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
|
|
883
|
+
const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
|
|
884
|
+
new DecisionLedger({ stateDir: root, now: () => now }).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
|
|
885
|
+
scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
|
|
886
|
+
c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
|
|
887
|
+
q: '결측 이웃의 업무를 어떻게 처리할까?' },
|
|
888
|
+
options: [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
|
|
889
|
+
{ key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }],
|
|
890
|
+
recommendation: { skipped: true, reason: '결측만으로 대행이나 보류를 자동 결정하지 않는다' },
|
|
891
|
+
raisedBy: { agent: 'seat-loop' }, refs: [ref], raisedAt: now.toISOString(),
|
|
892
|
+
crossCheckSkipped: '이웃 결측으로 교차 확인 불가' }, ref);
|
|
893
|
+
action = 'decision-card';
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: judgment.state, action, plannedAction, mode: config.mode }); }
|
|
897
|
+
catch { /* Observation cannot change the judgment. */ }
|
|
898
|
+
} catch (error) {
|
|
899
|
+
observe('neighbor-judgment-failed', { seat, neighbor: neighbor.id, error: String(error).slice(0, 200) });
|
|
900
|
+
try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: 'unknown', action: null, mode: config.mode }); }
|
|
901
|
+
catch { /* Observation cannot change the turn. */ }
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
669
906
|
export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
670
907
|
if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
|
|
671
|
-
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: '
|
|
672
|
-
if (config.mode === 'off'
|
|
908
|
+
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
|
|
909
|
+
if (config.mode === 'off') {
|
|
673
910
|
observe('skipped-off', { seat });
|
|
674
911
|
return { seat, status: 'skipped-off' };
|
|
675
912
|
}
|
|
913
|
+
if (!(config.seats ?? ['MK']).includes(seat)) {
|
|
914
|
+
// An excluded seat does not read inputs or run the shadow loop; never report it as shadow.
|
|
915
|
+
observe('skipped-off', { seat });
|
|
916
|
+
return { seat, status: 'skipped-off',
|
|
917
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: '자리 목록에 없음 (loops.seat.seats)' } : {}) };
|
|
918
|
+
}
|
|
676
919
|
const now = (deps.now ?? (() => new Date()))();
|
|
677
920
|
const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
|
|
678
921
|
const directory = dirname(path);
|
|
679
922
|
return withSeatLock(directory, async () => {
|
|
680
|
-
const ledger = readSeatLedger(seat, deps, now);
|
|
681
923
|
const stateRoot = deps.root ?? effectiveInstanceRoot();
|
|
924
|
+
const recorded = readSeatLedger(seat, deps, now);
|
|
925
|
+
const ledger = config.mode === 'live-safe' ? reconciledQueueLedger(recorded, stateRoot, deps) : recorded;
|
|
682
926
|
let turnError: unknown;
|
|
683
927
|
try {
|
|
928
|
+
if (config.mode === 'on' && config.questions === 'on') {
|
|
929
|
+
const proposals = ledger.filter((row) => row.status === 'awaiting-resolution' && row.xcheckQuestionId !== undefined
|
|
930
|
+
&& ((row.item?.source === 'checklist' && row.item.version) || row.item?.source === 'seat-question'));
|
|
931
|
+
for (const proposal of proposals) {
|
|
932
|
+
if (ledger.some((row) => row.status === 'resolved-by-neighbor' && row.xcheckQuestionId === proposal.xcheckQuestionId)) continue;
|
|
933
|
+
// Review round 3 ②: a seat question has no checklist cell — it is settled when this seat answers it later
|
|
934
|
+
// (new evidence) and otherwise follows the same deadline escalation below.
|
|
935
|
+
const isQuestion = proposal.item!.source === 'seat-question';
|
|
936
|
+
if (isQuestion && ledger.some((row) => row.item && itemKey(row.item) === itemKey(proposal.item!)
|
|
937
|
+
&& (row.status === 'answered' || row.status === 'hitl') && Date.parse(row.at) > Date.parse(proposal.at))) continue;
|
|
938
|
+
let cell: { status: string } | undefined;
|
|
939
|
+
if (!isQuestion) try {
|
|
940
|
+
cell = (deps.checklistItems ?? ((version: string) => listChecklist(version, stateRoot).items))(proposal.item!.version!)
|
|
941
|
+
.find((candidate) => candidate.id === proposal.item!.id && candidate.owner === seat);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
observe('xcheck-resolution-verification-failed', { seat, neighbor: proposal.xcheckNeighbor, item: itemKey(proposal.item!), error: String(error).slice(0, 200) });
|
|
944
|
+
}
|
|
945
|
+
if (cell?.status === 'green' || cell?.status === 'done') {
|
|
946
|
+
const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'resolved-by-neighbor' };
|
|
947
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
948
|
+
xcheckEvent('resolved-by-neighbor', seat, proposal.xcheckNeighbor ?? '', itemKey(proposal.item!), cell.status);
|
|
949
|
+
return entry;
|
|
950
|
+
}
|
|
951
|
+
// Review round 3 ①: a neighbor's «I can resolve it» has a deadline — if the cell is still open after the
|
|
952
|
+
// cross-check wait, escalate to a CEO card instead of excluding the item forever.
|
|
953
|
+
const waited = proposal.xcheckWaitMinutes ?? 120;
|
|
954
|
+
const proposedAt = Date.parse(proposal.at);
|
|
955
|
+
if ((cell || isQuestion) && Number.isFinite(proposedAt) && now.getTime() - proposedAt >= waited * 60_000) {
|
|
956
|
+
const neighbor = proposal.xcheckNeighbor ?? '';
|
|
957
|
+
const question = proposal.inquiry?.question ?? proposal.item!.title;
|
|
958
|
+
const note = `${proposal.xcheckNote ?? ''} — 해결 가능하다고 했으나 ${waited}분 안 해소 안 됨`.trim();
|
|
959
|
+
if (!decisionReceipt(stateRoot, seat, proposal.item!)) {
|
|
960
|
+
await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
|
|
961
|
+
'--s', `${seat} 질문 · ${proposal.item!.title}`, '--c', '이웃 자리 해결 제안이 기한 안에 해소되지 않았다',
|
|
962
|
+
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
963
|
+
'--skip-recommend', '근거가 부족하다', '--xcheck', `${neighbor}:${note}`,
|
|
964
|
+
'--agent', 'seat-loop', '--ref', decisionRef(seat, proposal.item!), '--json']);
|
|
965
|
+
}
|
|
966
|
+
const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'hitl' };
|
|
967
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
968
|
+
xcheckEvent('raised', seat, neighbor, itemKey(proposal.item!), 'resolution-timed-out');
|
|
969
|
+
return entry;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const received = seatCrossChecks(stateRoot, seat as SeatId).filter((question) => !crossCheckAnswer(stateRoot, question));
|
|
973
|
+
const receivedInputs = received.length ? await gatherSeatInputs(seat, deps, ledger) : null;
|
|
974
|
+
const judgmentEvidence = receivedInputs?.checklist.filter((item) => item.evidence?.trim())
|
|
975
|
+
.map((item) => `${item.id} · ${item.title}: ${item.evidence!.trim()}`).join('\n').slice(0, 8_000) ?? '';
|
|
976
|
+
const pending = received.find((question) => {
|
|
977
|
+
const senderRows = readSeatLedger(question.from, { root: stateRoot }, now).filter((row) => row.xcheckQuestionId === question.id);
|
|
978
|
+
const latest = senderRows.at(-1);
|
|
979
|
+
if (latest?.status === 'hitl' || latest?.status === 'resolved-by-neighbor' || latest?.status === 'awaiting-resolution') return false;
|
|
980
|
+
const request = senderRows.find((row) => row.status === 'awaiting-xcheck' && row.xcheckRequestedAt);
|
|
981
|
+
const waitMinutes = request?.xcheckWaitMinutes ?? 120;
|
|
982
|
+
if (request?.xcheckRequestedAt && now.getTime() - Date.parse(request.xcheckRequestedAt) >= waitMinutes * 60_000) return false;
|
|
983
|
+
return !ledger.some((row) => row.xcheckQuestionId === question.id && row.status === 'awaiting-xcheck'
|
|
984
|
+
&& row.xcheckJudgmentEvidence === judgmentEvidence);
|
|
985
|
+
});
|
|
986
|
+
if (pending) {
|
|
987
|
+
const append = deps.append ?? defaultAppend;
|
|
988
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'awaiting-xcheck',
|
|
989
|
+
action: 'seat-answer', xcheckQuestionId: pending.id, xcheckNeighbor: pending.from as SeatId,
|
|
990
|
+
xcheckJudgmentEvidence: judgmentEvidence };
|
|
991
|
+
try {
|
|
992
|
+
const judge = deps.crossCheck ?? (async (receiver: SeatId, draft: string, from: SeatId): Promise<CrossCheckJudgment | null> => {
|
|
993
|
+
const inputs = receivedInputs!;
|
|
994
|
+
const evidence = judgmentEvidence;
|
|
995
|
+
if (!evidence) throw new Error('cross-check judgment requires receiving-seat evidence');
|
|
996
|
+
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
997
|
+
`[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 결정 초안을 교차 확인한다. 아래 실제 근거만 사용하고 모르는 사실은 지어내지 마라. JSON 하나만 반환: {"agree":boolean,"note":"근거를 포함한 메모","resolves":boolean}. 이웃 자리에서 직접 해결할 수 있을 때만 resolves=true. 초안: ${draft}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
|
|
998
|
+
const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
999
|
+
return value as CrossCheckJudgment;
|
|
1000
|
+
});
|
|
1001
|
+
const answer = await judge(seat as SeatId, pending.body, pending.from as SeatId);
|
|
1002
|
+
if (!answer || typeof answer.agree !== 'boolean' || typeof answer.resolves !== 'boolean'
|
|
1003
|
+
|| typeof answer.note !== 'string' || !answer.note.trim()) throw new Error('invalid cross-check judgment');
|
|
1004
|
+
answerCrossCheck(stateRoot, pending, answer);
|
|
1005
|
+
entry.status = 'answered';
|
|
1006
|
+
entry.xcheckNote = answer.note.trim();
|
|
1007
|
+
append(path, entry);
|
|
1008
|
+
xcheckEvent('answered', seat, pending.from, String(pending.id), answer.resolves ? 'resolves' : answer.agree ? 'agree' : 'dissent');
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
append(path, entry);
|
|
1011
|
+
xcheckEvent('answered', seat, pending.from, String(pending.id), `judgment-failed: ${String(error).slice(0, 200)}`);
|
|
1012
|
+
}
|
|
1013
|
+
return entry;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
let tcDowngrade: SeatEntry | undefined;
|
|
684
1017
|
if (seat === 'TC') {
|
|
685
1018
|
const candidates = await tcCandidates(deps, now);
|
|
686
1019
|
for (const candidate of candidates) {
|
|
@@ -691,19 +1024,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
691
1024
|
&& row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
|
|
692
1025
|
if (!inLedger) {
|
|
693
1026
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
1027
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC 타 자리 결함 질문은 shadow 기록만 구현' } : {}),
|
|
694
1028
|
item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
|
|
695
1029
|
action: 'seat-question', inquiry: { to: candidate.to, question } };
|
|
696
1030
|
(deps.append ?? defaultAppend)(path, entry);
|
|
1031
|
+
tcDowngrade ??= entry;
|
|
697
1032
|
}
|
|
698
1033
|
if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
|
|
699
1034
|
continue;
|
|
700
1035
|
}
|
|
701
1036
|
if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
|
|
702
|
-
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate
|
|
1037
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
1038
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC PR 판정은 shadow 기록만 구현' } : {}) };
|
|
703
1039
|
(deps.append ?? defaultAppend)(path, entry);
|
|
1040
|
+
tcDowngrade ??= entry;
|
|
704
1041
|
observe('tc-shadow-candidate', { seat, candidate });
|
|
705
1042
|
}
|
|
706
1043
|
}
|
|
1044
|
+
if (seat === 'OP' && config.mode === 'live-safe') {
|
|
1045
|
+
const unreadable = checkOpLandingCutoff(deps, now, path, stateRoot);
|
|
1046
|
+
if (unreadable) return unreadable;
|
|
1047
|
+
}
|
|
707
1048
|
if (seat === 'OP' && !seatQuestions(stateRoot, 'OP').some((question) =>
|
|
708
1049
|
!hasSeatAnswer(stateRoot, question)
|
|
709
1050
|
&& !handledKeys(ledger, config.mode !== 'on' || (config.questions ?? 'shadow') !== 'on', stateRoot).has(`seat-question::${question.id}`))) {
|
|
@@ -740,7 +1081,8 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
740
1081
|
if (id !== currentDecision.id) candidates.push({ kind: 'decision-delegation', id, verdict: 'none' });
|
|
741
1082
|
}
|
|
742
1083
|
}
|
|
743
|
-
const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate
|
|
1084
|
+
const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
1085
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' } : {}) }));
|
|
744
1086
|
for (const entry of entries) {
|
|
745
1087
|
const candidate = entry.candidate!;
|
|
746
1088
|
const previous = [...ledger].reverse().find((row) => row.candidate?.kind === candidate.kind &&
|
|
@@ -756,20 +1098,53 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
756
1098
|
}
|
|
757
1099
|
return entries[0]!;
|
|
758
1100
|
}
|
|
1101
|
+
const failedDecisions = new Set<string>();
|
|
1102
|
+
let lastDecisionFailure: SeatEntry | undefined;
|
|
1103
|
+
while (true) {
|
|
759
1104
|
const inputs = await gatherSeatInputs(seat, deps, ledger);
|
|
1105
|
+
inputs.requests = inputs.requests.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
|
|
1106
|
+
inputs.checklist = inputs.checklist.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
|
|
760
1107
|
const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
|
|
761
1108
|
const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
|
|
762
|
-
const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
|
|
763
|
-
|
|
764
|
-
|
|
1109
|
+
const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
|
|
1110
|
+
&& eligibleItem(candidate, ledger, questionHandled, questionsMode !== 'on')
|
|
1111
|
+
&& !ledger.some((row) => row.status === 'awaiting-resolution' && row.item
|
|
1112
|
+
&& itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate)));
|
|
1113
|
+
const ordinary = pickNext({ ...inputs, requests: inputs.requests.filter((candidate) => candidate.source !== 'seat-question'),
|
|
1114
|
+
checklist: inputs.checklist.filter((candidate) => !ledger.some((row) => row.status === 'awaiting-resolution'
|
|
1115
|
+
&& row.item && itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate))) }, ledger,
|
|
1116
|
+
{ shadow: config.mode === 'shadow', liveSafe: config.mode === 'live-safe', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
|
|
765
1117
|
const item = pendingQuestion ?? ordinary;
|
|
766
1118
|
const append = deps.append ?? defaultAppend;
|
|
1119
|
+
const revisedProposal = item?.source === 'checklist' && ledger.some((row) => row.status === 'awaiting-resolution'
|
|
1120
|
+
&& row.item && itemKey(row.item) === itemKey(item) && itemSnapshot(row.item) !== itemSnapshot(item));
|
|
1121
|
+
if (config.mode === 'live-safe' && !pendingQuestion) {
|
|
1122
|
+
const pendingRows = (deps.queueItems ?? ((root: string) => listHarnessQueue({ root })))(stateRoot);
|
|
1123
|
+
for (const candidate of [...inputs.requests, ...inputs.checklist].filter((row) => row.source !== 'seat-question')) {
|
|
1124
|
+
const pending = pendingRows.find((row) => row.seat === seat && row.kind === 'say'
|
|
1125
|
+
&& row.idempotencyKey === queueKey(seat, candidate) && row.input === seatTaskText(seat, candidate)
|
|
1126
|
+
&& ['queued', 'launching', 'launched'].includes(row.status)
|
|
1127
|
+
&& ['pending', 'unknown'].includes((deps.queueOutcome ?? ((id: string, root: string) => harnessQueueOutcome(id, { root })))(row.id, stateRoot)));
|
|
1128
|
+
if (!pending) continue;
|
|
1129
|
+
if (ledger.some((row) => row.queueId === pending.id && row.status === 'queued')) continue;
|
|
1130
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'queued', item: candidate, action: 'harness', queueId: pending.id };
|
|
1131
|
+
append(path, entry);
|
|
1132
|
+
observe('queued', { seat, item: candidate, queueId: pending.id, deduplicated: true });
|
|
1133
|
+
return entry;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
767
1136
|
// RM3 review must-fix: `questions` now has a default, so it can't be the trigger — a shadow seat loop never calls the model on its own.
|
|
768
|
-
if (item && (
|
|
1137
|
+
if (item && (!revisedProposal || planAction(item, seat).kind === 'decision')
|
|
1138
|
+
&& (item.source === 'seat-question' || (config.mode !== 'live-safe' && (deps.inquire || (config.mode === 'on' && config.questions !== undefined))))) {
|
|
769
1139
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1140
|
+
const unfinished = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
|
|
1141
|
+
&& !(item.source === 'seat-question' && ledger.some((prior) => prior.status === 'awaiting-resolution'
|
|
1142
|
+
&& prior.item && itemKey(prior.item) === itemKey(item) && itemSnapshot(prior.item) !== itemSnapshot(item)))
|
|
1143
|
+
&& (row.status === 'attempting' || row.status === 'outcome-unknown' || row.status === 'awaiting-xcheck')
|
|
1144
|
+
&& (row.action === 'seat-question' || row.action === 'seat-answer' || row.inquiry?.to === 'CEO'));
|
|
1145
|
+
const inquiry = item.source === 'seat-question' ? null : revisedProposal && item.source === 'checklist'
|
|
1146
|
+
? ledger.find((row) => row.status === 'awaiting-resolution' && row.item && itemKey(row.item) === itemKey(item))?.inquiry ?? null
|
|
1147
|
+
: unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
|
|
773
1148
|
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
774
1149
|
`[${asking} 자리 · 역할 docs/roles/${asking}.md] 이 항목을 판단할 근거가 부족한 경우에만 다른 자리 또는 사람에게 질문한다. 근거가 충분하면 null. JSON 객체 하나만 반환: null 또는 {"to":"OP|TC|MK|UX|CEO","question":"필요한 근거를 묻는 질문"}. 항목: ${work.id} ${work.title}\n내용: ${work.text}\n역할: ${(deps.read ?? defaultRead)(join(deps.repo ?? repoRoot, 'docs', 'roles', `${asking}.md`)).slice(0, 4_000)}`])).trim()) as { reply?: unknown };
|
|
775
1150
|
const value: unknown = typeof output?.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
@@ -788,7 +1163,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
788
1163
|
const reply = item.source === 'seat-question' && item.from && (questionsMode === 'on' || deps.reply || config.questions !== undefined)
|
|
789
1164
|
? unfinished?.answer ? { answer: unfinished.answer } : unfinished?.inquiry ? unfinished.inquiry : await (deps.reply ?? (async (receiver: SeatId, question: string, from: SeatId) => {
|
|
790
1165
|
const evidence = inputs.checklist.map((candidate) =>
|
|
791
|
-
`${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})`).join('\n').slice(0, 8_000);
|
|
1166
|
+
`${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})${candidate.evidence?.trim() ? ` · 근거: ${candidate.evidence.trim()}` : ''}`).join('\n').slice(0, 8_000);
|
|
792
1167
|
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
793
1168
|
`[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 질문에 근거를 갖고 답하라. 아래 ${receiver} 자리의 실제 체크리스트 근거에 없는 사실은 지어내지 마라. 모르면 사람 결정이 필요한 질문을 작성하라. JSON 하나만 반환: {"answer":"근거를 포함한 답"} 또는 {"to":"CEO","question":"결정할 질문"}. 질문: ${question}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
|
|
794
1169
|
const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
@@ -810,6 +1185,10 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
810
1185
|
entry.action = 'decision';
|
|
811
1186
|
}
|
|
812
1187
|
if (questionsMode !== 'on') {
|
|
1188
|
+
if (config.mode === 'live-safe') entry.modeDowngradeReason = '질문 전달 선행 조건 미충족 (questions shadow)';
|
|
1189
|
+
if (config.mode === 'live-safe' && !isRescueAllowedAction('seat-question')) {
|
|
1190
|
+
observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'question delivery is shadow-only' });
|
|
1191
|
+
}
|
|
813
1192
|
append(path, entry);
|
|
814
1193
|
observe('question-shadow', { seat, item, inquiry: entry.inquiry, answered: !!entry.answer });
|
|
815
1194
|
return entry;
|
|
@@ -819,7 +1198,18 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
819
1198
|
append(path, entry);
|
|
820
1199
|
return entry;
|
|
821
1200
|
}
|
|
822
|
-
|
|
1201
|
+
if (entry.inquiry?.to === 'CEO' && !item.evidence?.trim() && !decisionReceipt(root, seat, item)
|
|
1202
|
+
&& !ledger.some((row) => row.item && itemKey(row.item) === itemKey(item) && row.status === 'awaiting-xcheck')
|
|
1203
|
+
&& !deliveredSeatQuestion(root, `xcheck:${seat}:${itemKey(item)}`)) {
|
|
1204
|
+
entry.status = 'rejected-no-evidence';
|
|
1205
|
+
append(path, entry);
|
|
1206
|
+
xcheckEvent('rejected-no-evidence', seat, (deps.decisionsConfig ?? getUserConfig().decisions)?.crossCheckNeighbor?.[seat as SeatId] ?? 'OP', itemKey(item), 'no-evidence');
|
|
1207
|
+
return entry;
|
|
1208
|
+
}
|
|
1209
|
+
if (!revisedProposal && (entry.inquiry?.to !== 'CEO' || (unfinished?.status !== 'awaiting-xcheck' && !ledger.some((row) =>
|
|
1210
|
+
row.item && itemKey(row.item) === itemKey(item) && row.status === 'awaiting-xcheck')))) {
|
|
1211
|
+
append(path, { ...entry, status: 'attempting' });
|
|
1212
|
+
}
|
|
823
1213
|
try {
|
|
824
1214
|
if (item.source === 'seat-question' && reply && 'answer' in reply) {
|
|
825
1215
|
const question = { id: Number(item.id), from: item.from!, to: seat, body: item.text, kind: 'seat-question', createdAt: item.createdAt ?? '' };
|
|
@@ -829,16 +1219,55 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
829
1219
|
const ask = entry.inquiry!;
|
|
830
1220
|
if (!ask.question.trim()) throw new Error('empty seat question');
|
|
831
1221
|
if (ask.to === 'CEO') {
|
|
1222
|
+
const settings = deps.decisionsConfig ?? getUserConfig().decisions!;
|
|
1223
|
+
const neighbor = settings.crossCheckNeighbor?.[seat as SeatId] ?? ({ OP: 'TC', TC: 'OP', MK: 'OP', UX: 'OP' } as const)[seat as SeatId];
|
|
1224
|
+
const waitMinutes = settings.crossCheckWaitMinutes ?? 120;
|
|
1225
|
+
const previous = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
|
|
1226
|
+
&& row.status === 'awaiting-xcheck' && row.xcheckQuestionId !== undefined);
|
|
832
1227
|
const prior = decisionReceipt(root, seat, item);
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1228
|
+
if (prior) {
|
|
1229
|
+
entry.status = 'hitl';
|
|
1230
|
+
} else {
|
|
1231
|
+
const key = `xcheck:${seat}:${itemKey(item)}`;
|
|
1232
|
+
const delivered = deliveredSeatQuestion(root, key);
|
|
1233
|
+
if (delivered && (delivered.kind !== 'seat-xcheck' || delivered.from !== seat || !isSeatId(delivered.to))) {
|
|
1234
|
+
throw new Error('cross-check retry has an invalid recorded recipient');
|
|
1235
|
+
}
|
|
1236
|
+
const requestedNeighbor = delivered ? delivered.to as SeatId : neighbor;
|
|
1237
|
+
if (!delivered && !item.evidence?.trim()) {
|
|
1238
|
+
entry.status = 'rejected-no-evidence';
|
|
1239
|
+
xcheckEvent('rejected-no-evidence', seat, neighbor, itemKey(item), 'no-evidence');
|
|
1240
|
+
} else {
|
|
1241
|
+
const question = delivered ?? askCrossCheck(root, seat as SeatId, neighbor,
|
|
1242
|
+
`제목: ${seat}: ${ask.question}\nS: ${seat} 질문 · ${item.title}\nC: 판단 근거 부족 — 사람의 결정이 필요하다\n질문: ${ask.question}\n근거: ${item.evidence!.trim()}`, key);
|
|
1243
|
+
const requestedAt = previous?.xcheckRequestedAt ?? (delivered ? delivered.createdAt : now.toISOString());
|
|
1244
|
+
entry.xcheckQuestionId = question.id;
|
|
1245
|
+
entry.xcheckRequestedAt = requestedAt;
|
|
1246
|
+
entry.xcheckNeighbor = requestedNeighbor;
|
|
1247
|
+
entry.xcheckWaitMinutes = previous?.xcheckWaitMinutes ?? waitMinutes;
|
|
1248
|
+
if (!delivered) xcheckEvent('requested', seat, requestedNeighbor, itemKey(item), 'awaiting-xcheck');
|
|
1249
|
+
const answer = delivered ? crossCheckAnswer(root, question) : null;
|
|
1250
|
+
if (!delivered || (!answer && now.getTime() - Date.parse(requestedAt) < entry.xcheckWaitMinutes * 60_000)) {
|
|
1251
|
+
entry.status = 'awaiting-xcheck';
|
|
1252
|
+
} else if (answer?.resolves) {
|
|
1253
|
+
entry.status = 'awaiting-resolution';
|
|
1254
|
+
entry.xcheckNote = answer.note;
|
|
1255
|
+
if (!revisedProposal) xcheckEvent('answered', seat, requestedNeighbor, itemKey(item), 'resolution-proposed');
|
|
1256
|
+
} else {
|
|
1257
|
+
if (!answer) xcheckEvent('timed-out', seat, requestedNeighbor, itemKey(item), `unanswered ${entry.xcheckWaitMinutes}m`);
|
|
1258
|
+
const check = answer ? ['--xcheck', `${requestedNeighbor}:${answer.note}`,
|
|
1259
|
+
...(answer.agree ? [] : ['--dissent', `${requestedNeighbor}: ${answer.note}`])]
|
|
1260
|
+
: ['--no-xcheck', `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`];
|
|
1261
|
+
await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
|
|
1262
|
+
'--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
|
|
1263
|
+
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
1264
|
+
'--skip-recommend', '근거가 부족하다', ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
|
|
1265
|
+
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
1266
|
+
entry.status = 'hitl';
|
|
1267
|
+
xcheckEvent('raised', seat, requestedNeighbor, itemKey(item), answer ? answer.agree ? 'agree' : 'dissent' : 'timed-out');
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
839
1270
|
}
|
|
840
|
-
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
841
|
-
entry.status = 'hitl';
|
|
842
1271
|
} else {
|
|
843
1272
|
if (!isSeatId(ask.to) || ask.to === seat) throw new Error('invalid recipient seat');
|
|
844
1273
|
const key = `ask:${seat}:${itemKey(item)}`;
|
|
@@ -861,9 +1290,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
861
1290
|
}
|
|
862
1291
|
if (item) observe('picked', { seat, item, date: seatDay(now) });
|
|
863
1292
|
const planned = item ? planAction(item, seat) : undefined;
|
|
1293
|
+
if (item && config.mode === 'live-safe' && deps.inquire && !isRescueAllowedAction('seat-question')) {
|
|
1294
|
+
observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'outbound inquiry not in rescue policy' });
|
|
1295
|
+
}
|
|
1296
|
+
if (!item && config.mode === 'live-safe' && tcDowngrade) return tcDowngrade;
|
|
864
1297
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
|
|
865
1298
|
...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
|
|
866
1299
|
if (config.mode === 'shadow' || !item) {
|
|
1300
|
+
if (!item && lastDecisionFailure) return lastDecisionFailure;
|
|
867
1301
|
append(path, entry);
|
|
868
1302
|
if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
|
|
869
1303
|
return entry;
|
|
@@ -873,6 +1307,19 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
873
1307
|
append(path, entry);
|
|
874
1308
|
return entry;
|
|
875
1309
|
}
|
|
1310
|
+
if (config.mode === 'live-safe') {
|
|
1311
|
+
const requested = planned!.kind === 'harness' ? 'launch' : 'decision-card';
|
|
1312
|
+
if (planned!.kind === 'decision' && !isRescueAllowedAction(planned!.kind)) {
|
|
1313
|
+
observe('refused', { seat, item: item.id, action: planned!.reason ?? 'decision', reason: 'execution forbidden; decision card only' });
|
|
1314
|
+
}
|
|
1315
|
+
if (!isRescueAllowedAction(requested)) {
|
|
1316
|
+
entry.status = 'refused';
|
|
1317
|
+
entry.reason = `action not permitted: ${requested}`;
|
|
1318
|
+
append(path, entry);
|
|
1319
|
+
observe('refused', { seat, item: item.id, action: requested, reason: entry.reason });
|
|
1320
|
+
return entry;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
876
1323
|
const run = deps.run ?? defaultRun;
|
|
877
1324
|
let outcome: string | undefined;
|
|
878
1325
|
try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
|
|
@@ -912,7 +1359,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
912
1359
|
observe('skipped-budget', { seat, item, reason: budget.reason });
|
|
913
1360
|
const parent = (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat];
|
|
914
1361
|
// Dedupe only on a DELIVERED escalation, so a failed send is retried on the next turn (TC harvest · review round 1).
|
|
915
|
-
if (parent && parent !== seat &&
|
|
1362
|
+
if (parent && parent !== seat && config.mode === 'live-safe' && !isRescueAllowedAction('alert')) {
|
|
1363
|
+
observe('refused', { seat, item: item.id, action: 'alert', reason: 'policy denied budget alert' });
|
|
1364
|
+
} else if (parent && parent !== seat && !ledger.some((prior) => prior.status === 'skipped-budget' && prior.escalated === true && prior.reason === budget.reason
|
|
916
1365
|
&& prior.item && itemKey(prior.item) === itemKey(item) && seatDay(new Date(prior.ts ?? prior.at)) === seatDay(now))) {
|
|
917
1366
|
const line = `[${seat} → ${parent}] ${item.id}: ${budget.reason}`;
|
|
918
1367
|
try {
|
|
@@ -936,11 +1385,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
936
1385
|
append(path, { ...entry, status: 'attempting' });
|
|
937
1386
|
try {
|
|
938
1387
|
if (action.kind === 'decision') {
|
|
939
|
-
const title = `${seat}: ${action.text
|
|
1388
|
+
const title = `${seat}: ${decisionSummary(action.text)}`;
|
|
1389
|
+
const source = decisionSourceRef(seat, item);
|
|
940
1390
|
const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
|
|
941
|
-
'--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}
|
|
1391
|
+
'--s', decisionSummary(`${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')} 역할: ${inputs.role}`),
|
|
1392
|
+
'--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
|
|
942
1393
|
'--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
|
|
943
|
-
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
|
|
1394
|
+
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
|
|
1395
|
+
'--agent', 'seat-loop', '--ref', source, '--json']);
|
|
944
1396
|
const decision: unknown = JSON.parse(raised.trim());
|
|
945
1397
|
if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
|
|
946
1398
|
throw new Error('decisions raise returned no decision id');
|
|
@@ -950,6 +1402,26 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
950
1402
|
observe('hitl', { seat, item });
|
|
951
1403
|
return entry;
|
|
952
1404
|
}
|
|
1405
|
+
if (config.mode === 'live-safe') {
|
|
1406
|
+
let queued: { id: string };
|
|
1407
|
+
try {
|
|
1408
|
+
queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
// Same key, different task body: never adopt the other work. Refuse this turn; the key frees once that item finishes.
|
|
1411
|
+
if (!/idempotency key collision/.test(String(error))) throw error;
|
|
1412
|
+
entry.status = 'refused';
|
|
1413
|
+
entry.reason = 'AUTOQ idempotency key collision: a queued item with this key carries a different task';
|
|
1414
|
+
append(path, entry);
|
|
1415
|
+
observe('refused', { seat, item: item.id, action: 'launch', reason: entry.reason });
|
|
1416
|
+
return entry;
|
|
1417
|
+
}
|
|
1418
|
+
if (!queued || typeof queued.id !== 'string' || !/^hq-[0-9a-f-]{36}$/i.test(queued.id)) throw new Error('AUTOQ returned no queue id');
|
|
1419
|
+
entry.status = 'queued';
|
|
1420
|
+
entry.queueId = queued.id;
|
|
1421
|
+
append(path, entry);
|
|
1422
|
+
observe('queued', { seat, item, queueId: queued.id });
|
|
1423
|
+
return entry;
|
|
1424
|
+
}
|
|
953
1425
|
const output = await run(['harness', 'say', action.text, '--substrate', 'pod', '--pod-pool', config.podPool ?? 'pool-node-b@node-b:8', '--base', 'main', '--json']);
|
|
954
1426
|
const results: unknown = JSON.parse(output.trim());
|
|
955
1427
|
if (!Array.isArray(results) || results.length === 0 || results.some((result) =>
|
|
@@ -966,14 +1438,33 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
966
1438
|
observe('launched', { seat, item, runId });
|
|
967
1439
|
return entry;
|
|
968
1440
|
} catch (error) {
|
|
969
|
-
|
|
1441
|
+
const failure: SeatEntry = { ...entry, status: 'outcome-unknown' };
|
|
1442
|
+
append(path, failure);
|
|
1443
|
+
if (action.kind === 'decision') {
|
|
1444
|
+
lastDecisionFailure = failure;
|
|
1445
|
+
const source = decisionSourceRef(seat, item);
|
|
1446
|
+
try { debug.log('seat-loop', 'decision-raise-failed', { source, error: String(error) }); }
|
|
1447
|
+
catch { /* A failed observation cannot interrupt the remaining items. */ }
|
|
1448
|
+
failedDecisions.add(itemKey(item));
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
970
1451
|
throw error;
|
|
971
1452
|
}
|
|
1453
|
+
}
|
|
972
1454
|
} catch (error) {
|
|
973
1455
|
turnError = error;
|
|
974
1456
|
throw error;
|
|
975
1457
|
} finally {
|
|
976
1458
|
// A turn ends even when there is no picked item or a launch fails.
|
|
1459
|
+
try { judgeSeatNeighbors(seat, config, deps, stateRoot, (deps.now ?? (() => new Date()))()); }
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
observe('neighbor-judgment-failed', { seat, error: String(error).slice(0, 200) });
|
|
1462
|
+
const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
|
|
1463
|
+
for (const neighbor of neighbors) {
|
|
1464
|
+
try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: 'unknown', action: null, mode: config.mode }); }
|
|
1465
|
+
catch { /* Observation cannot change the turn. */ }
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
977
1468
|
try { escalateStalls(seat, deps, config, (deps.now ?? (() => new Date()))()); }
|
|
978
1469
|
catch (error) {
|
|
979
1470
|
debug.log('org.stall', 'escalate-error', { seat, error: String(error).slice(0, 200) }, { level: 'warn' });
|