elanous 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +2 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +186 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/coord-post.sh +11 -1
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +163 -0
- package/scripts/heal/run-role.ts +14 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +223 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lessons/handbook-page.ts +85 -0
- package/scripts/release-loop/gate-node.ts +62 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +196 -0
- package/scripts/release-story/draft.ts +172 -0
- package/scripts/rhythm/daily.ts +383 -0
- package/scripts/rhythm/weekly.ts +294 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/test-deterministic.ts +57 -11
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +15 -10
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/build/llm-conflict-merge.ts +45 -8
- package/src/autopilot/build/merge-intent.ts +53 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +157 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +84 -20
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +44 -2
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +248 -45
- package/src/cli/rfc-status.ts +91 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +189 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-public.ts +86 -0
- package/src/context-bus/context-now-surfaces.ts +70 -4
- package/src/context-bus/context-now.ts +4 -3
- package/src/context-bus/long-term-memory.ts +201 -24
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +83 -18
- package/src/discord/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- package/src/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +13 -2
- package/src/graph-runner/runner.ts +111 -22
- package/src/harness/harness-cli-command.ts +178 -27
- package/src/harness/harness-incidents.ts +43 -12
- package/src/harness/harness-pod-dispatch.ts +2 -0
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +494 -0
- package/src/harness/helper-scan.ts +139 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/hq/promote.ts +301 -0
- package/src/index.ts +281 -27
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +72 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +32 -0
- package/src/loops/checker.ts +180 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +72 -6
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +59 -0
- package/src/loops/orchestrator/author-depth.ts +164 -0
- package/src/loops/orchestrator/author-input-check.ts +77 -0
- package/src/loops/orchestrator/author-ledger.ts +230 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +180 -0
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/tick.ts +302 -0
- package/src/loops/orchestrator/traffic.ts +165 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/registry.ts +454 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +34 -0
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +65 -2
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/schedules-read.ts +5 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/auto-start.ts +48 -0
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +28 -16
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/placement.ts +242 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +73 -15
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-loop/seat-loop.ts +495 -46
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/dev-pipeline.ts +6 -2
- package/src/self-dev/draft-sweep.ts +76 -0
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +8 -0
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +294 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +79 -0
- package/src/self-implement/host-regate.ts +25 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +114 -10
- package/src/self-implement/revert-guard.ts +62 -0
- package/src/self-implement/run-ledger.ts +1 -1
- package/src/self-implement/seams.ts +28 -8
- package/src/session/index.ts +2 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +86 -0
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +204 -16
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { Database } from 'bun:sqlite';
|
|
5
5
|
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';
|
|
15
|
+
import { loopEvent } from '../loops/observe.js';
|
|
11
16
|
import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
|
|
12
17
|
import { checkSeatBudget, type RunningSeatPod } from '../org/seat-budget.js';
|
|
13
18
|
import { DEFAULT_STALL_PARENTS, findStalls } from '../org/stall-escalation.js';
|
|
14
19
|
import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
|
|
15
20
|
import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
16
|
-
import {
|
|
21
|
+
import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
|
|
17
22
|
import { PersonaRegistry } from '../persona/registry.js';
|
|
18
|
-
import {
|
|
23
|
+
import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
|
|
24
|
+
import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
|
|
19
25
|
import { queryRunningRuns } from '../self-implement/running-runs.js';
|
|
20
26
|
import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
21
27
|
import { readSchedules, releasedVersion } from '../release-loop/feature-store.js';
|
|
@@ -33,11 +39,12 @@ export type OpCandidate =
|
|
|
33
39
|
| { kind: 'decision-delegation'; id: string | null; title?: string; category?: DecisionEntry['category']; dueAt?: string; verdict: 'review-delegation' | 'none' };
|
|
34
40
|
export type TcCandidate =
|
|
35
41
|
| { kind: 'merged-pr-yellow-cell'; version: string; id: string; title: string; pr: number; mergedAt: string }
|
|
36
|
-
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
42
|
+
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
43
|
+
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
37
44
|
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 };
|
|
38
|
-
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 };
|
|
45
|
+
export type SeatEntry = { seat: string; ts?: string; at: 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 };
|
|
39
46
|
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
|
|
40
|
-
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null };
|
|
47
|
+
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
|
|
41
48
|
export type SeatDeps = {
|
|
42
49
|
personaConfig?: PersonaLoopConfig;
|
|
43
50
|
personaRegistry?: PersonaRegistry;
|
|
@@ -52,6 +59,10 @@ export type SeatDeps = {
|
|
|
52
59
|
now?: () => Date;
|
|
53
60
|
read?: (path: string) => string;
|
|
54
61
|
run?: (args: string[]) => Promise<string>;
|
|
62
|
+
enqueue?: (seat: string, text: string, root: string, idempotencyKey: string) => Promise<{ id: string }>;
|
|
63
|
+
queueOutcome?: (id: string, root: string) => ReturnType<typeof harnessQueueOutcome>;
|
|
64
|
+
queueItems?: (root: string) => ReturnType<typeof listHarnessQueue>;
|
|
65
|
+
queueIdForKey?: (key: string, root: string) => string | undefined;
|
|
55
66
|
append?: (path: string, entry: SeatEntry) => void;
|
|
56
67
|
versions?: () => string[];
|
|
57
68
|
/** 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). */
|
|
@@ -74,6 +85,9 @@ export type SeatDeps = {
|
|
|
74
85
|
inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
|
|
75
86
|
/** Supply the receiving seat's answer; absent evidence must not invent a reply. */
|
|
76
87
|
reply?: (seat: SeatId, question: string, from: SeatId) => Promise<SeatReply | null> | SeatReply | null;
|
|
88
|
+
/** Judge a decision draft against the receiving seat's evidence; null leaves the request pending. */
|
|
89
|
+
crossCheck?: (seat: SeatId, draft: string, from: SeatId) => Promise<CrossCheckJudgment | null> | CrossCheckJudgment | null;
|
|
90
|
+
decisionsConfig?: UserConfig['decisions'];
|
|
77
91
|
};
|
|
78
92
|
|
|
79
93
|
const repoRoot = resolve(import.meta.dir, '../..');
|
|
@@ -134,8 +148,17 @@ export function personaShadowLedgerPath(personaId: string, root: string, now: Da
|
|
|
134
148
|
return join(root, 'persona-loop', personaId, `${seatDay(now)}.jsonl`);
|
|
135
149
|
}
|
|
136
150
|
|
|
151
|
+
export async function runPersonaLoopOnce(name: string, deps: SeatDeps = {}): Promise<PersonaShadowEntry> {
|
|
152
|
+
const entries = await pickPersonaShadows(deps, name);
|
|
153
|
+
return entries[0]!;
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<PersonaShadowEntry[]> {
|
|
138
157
|
if ((deps.personaConfig ?? getUserConfig().loops?.persona)?.enabled !== true) return [];
|
|
158
|
+
return pickPersonaShadows(deps);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<PersonaShadowEntry[]> {
|
|
139
162
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
140
163
|
const dir = deps.personaDir ?? (deps.root ? join(root, 'personas') : resolveStatePersonaDir());
|
|
141
164
|
const registry = deps.personaRegistry ?? new PersonaRegistry();
|
|
@@ -145,19 +168,31 @@ export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<Persona
|
|
|
145
168
|
}
|
|
146
169
|
const now = (deps.now ?? (() => new Date()))();
|
|
147
170
|
const entries: PersonaShadowEntry[] = [];
|
|
148
|
-
|
|
171
|
+
const profiles = registry.list().sort((a, b) => a.personaId.localeCompare(b.personaId));
|
|
172
|
+
const selected = name === undefined ? profiles : profiles.filter((profile) => profile.personaId.toLowerCase() === name.trim().toLowerCase()
|
|
173
|
+
|| profile.displayName.toLowerCase() === name.trim().toLowerCase());
|
|
174
|
+
if (name !== undefined && selected.length !== 1) throw new Error(selected.length ? `ambiguous persona name: ${name}` : `persona not found: ${name}`);
|
|
175
|
+
for (const profile of selected) {
|
|
149
176
|
const personaId = profile.personaId;
|
|
150
|
-
const
|
|
177
|
+
const todoFile = profile.todo ?? `${personaId}.todo.jsonl`;
|
|
178
|
+
const { todos, errors } = readPersonaTodos(dir, todoFile.slice(0, -'.todo.jsonl'.length));
|
|
151
179
|
for (const error of errors) observe('persona-todo-error', { personaId, error });
|
|
152
|
-
const
|
|
153
|
-
const
|
|
180
|
+
const candidates = orderedPersonaTodos(todos);
|
|
181
|
+
const picked = pickNext({ requests: candidates.map((candidate) => ({
|
|
182
|
+
source: 'request', id: candidate.id, title: candidate.title, text: candidate.title, createdAt: candidate.createdAt,
|
|
183
|
+
})), checklist: [], role: '' }, [], { shadow: true });
|
|
184
|
+
const todo = picked ? candidates.find((candidate) => candidate.id === picked.id) ?? null : null;
|
|
185
|
+
const planned = name !== undefined && todo && picked ? planAction(picked, profile.seat) : undefined;
|
|
186
|
+
const entry: PersonaShadowEntry = { personaId, ts: now.toISOString(), status: todo ? 'shadow' : 'skipped-empty', todo,
|
|
187
|
+
...(planned ? { action: planned.kind, what: planned.text } : {}) };
|
|
154
188
|
const path = personaShadowLedgerPath(personaId, root, now);
|
|
155
189
|
if (deps.appendPersona) deps.appendPersona(path, entry);
|
|
156
190
|
else {
|
|
157
191
|
mkdirSync(dirname(path), { recursive: true });
|
|
158
192
|
appendFileSync(path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
159
193
|
}
|
|
160
|
-
|
|
194
|
+
try { debug.log('persona.loop', 'picked', { personaId, status: entry.status, todoId: todo?.id ?? null, title: todo?.title ?? null, action: entry.action ?? null }); }
|
|
195
|
+
catch { /* observation cannot alter the shadow ledger */ }
|
|
161
196
|
entries.push(entry);
|
|
162
197
|
}
|
|
163
198
|
return entries;
|
|
@@ -167,6 +202,25 @@ export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promis
|
|
|
167
202
|
const result = await runSeatLoopOnce(seat, deps);
|
|
168
203
|
try { await runPersonaShadowOnce(deps); }
|
|
169
204
|
catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
|
|
205
|
+
try {
|
|
206
|
+
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'off' };
|
|
207
|
+
const profile = result.status === 'skipped-off' ? 'off' : config.mode;
|
|
208
|
+
const context = { runId: 'runId' in result && result.runId ? result.runId : `seat-turn-${randomUUID()}`,
|
|
209
|
+
profile, sourceRef: `docs/roles/${seat}.md` };
|
|
210
|
+
const loopId = `${seat.toLowerCase()}-seat`;
|
|
211
|
+
loopEvent(loopId, 'tick', { ...context, outcome: result.status, reason: result.status });
|
|
212
|
+
if ('inquiry' in result && result.inquiry && (result.status === 'asked' || result.status === 'hitl')) {
|
|
213
|
+
loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'delegation',
|
|
214
|
+
from: seat, to: result.inquiry.to, itemId: result.item?.id });
|
|
215
|
+
}
|
|
216
|
+
if ('escalated' in result && result.escalated && result.status === 'skipped-budget') {
|
|
217
|
+
loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'delegation',
|
|
218
|
+
from: seat, to: (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat], itemId: result.item?.id });
|
|
219
|
+
} else if (result.status === 'skipped-budget') {
|
|
220
|
+
loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'requeue',
|
|
221
|
+
itemId: result.item?.id });
|
|
222
|
+
}
|
|
223
|
+
} catch { /* observation cannot change a completed seat turn */ }
|
|
170
224
|
return result;
|
|
171
225
|
}
|
|
172
226
|
|
|
@@ -181,6 +235,64 @@ function versionOrder(a: string, b: string): number {
|
|
|
181
235
|
|
|
182
236
|
function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
|
|
183
237
|
|
|
238
|
+
function handledItemKey(item: SeatItem): string {
|
|
239
|
+
return `${itemKey(item)}:${JSON.stringify([item.evidenceHash ?? null, itemSnapshot(item)])}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function queueKey(seat: string, item: SeatItem): string {
|
|
243
|
+
return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 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
|
|
247
|
+
* seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
|
|
248
|
+
function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
|
|
249
|
+
const unsettled = (entry: SeatEntry) => (entry.status === 'attempting' || entry.status === 'outcome-unknown') && entry.action === 'harness' && !entry.queueId && !!entry.item;
|
|
250
|
+
const attempting = ledger.filter(unsettled);
|
|
251
|
+
if (attempting.length === 0) return [...ledger];
|
|
252
|
+
let rows: ReturnType<typeof listHarnessQueue>;
|
|
253
|
+
try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
|
|
254
|
+
catch (error) { observe('queue-recovery-unreadable', { error: String(error).slice(0, 200) }); return [...ledger]; }
|
|
255
|
+
return ledger.map((entry, index) => {
|
|
256
|
+
const item = entry.item;
|
|
257
|
+
if (!unsettled(entry) || !item) return entry;
|
|
258
|
+
const key = handledItemKey(item);
|
|
259
|
+
// A later row for the same item already settled this attempt.
|
|
260
|
+
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;
|
|
261
|
+
const row = rows.find((candidate) => candidate.seat === entry.seat && candidate.idempotencyKey === queueKey(entry.seat, item)
|
|
262
|
+
&& candidate.input === seatTaskText(entry.seat, item));
|
|
263
|
+
// The row may already have left the queue (finished · cancelled); its settled id is kept by key.
|
|
264
|
+
const id = row?.id ?? (deps.queueIdForKey ?? ((key: string, stateRoot: string) => harnessQueueIdForKey(key, { root: stateRoot })))(queueKey(entry.seat, item), root);
|
|
265
|
+
if (!id) return entry;
|
|
266
|
+
observe('queue-recovered', { seat: entry.seat, item: item.id, queueId: id, from: row ? 'queue' : 'key-marker' });
|
|
267
|
+
return { ...entry, status: 'queued' as const, queueId: id };
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function reconciledQueueLedger(recorded: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
|
|
272
|
+
const ledger = recoverAttemptingQueueIds(recorded, root, deps);
|
|
273
|
+
const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
|
|
274
|
+
const retryable = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
|
|
275
|
+
&& outcome(entry.queueId, root) === 'retryable' && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
|
|
276
|
+
const pending = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
|
|
277
|
+
&& ['pending', 'unknown'].includes(outcome(entry.queueId, root)) && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
|
|
278
|
+
const succeeded = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
|
|
279
|
+
&& outcome(entry.queueId, root) === 'succeeded' && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
|
|
280
|
+
return ledger.map((entry) => {
|
|
281
|
+
if (entry.status === 'attempting' && entry.item) {
|
|
282
|
+
const key = `${entry.seat}:${handledItemKey(entry.item)}`;
|
|
283
|
+
if (retryable.has(key) && !pending.has(key) && !succeeded.has(key)) return { ...entry, status: 'refused' as const };
|
|
284
|
+
}
|
|
285
|
+
if (entry.status !== 'queued' || !entry.queueId) return entry;
|
|
286
|
+
const state = outcome(entry.queueId, root);
|
|
287
|
+
if (state === 'retryable') {
|
|
288
|
+
observe('queue-retryable', { seat: entry.seat, item: entry.item?.id, queueId: entry.queueId });
|
|
289
|
+
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,
|
|
290
|
+
reason: 'AUTOQ failed or cancelled; eligible for retry' };
|
|
291
|
+
}
|
|
292
|
+
return { ...entry, status: state === 'succeeded' ? 'launched' as const : entry.status };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
184
296
|
function itemSnapshot(item: SeatItem): string {
|
|
185
297
|
return JSON.stringify([item.title, item.text, item.evidence ?? null, item.status ?? null]);
|
|
186
298
|
}
|
|
@@ -195,12 +307,15 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
|
|
|
195
307
|
}
|
|
196
308
|
|
|
197
309
|
// RM3 (asked/answered · decision-card receipts for seat inquiries) ⊕ main (checklist cells re-picked by evidence, not by a shadow row).
|
|
198
|
-
function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string): Set<string> {
|
|
310
|
+
function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string, liveSafe = false): Set<string> {
|
|
199
311
|
const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
|
|
200
312
|
return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
|
|
313
|
+
|| entry.status === 'resolved-by-neighbor'
|
|
314
|
+
// 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.
|
|
315
|
+
|| (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
|
|
201
316
|
|| (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
|
|
202
317
|
|| (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
|
|
203
|
-
|| (!inquiryRow(entry) && entry.status === 'outcome-unknown')
|
|
318
|
+
|| (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
|
|
204
319
|
|| (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
|
|
205
320
|
later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
|
|
206
321
|
&& later.item.version === entry.item?.version && later.item.id === entry.item?.id
|
|
@@ -208,12 +323,14 @@ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: strin
|
|
|
208
323
|
|| (shadow && entry.status === 'shadow' && entry.action !== 'wait' && entry.item?.source !== 'checklist')
|
|
209
324
|
|| (entry.status === 'launched' && entry.item?.source !== 'checklist'))
|
|
210
325
|
.filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string')
|
|
211
|
-
.map((entry) => itemKey(entry.item)));
|
|
326
|
+
.map((entry) => !liveSafe || inquiryRow(entry) ? itemKey(entry.item) : handledItemKey(entry.item)));
|
|
212
327
|
}
|
|
213
328
|
|
|
214
329
|
function eligibleItem(item: SeatItem, ledger: readonly SeatEntry[], handled: ReadonlySet<string>, shadow: boolean): boolean {
|
|
215
330
|
const key = itemKey(item);
|
|
216
|
-
if (handled.has(key)) return false;
|
|
331
|
+
if (handled.has(key) || handled.has(handledItemKey(item))) return false;
|
|
332
|
+
const rejected = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key && entry.status === 'rejected-no-evidence');
|
|
333
|
+
if (rejected?.item && itemSnapshot(rejected.item) === itemSnapshot(item)) return false;
|
|
217
334
|
const lastWait = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key
|
|
218
335
|
&& (entry.status === 'wait' || (shadow && entry.status === 'shadow' && entry.action === 'wait')));
|
|
219
336
|
return !lastWait?.item || itemSnapshot(lastWait.item) !== itemSnapshot(item);
|
|
@@ -230,7 +347,7 @@ export function alreadyHandled(item: SeatItem, ledger: readonly SeatEntry[]): bo
|
|
|
230
347
|
const at = Date.parse(entry.at || entry.ts || '');
|
|
231
348
|
return entry.seat === item.seat && at >= now - CHECKLIST_HANDLED_WINDOW_MS && at <= now
|
|
232
349
|
// A shadowed seat question was never delivered — it must be picked again once delivery is on (RM3 · TC merge 10-03).
|
|
233
|
-
&& (entry.status === 'launched' || (entry.status === 'shadow' && !entry.inquiry))
|
|
350
|
+
&& (entry.status === 'launched' || entry.status === 'queued' || (entry.status === 'shadow' && !entry.inquiry))
|
|
234
351
|
&& entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version === item.version
|
|
235
352
|
&& entry.item.title === item.title && entry.item.evidenceHash === item.evidenceHash;
|
|
236
353
|
});
|
|
@@ -247,7 +364,8 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
247
364
|
const read = deps.read ?? defaultRead;
|
|
248
365
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
249
366
|
const activeLedger = ledger ?? readSeatLedger(seat, deps, (deps.now ?? (() => new Date()))());
|
|
250
|
-
const
|
|
367
|
+
const mode = (deps.config ?? getUserConfig().loops?.seat)?.mode;
|
|
368
|
+
const consumed = handledKeys(activeLedger, mode === 'shadow', root, mode === 'live-safe');
|
|
251
369
|
const requests = new Map<string, Record<string, unknown>>();
|
|
252
370
|
for (const row of rows(read(join(root, 'seat-requests', 'requests.jsonl')))) {
|
|
253
371
|
if (typeof row.key === 'string') requests.set(row.key, row);
|
|
@@ -285,7 +403,7 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
285
403
|
try { fromSchedule = readSchedules().map((row) => row.version); } catch { /* ledger unreadable — directories only */ }
|
|
286
404
|
return [...new Set([...fromDirs, ...fromSchedule])].filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
287
405
|
});
|
|
288
|
-
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
|
|
406
|
+
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items);
|
|
289
407
|
let shipped: string | undefined;
|
|
290
408
|
try { shipped = releasedVersion(releaseRoot); }
|
|
291
409
|
catch (error) {
|
|
@@ -303,9 +421,10 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
303
421
|
debug.log('seat.loop', 'schedule-unreadable', { seat, error: String(error).slice(0, 200) });
|
|
304
422
|
}
|
|
305
423
|
const checklist: SeatItem[] = [];
|
|
424
|
+
const closed: string[] = [];
|
|
306
425
|
for (const version of open === null ? [] : versions().sort(versionOrder)) {
|
|
307
426
|
if (!open!.has(version)) {
|
|
308
|
-
|
|
427
|
+
closed.push(version);
|
|
309
428
|
continue;
|
|
310
429
|
}
|
|
311
430
|
let items: ReturnType<typeof itemsOf>;
|
|
@@ -325,7 +444,15 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
325
444
|
}
|
|
326
445
|
}
|
|
327
446
|
}
|
|
447
|
+
if (closed.length > 0) {
|
|
448
|
+
debug.log('seat.loop', 'skip-closed-versions', { seat, count: closed.length, newest: closed.at(-1)!, oldest: closed[0]! });
|
|
449
|
+
}
|
|
328
450
|
checklist.sort((a, b) => versionOrder(a.version!, b.version!) || a.id.localeCompare(b.id));
|
|
451
|
+
const ownedEvidence = checklist.filter((item) => item.evidence?.trim())
|
|
452
|
+
.map((item) => `${item.id} · ${item.title}: ${item.evidence!.trim()}`).join('\n').slice(0, 8_000);
|
|
453
|
+
for (const request of pending) {
|
|
454
|
+
if (request.source === 'seat-question' && ownedEvidence) request.evidence = ownedEvidence;
|
|
455
|
+
}
|
|
329
456
|
return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
|
|
330
457
|
}
|
|
331
458
|
|
|
@@ -375,7 +502,7 @@ function readTcChecklist(root: string): Array<{ version: string; id: string; tit
|
|
|
375
502
|
const db = new Database(path, { readonly: true, strict: true });
|
|
376
503
|
try {
|
|
377
504
|
return db.query(`SELECT a.version, f.id, COALESCE(a.title_override, f.title) AS title, a.status, a.owner
|
|
378
|
-
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status
|
|
505
|
+
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN ('OP', 'TC', 'MK', 'UX')
|
|
379
506
|
ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
|
|
380
507
|
} finally { db.close(); }
|
|
381
508
|
}
|
|
@@ -461,16 +588,27 @@ async function tcCandidates(deps: SeatDeps, now: Date): Promise<TcCandidate[]> {
|
|
|
461
588
|
const candidates: TcCandidate[] = [];
|
|
462
589
|
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
463
590
|
try {
|
|
464
|
-
const cells =
|
|
591
|
+
const cells = deps.checklistItems && deps.schedules
|
|
465
592
|
? deps.schedules().flatMap(({ version }) => deps.checklistItems!(version).map((cell) => ({ ...cell, version })))
|
|
466
593
|
: readTcChecklist(releaseRoot);
|
|
594
|
+
let shipped: string | undefined;
|
|
595
|
+
try { shipped = releasedVersion(releaseRoot); }
|
|
596
|
+
catch (error) { observe('tc-release-unreadable', { seat: 'TC', error: String(error).slice(0, 200) }); }
|
|
597
|
+
const openVersions = deps.schedules ? new Set(deps.schedules().filter(({ cutAt }) => Number.isFinite(Date.parse(cutAt))
|
|
598
|
+
&& Date.parse(cutAt) > now.getTime()).map(({ version }) => version)) : null;
|
|
467
599
|
for (const cell of cells) {
|
|
468
|
-
if (cell.owner !== 'TC'
|
|
600
|
+
if (cell.owner !== 'TC' && ((shipped && versionOrder(cell.version, shipped) <= 0) || (openVersions && !openVersions.has(cell.version)))) continue;
|
|
601
|
+
if (cell.status !== 'yellow' && cell.status !== 'red') continue;
|
|
602
|
+
if (!cell.owner || !isSeatId(cell.owner)) continue;
|
|
603
|
+
if (cell.owner !== 'TC') {
|
|
604
|
+
if (cell.status === 'red') candidates.push({ kind: 'other-seat-cell-defect', version: cell.version, id: cell.id, title: cell.title, to: cell.owner });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
469
607
|
for (const pr of merged) {
|
|
470
608
|
const id = cell.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
471
609
|
const reference = new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`);
|
|
472
610
|
if (reference.test(pr.title) || reference.test(pr.body ?? '')) {
|
|
473
|
-
candidates.push({ kind: 'merged-pr-yellow-cell', version: cell.version, id: cell.id, title: cell.title, pr: pr.number, mergedAt: pr.mergedAt! });
|
|
611
|
+
if (cell.owner === 'TC' && cell.status === 'yellow') candidates.push({ kind: 'merged-pr-yellow-cell', version: cell.version, id: cell.id, title: cell.title, pr: pr.number, mergedAt: pr.mergedAt! });
|
|
474
612
|
}
|
|
475
613
|
}
|
|
476
614
|
}
|
|
@@ -523,6 +661,10 @@ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, no
|
|
|
523
661
|
debug.log('org.stall', 'escalate', { ...data, mode: 'shadow' });
|
|
524
662
|
continue;
|
|
525
663
|
}
|
|
664
|
+
if (config.mode === 'live-safe' && !isRescueAllowedAction('alert')) {
|
|
665
|
+
observe('refused', { seat, item: item.id, action: 'alert', reason: 'policy denied internal stall alert' });
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
526
668
|
const key = JSON.stringify([schedule.version, item.id, from, to, reason, startedAt]);
|
|
527
669
|
const message: MessageEnvelope = { from, to, kind: 'stall-escalation',
|
|
528
670
|
body: `[stall:${key}] ${schedule.version} 체크리스트 ${item.id} · ${item.title} · ${reason} · ${stalledMin}분 정체 (${from} → ${to})` };
|
|
@@ -541,8 +683,8 @@ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, no
|
|
|
541
683
|
}
|
|
542
684
|
|
|
543
685
|
// In shadow mode a shadowed item counts as handled, so a rehearsal day walks the queue like the seat would.
|
|
544
|
-
export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
|
|
545
|
-
const handled = handledKeys(ledger, opts.shadow === true, opts.root);
|
|
686
|
+
export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; liveSafe?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
|
|
687
|
+
const handled = handledKeys(ledger, opts.shadow === true, opts.root, opts.liveSafe === true);
|
|
546
688
|
return [...inputs.requests, ...inputs.checklist].find((item) => {
|
|
547
689
|
if (alreadyHandled(item, ledger)) {
|
|
548
690
|
debug.log('seat.loop', 'skip-handled', { id: item.id });
|
|
@@ -606,9 +748,85 @@ export function seatTaskText(seat: string, item: SeatItem): string {
|
|
|
606
748
|
return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
|
|
607
749
|
}
|
|
608
750
|
|
|
751
|
+
function xcheckEvent(event: string, seat: string, neighbor: string, item: string, outcome: string): void {
|
|
752
|
+
try { debug.log('decisions.xcheck', event, { seat, neighbor, item, outcome }); }
|
|
753
|
+
catch { /* Observation does not alter delivery. */ }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function neighborLastSeen(root: string, isolated: boolean, now: Date, wanted: ReadonlySet<string>): Map<string, string> {
|
|
757
|
+
const path = isolated ? join(root, 'surface_events.db') : surfaceEventsDbPath();
|
|
758
|
+
const last = new Map<string, string>();
|
|
759
|
+
if (!existsSync(path)) return last;
|
|
760
|
+
const db = new Database(path, { readonly: true });
|
|
761
|
+
try {
|
|
762
|
+
// Same outbound seat events and refs.seat as context day; unlike its 24h display window,
|
|
763
|
+
// look back across the whole journal so a genuinely old ACK can be judged absent.
|
|
764
|
+
// Newest first and stop once every wanted seat has an ACK, so a long journal is not read and sorted in full each turn.
|
|
765
|
+
const statement = db.prepare(`SELECT ts, refs FROM events
|
|
766
|
+
WHERE surface IN ('coord:channel', 'context:session') AND direction='outbound' AND ts<=? ORDER BY ts DESC`);
|
|
767
|
+
try {
|
|
768
|
+
const events = statement.iterate(now.toISOString()) as Iterable<{ ts: string; refs: string | null }>;
|
|
769
|
+
for (const event of events) {
|
|
770
|
+
let seat: unknown;
|
|
771
|
+
try { seat = (JSON.parse(event.refs ?? '{}') as { seat?: unknown }).seat; } catch { continue; }
|
|
772
|
+
if (typeof seat === 'string' && wanted.has(seat) && !last.has(seat)
|
|
773
|
+
&& Number.isFinite(Date.parse(event.ts)) && Date.parse(event.ts) <= now.getTime()) last.set(seat, event.ts);
|
|
774
|
+
if (last.size >= wanted.size) break;
|
|
775
|
+
}
|
|
776
|
+
} finally { statement.finalize(); }
|
|
777
|
+
return last;
|
|
778
|
+
} finally { db.close(); }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function neighborSeat(id: string): string | null {
|
|
782
|
+
const match = /^(op|tc|mk|ux)-seat$/i.exec(id);
|
|
783
|
+
return match ? match[1]!.toUpperCase() : null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps, root: string, now: Date): void {
|
|
787
|
+
const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
|
|
788
|
+
if (!neighbors.length) return;
|
|
789
|
+
let seen = new Map<string, string>();
|
|
790
|
+
let unreadable = false;
|
|
791
|
+
const wanted = new Set(neighbors.flatMap((neighbor) => { const source = neighborSeat(neighbor.id); return source ? [source] : []; }));
|
|
792
|
+
try { seen = wanted.size ? neighborLastSeen(root, !!deps.root, now, wanted) : seen; }
|
|
793
|
+
catch (error) { unreadable = true; observe('neighbor-source-unreadable', { seat, error: String(error).slice(0, 200) }); }
|
|
794
|
+
for (const neighbor of neighbors) {
|
|
795
|
+
try {
|
|
796
|
+
const source = neighborSeat(neighbor.id);
|
|
797
|
+
const lastSeenAt = source ? seen.get(source) : undefined;
|
|
798
|
+
const judgment = judgeNeighbor({ neighbor, lastSeenAt: unreadable ? null : lastSeenAt, now });
|
|
799
|
+
// action = what this turn actually did; plannedAction = what onAbsent asks for (shadow only records it).
|
|
800
|
+
let action: string | null = null;
|
|
801
|
+
const plannedAction = judgment.state === 'absent' ? judgment.action?.action ?? null : null;
|
|
802
|
+
if (judgment.state === 'absent') {
|
|
803
|
+
if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
|
|
804
|
+
const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
|
|
805
|
+
new DecisionLedger({ stateDir: root, now: () => now }).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
|
|
806
|
+
scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
|
|
807
|
+
c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
|
|
808
|
+
q: '결측 이웃의 업무를 어떻게 처리할까?' },
|
|
809
|
+
options: [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
|
|
810
|
+
{ key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }],
|
|
811
|
+
recommendation: { skipped: true, reason: '결측만으로 대행이나 보류를 자동 결정하지 않는다' },
|
|
812
|
+
raisedBy: { agent: 'seat-loop' }, refs: [ref], raisedAt: now.toISOString(),
|
|
813
|
+
crossCheckSkipped: '이웃 결측으로 교차 확인 불가' }, ref);
|
|
814
|
+
action = 'decision-card';
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: judgment.state, action, plannedAction, mode: config.mode }); }
|
|
818
|
+
catch { /* Observation cannot change the judgment. */ }
|
|
819
|
+
} catch (error) {
|
|
820
|
+
observe('neighbor-judgment-failed', { seat, neighbor: neighbor.id, error: String(error).slice(0, 200) });
|
|
821
|
+
try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: 'unknown', action: null, mode: config.mode }); }
|
|
822
|
+
catch { /* Observation cannot change the turn. */ }
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
609
827
|
export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
610
828
|
if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
|
|
611
|
-
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: '
|
|
829
|
+
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
|
|
612
830
|
if (config.mode === 'off' || !(config.seats ?? ['MK']).includes(seat)) {
|
|
613
831
|
observe('skipped-off', { seat });
|
|
614
832
|
return { seat, status: 'skipped-off' };
|
|
@@ -617,13 +835,116 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
617
835
|
const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
|
|
618
836
|
const directory = dirname(path);
|
|
619
837
|
return withSeatLock(directory, async () => {
|
|
620
|
-
const ledger = readSeatLedger(seat, deps, now);
|
|
621
838
|
const stateRoot = deps.root ?? effectiveInstanceRoot();
|
|
839
|
+
const recorded = readSeatLedger(seat, deps, now);
|
|
840
|
+
const ledger = config.mode === 'live-safe' ? reconciledQueueLedger(recorded, stateRoot, deps) : recorded;
|
|
622
841
|
let turnError: unknown;
|
|
623
842
|
try {
|
|
843
|
+
if (config.mode === 'on' && config.questions === 'on') {
|
|
844
|
+
const proposals = ledger.filter((row) => row.status === 'awaiting-resolution' && row.xcheckQuestionId !== undefined
|
|
845
|
+
&& ((row.item?.source === 'checklist' && row.item.version) || row.item?.source === 'seat-question'));
|
|
846
|
+
for (const proposal of proposals) {
|
|
847
|
+
if (ledger.some((row) => row.status === 'resolved-by-neighbor' && row.xcheckQuestionId === proposal.xcheckQuestionId)) continue;
|
|
848
|
+
// Review round 3 ②: a seat question has no checklist cell — it is settled when this seat answers it later
|
|
849
|
+
// (new evidence) and otherwise follows the same deadline escalation below.
|
|
850
|
+
const isQuestion = proposal.item!.source === 'seat-question';
|
|
851
|
+
if (isQuestion && ledger.some((row) => row.item && itemKey(row.item) === itemKey(proposal.item!)
|
|
852
|
+
&& (row.status === 'answered' || row.status === 'hitl') && Date.parse(row.at) > Date.parse(proposal.at))) continue;
|
|
853
|
+
let cell: { status: string } | undefined;
|
|
854
|
+
if (!isQuestion) try {
|
|
855
|
+
cell = (deps.checklistItems ?? ((version: string) => listChecklist(version, stateRoot).items))(proposal.item!.version!)
|
|
856
|
+
.find((candidate) => candidate.id === proposal.item!.id && candidate.owner === seat);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
observe('xcheck-resolution-verification-failed', { seat, neighbor: proposal.xcheckNeighbor, item: itemKey(proposal.item!), error: String(error).slice(0, 200) });
|
|
859
|
+
}
|
|
860
|
+
if (cell?.status === 'green' || cell?.status === 'done') {
|
|
861
|
+
const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'resolved-by-neighbor' };
|
|
862
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
863
|
+
xcheckEvent('resolved-by-neighbor', seat, proposal.xcheckNeighbor ?? '', itemKey(proposal.item!), cell.status);
|
|
864
|
+
return entry;
|
|
865
|
+
}
|
|
866
|
+
// Review round 3 ①: a neighbor's «I can resolve it» has a deadline — if the cell is still open after the
|
|
867
|
+
// cross-check wait, escalate to a CEO card instead of excluding the item forever.
|
|
868
|
+
const waited = proposal.xcheckWaitMinutes ?? 120;
|
|
869
|
+
const proposedAt = Date.parse(proposal.at);
|
|
870
|
+
if ((cell || isQuestion) && Number.isFinite(proposedAt) && now.getTime() - proposedAt >= waited * 60_000) {
|
|
871
|
+
const neighbor = proposal.xcheckNeighbor ?? '';
|
|
872
|
+
const question = proposal.inquiry?.question ?? proposal.item!.title;
|
|
873
|
+
const note = `${proposal.xcheckNote ?? ''} — 해결 가능하다고 했으나 ${waited}분 안 해소 안 됨`.trim();
|
|
874
|
+
if (!decisionReceipt(stateRoot, seat, proposal.item!)) {
|
|
875
|
+
await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
|
|
876
|
+
'--s', `${seat} 질문 · ${proposal.item!.title}`, '--c', '이웃 자리 해결 제안이 기한 안에 해소되지 않았다',
|
|
877
|
+
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
878
|
+
'--skip-recommend', '근거가 부족하다', '--xcheck', `${neighbor}:${note}`,
|
|
879
|
+
'--agent', 'seat-loop', '--ref', decisionRef(seat, proposal.item!), '--json']);
|
|
880
|
+
}
|
|
881
|
+
const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'hitl' };
|
|
882
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
883
|
+
xcheckEvent('raised', seat, neighbor, itemKey(proposal.item!), 'resolution-timed-out');
|
|
884
|
+
return entry;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const received = seatCrossChecks(stateRoot, seat as SeatId).filter((question) => !crossCheckAnswer(stateRoot, question));
|
|
888
|
+
const receivedInputs = received.length ? await gatherSeatInputs(seat, deps, ledger) : null;
|
|
889
|
+
const judgmentEvidence = receivedInputs?.checklist.filter((item) => item.evidence?.trim())
|
|
890
|
+
.map((item) => `${item.id} · ${item.title}: ${item.evidence!.trim()}`).join('\n').slice(0, 8_000) ?? '';
|
|
891
|
+
const pending = received.find((question) => {
|
|
892
|
+
const senderRows = readSeatLedger(question.from, { root: stateRoot }, now).filter((row) => row.xcheckQuestionId === question.id);
|
|
893
|
+
const latest = senderRows.at(-1);
|
|
894
|
+
if (latest?.status === 'hitl' || latest?.status === 'resolved-by-neighbor' || latest?.status === 'awaiting-resolution') return false;
|
|
895
|
+
const request = senderRows.find((row) => row.status === 'awaiting-xcheck' && row.xcheckRequestedAt);
|
|
896
|
+
const waitMinutes = request?.xcheckWaitMinutes ?? 120;
|
|
897
|
+
if (request?.xcheckRequestedAt && now.getTime() - Date.parse(request.xcheckRequestedAt) >= waitMinutes * 60_000) return false;
|
|
898
|
+
return !ledger.some((row) => row.xcheckQuestionId === question.id && row.status === 'awaiting-xcheck'
|
|
899
|
+
&& row.xcheckJudgmentEvidence === judgmentEvidence);
|
|
900
|
+
});
|
|
901
|
+
if (pending) {
|
|
902
|
+
const append = deps.append ?? defaultAppend;
|
|
903
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'awaiting-xcheck',
|
|
904
|
+
action: 'seat-answer', xcheckQuestionId: pending.id, xcheckNeighbor: pending.from as SeatId,
|
|
905
|
+
xcheckJudgmentEvidence: judgmentEvidence };
|
|
906
|
+
try {
|
|
907
|
+
const judge = deps.crossCheck ?? (async (receiver: SeatId, draft: string, from: SeatId): Promise<CrossCheckJudgment | null> => {
|
|
908
|
+
const inputs = receivedInputs!;
|
|
909
|
+
const evidence = judgmentEvidence;
|
|
910
|
+
if (!evidence) throw new Error('cross-check judgment requires receiving-seat evidence');
|
|
911
|
+
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
912
|
+
`[${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 };
|
|
913
|
+
const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
914
|
+
return value as CrossCheckJudgment;
|
|
915
|
+
});
|
|
916
|
+
const answer = await judge(seat as SeatId, pending.body, pending.from as SeatId);
|
|
917
|
+
if (!answer || typeof answer.agree !== 'boolean' || typeof answer.resolves !== 'boolean'
|
|
918
|
+
|| typeof answer.note !== 'string' || !answer.note.trim()) throw new Error('invalid cross-check judgment');
|
|
919
|
+
answerCrossCheck(stateRoot, pending, answer);
|
|
920
|
+
entry.status = 'answered';
|
|
921
|
+
entry.xcheckNote = answer.note.trim();
|
|
922
|
+
append(path, entry);
|
|
923
|
+
xcheckEvent('answered', seat, pending.from, String(pending.id), answer.resolves ? 'resolves' : answer.agree ? 'agree' : 'dissent');
|
|
924
|
+
} catch (error) {
|
|
925
|
+
append(path, entry);
|
|
926
|
+
xcheckEvent('answered', seat, pending.from, String(pending.id), `judgment-failed: ${String(error).slice(0, 200)}`);
|
|
927
|
+
}
|
|
928
|
+
return entry;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
624
931
|
if (seat === 'TC') {
|
|
625
932
|
const candidates = await tcCandidates(deps, now);
|
|
626
933
|
for (const candidate of candidates) {
|
|
934
|
+
if (candidate.kind === 'other-seat-cell-defect') {
|
|
935
|
+
const question = `${candidate.version} 체크리스트 ${candidate.id} · ${candidate.title}: ${candidate.to} 칸 결함을 확인해 주세요.`;
|
|
936
|
+
const recorded = recordSeatAskShadow(stateRoot, { from: 'TC', to: candidate.to, itemId: candidate.id, version: candidate.version, question }, now.getTime());
|
|
937
|
+
const inLedger = ledger.some((row) => row.status === 'shadow' && row.candidate?.kind === 'other-seat-cell-defect'
|
|
938
|
+
&& row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
|
|
939
|
+
if (!inLedger) {
|
|
940
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
941
|
+
item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
|
|
942
|
+
action: 'seat-question', inquiry: { to: candidate.to, question } };
|
|
943
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
944
|
+
}
|
|
945
|
+
if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
627
948
|
if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
|
|
628
949
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
|
|
629
950
|
(deps.append ?? defaultAppend)(path, entry);
|
|
@@ -685,17 +1006,45 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
685
1006
|
const inputs = await gatherSeatInputs(seat, deps, ledger);
|
|
686
1007
|
const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
|
|
687
1008
|
const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
|
|
688
|
-
const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
|
|
689
|
-
|
|
690
|
-
|
|
1009
|
+
const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
|
|
1010
|
+
&& eligibleItem(candidate, ledger, questionHandled, questionsMode !== 'on')
|
|
1011
|
+
&& !ledger.some((row) => row.status === 'awaiting-resolution' && row.item
|
|
1012
|
+
&& itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate)));
|
|
1013
|
+
const ordinary = pickNext({ ...inputs, requests: inputs.requests.filter((candidate) => candidate.source !== 'seat-question'),
|
|
1014
|
+
checklist: inputs.checklist.filter((candidate) => !ledger.some((row) => row.status === 'awaiting-resolution'
|
|
1015
|
+
&& row.item && itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate))) }, ledger,
|
|
1016
|
+
{ shadow: config.mode === 'shadow', liveSafe: config.mode === 'live-safe', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
|
|
691
1017
|
const item = pendingQuestion ?? ordinary;
|
|
692
1018
|
const append = deps.append ?? defaultAppend;
|
|
1019
|
+
const revisedProposal = item?.source === 'checklist' && ledger.some((row) => row.status === 'awaiting-resolution'
|
|
1020
|
+
&& row.item && itemKey(row.item) === itemKey(item) && itemSnapshot(row.item) !== itemSnapshot(item));
|
|
1021
|
+
if (config.mode === 'live-safe' && !pendingQuestion) {
|
|
1022
|
+
const pendingRows = (deps.queueItems ?? ((root: string) => listHarnessQueue({ root })))(stateRoot);
|
|
1023
|
+
for (const candidate of [...inputs.requests, ...inputs.checklist].filter((row) => row.source !== 'seat-question')) {
|
|
1024
|
+
const pending = pendingRows.find((row) => row.seat === seat && row.kind === 'say'
|
|
1025
|
+
&& row.idempotencyKey === queueKey(seat, candidate) && row.input === seatTaskText(seat, candidate)
|
|
1026
|
+
&& ['queued', 'launching', 'launched'].includes(row.status)
|
|
1027
|
+
&& ['pending', 'unknown'].includes((deps.queueOutcome ?? ((id: string, root: string) => harnessQueueOutcome(id, { root })))(row.id, stateRoot)));
|
|
1028
|
+
if (!pending) continue;
|
|
1029
|
+
if (ledger.some((row) => row.queueId === pending.id && row.status === 'queued')) continue;
|
|
1030
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'queued', item: candidate, action: 'harness', queueId: pending.id };
|
|
1031
|
+
append(path, entry);
|
|
1032
|
+
observe('queued', { seat, item: candidate, queueId: pending.id, deduplicated: true });
|
|
1033
|
+
return entry;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
693
1036
|
// 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.
|
|
694
|
-
if (item && (
|
|
1037
|
+
if (item && (!revisedProposal || planAction(item, seat).kind === 'decision')
|
|
1038
|
+
&& (item.source === 'seat-question' || (config.mode !== 'live-safe' && (deps.inquire || (config.mode === 'on' && config.questions !== undefined))))) {
|
|
695
1039
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1040
|
+
const unfinished = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
|
|
1041
|
+
&& !(item.source === 'seat-question' && ledger.some((prior) => prior.status === 'awaiting-resolution'
|
|
1042
|
+
&& prior.item && itemKey(prior.item) === itemKey(item) && itemSnapshot(prior.item) !== itemSnapshot(item)))
|
|
1043
|
+
&& (row.status === 'attempting' || row.status === 'outcome-unknown' || row.status === 'awaiting-xcheck')
|
|
1044
|
+
&& (row.action === 'seat-question' || row.action === 'seat-answer' || row.inquiry?.to === 'CEO'));
|
|
1045
|
+
const inquiry = item.source === 'seat-question' ? null : revisedProposal && item.source === 'checklist'
|
|
1046
|
+
? ledger.find((row) => row.status === 'awaiting-resolution' && row.item && itemKey(row.item) === itemKey(item))?.inquiry ?? null
|
|
1047
|
+
: unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
|
|
699
1048
|
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
700
1049
|
`[${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 };
|
|
701
1050
|
const value: unknown = typeof output?.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
@@ -714,7 +1063,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
714
1063
|
const reply = item.source === 'seat-question' && item.from && (questionsMode === 'on' || deps.reply || config.questions !== undefined)
|
|
715
1064
|
? unfinished?.answer ? { answer: unfinished.answer } : unfinished?.inquiry ? unfinished.inquiry : await (deps.reply ?? (async (receiver: SeatId, question: string, from: SeatId) => {
|
|
716
1065
|
const evidence = inputs.checklist.map((candidate) =>
|
|
717
|
-
`${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})`).join('\n').slice(0, 8_000);
|
|
1066
|
+
`${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})${candidate.evidence?.trim() ? ` · 근거: ${candidate.evidence.trim()}` : ''}`).join('\n').slice(0, 8_000);
|
|
718
1067
|
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
719
1068
|
`[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 질문에 근거를 갖고 답하라. 아래 ${receiver} 자리의 실제 체크리스트 근거에 없는 사실은 지어내지 마라. 모르면 사람 결정이 필요한 질문을 작성하라. JSON 하나만 반환: {"answer":"근거를 포함한 답"} 또는 {"to":"CEO","question":"결정할 질문"}. 질문: ${question}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
|
|
720
1069
|
const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
@@ -736,6 +1085,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
736
1085
|
entry.action = 'decision';
|
|
737
1086
|
}
|
|
738
1087
|
if (questionsMode !== 'on') {
|
|
1088
|
+
if (config.mode === 'live-safe' && !isRescueAllowedAction('seat-question')) {
|
|
1089
|
+
observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'question delivery is shadow-only' });
|
|
1090
|
+
}
|
|
739
1091
|
append(path, entry);
|
|
740
1092
|
observe('question-shadow', { seat, item, inquiry: entry.inquiry, answered: !!entry.answer });
|
|
741
1093
|
return entry;
|
|
@@ -745,7 +1097,18 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
745
1097
|
append(path, entry);
|
|
746
1098
|
return entry;
|
|
747
1099
|
}
|
|
748
|
-
|
|
1100
|
+
if (entry.inquiry?.to === 'CEO' && !item.evidence?.trim() && !decisionReceipt(root, seat, item)
|
|
1101
|
+
&& !ledger.some((row) => row.item && itemKey(row.item) === itemKey(item) && row.status === 'awaiting-xcheck')
|
|
1102
|
+
&& !deliveredSeatQuestion(root, `xcheck:${seat}:${itemKey(item)}`)) {
|
|
1103
|
+
entry.status = 'rejected-no-evidence';
|
|
1104
|
+
append(path, entry);
|
|
1105
|
+
xcheckEvent('rejected-no-evidence', seat, (deps.decisionsConfig ?? getUserConfig().decisions)?.crossCheckNeighbor?.[seat as SeatId] ?? 'OP', itemKey(item), 'no-evidence');
|
|
1106
|
+
return entry;
|
|
1107
|
+
}
|
|
1108
|
+
if (!revisedProposal && (entry.inquiry?.to !== 'CEO' || (unfinished?.status !== 'awaiting-xcheck' && !ledger.some((row) =>
|
|
1109
|
+
row.item && itemKey(row.item) === itemKey(item) && row.status === 'awaiting-xcheck')))) {
|
|
1110
|
+
append(path, { ...entry, status: 'attempting' });
|
|
1111
|
+
}
|
|
749
1112
|
try {
|
|
750
1113
|
if (item.source === 'seat-question' && reply && 'answer' in reply) {
|
|
751
1114
|
const question = { id: Number(item.id), from: item.from!, to: seat, body: item.text, kind: 'seat-question', createdAt: item.createdAt ?? '' };
|
|
@@ -755,16 +1118,55 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
755
1118
|
const ask = entry.inquiry!;
|
|
756
1119
|
if (!ask.question.trim()) throw new Error('empty seat question');
|
|
757
1120
|
if (ask.to === 'CEO') {
|
|
1121
|
+
const settings = deps.decisionsConfig ?? getUserConfig().decisions!;
|
|
1122
|
+
const neighbor = settings.crossCheckNeighbor?.[seat as SeatId] ?? ({ OP: 'TC', TC: 'OP', MK: 'OP', UX: 'OP' } as const)[seat as SeatId];
|
|
1123
|
+
const waitMinutes = settings.crossCheckWaitMinutes ?? 120;
|
|
1124
|
+
const previous = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
|
|
1125
|
+
&& row.status === 'awaiting-xcheck' && row.xcheckQuestionId !== undefined);
|
|
758
1126
|
const prior = decisionReceipt(root, seat, item);
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1127
|
+
if (prior) {
|
|
1128
|
+
entry.status = 'hitl';
|
|
1129
|
+
} else {
|
|
1130
|
+
const key = `xcheck:${seat}:${itemKey(item)}`;
|
|
1131
|
+
const delivered = deliveredSeatQuestion(root, key);
|
|
1132
|
+
if (delivered && (delivered.kind !== 'seat-xcheck' || delivered.from !== seat || !isSeatId(delivered.to))) {
|
|
1133
|
+
throw new Error('cross-check retry has an invalid recorded recipient');
|
|
1134
|
+
}
|
|
1135
|
+
const requestedNeighbor = delivered ? delivered.to as SeatId : neighbor;
|
|
1136
|
+
if (!delivered && !item.evidence?.trim()) {
|
|
1137
|
+
entry.status = 'rejected-no-evidence';
|
|
1138
|
+
xcheckEvent('rejected-no-evidence', seat, neighbor, itemKey(item), 'no-evidence');
|
|
1139
|
+
} else {
|
|
1140
|
+
const question = delivered ?? askCrossCheck(root, seat as SeatId, neighbor,
|
|
1141
|
+
`제목: ${seat}: ${ask.question}\nS: ${seat} 질문 · ${item.title}\nC: 판단 근거 부족 — 사람의 결정이 필요하다\n질문: ${ask.question}\n근거: ${item.evidence!.trim()}`, key);
|
|
1142
|
+
const requestedAt = previous?.xcheckRequestedAt ?? (delivered ? delivered.createdAt : now.toISOString());
|
|
1143
|
+
entry.xcheckQuestionId = question.id;
|
|
1144
|
+
entry.xcheckRequestedAt = requestedAt;
|
|
1145
|
+
entry.xcheckNeighbor = requestedNeighbor;
|
|
1146
|
+
entry.xcheckWaitMinutes = previous?.xcheckWaitMinutes ?? waitMinutes;
|
|
1147
|
+
if (!delivered) xcheckEvent('requested', seat, requestedNeighbor, itemKey(item), 'awaiting-xcheck');
|
|
1148
|
+
const answer = delivered ? crossCheckAnswer(root, question) : null;
|
|
1149
|
+
if (!delivered || (!answer && now.getTime() - Date.parse(requestedAt) < entry.xcheckWaitMinutes * 60_000)) {
|
|
1150
|
+
entry.status = 'awaiting-xcheck';
|
|
1151
|
+
} else if (answer?.resolves) {
|
|
1152
|
+
entry.status = 'awaiting-resolution';
|
|
1153
|
+
entry.xcheckNote = answer.note;
|
|
1154
|
+
if (!revisedProposal) xcheckEvent('answered', seat, requestedNeighbor, itemKey(item), 'resolution-proposed');
|
|
1155
|
+
} else {
|
|
1156
|
+
if (!answer) xcheckEvent('timed-out', seat, requestedNeighbor, itemKey(item), `unanswered ${entry.xcheckWaitMinutes}m`);
|
|
1157
|
+
const check = answer ? ['--xcheck', `${requestedNeighbor}:${answer.note}`,
|
|
1158
|
+
...(answer.agree ? [] : ['--dissent', `${requestedNeighbor}: ${answer.note}`])]
|
|
1159
|
+
: ['--no-xcheck', `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`];
|
|
1160
|
+
await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
|
|
1161
|
+
'--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
|
|
1162
|
+
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
1163
|
+
'--skip-recommend', '근거가 부족하다', ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
|
|
1164
|
+
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
1165
|
+
entry.status = 'hitl';
|
|
1166
|
+
xcheckEvent('raised', seat, requestedNeighbor, itemKey(item), answer ? answer.agree ? 'agree' : 'dissent' : 'timed-out');
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
765
1169
|
}
|
|
766
|
-
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
767
|
-
entry.status = 'hitl';
|
|
768
1170
|
} else {
|
|
769
1171
|
if (!isSeatId(ask.to) || ask.to === seat) throw new Error('invalid recipient seat');
|
|
770
1172
|
const key = `ask:${seat}:${itemKey(item)}`;
|
|
@@ -787,6 +1189,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
787
1189
|
}
|
|
788
1190
|
if (item) observe('picked', { seat, item, date: seatDay(now) });
|
|
789
1191
|
const planned = item ? planAction(item, seat) : undefined;
|
|
1192
|
+
if (item && config.mode === 'live-safe' && deps.inquire && !isRescueAllowedAction('seat-question')) {
|
|
1193
|
+
observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'outbound inquiry not in rescue policy' });
|
|
1194
|
+
}
|
|
790
1195
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
|
|
791
1196
|
...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
|
|
792
1197
|
if (config.mode === 'shadow' || !item) {
|
|
@@ -799,6 +1204,19 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
799
1204
|
append(path, entry);
|
|
800
1205
|
return entry;
|
|
801
1206
|
}
|
|
1207
|
+
if (config.mode === 'live-safe') {
|
|
1208
|
+
const requested = planned!.kind === 'harness' ? 'launch' : 'decision-card';
|
|
1209
|
+
if (planned!.kind === 'decision' && !isRescueAllowedAction(planned!.kind)) {
|
|
1210
|
+
observe('refused', { seat, item: item.id, action: planned!.reason ?? 'decision', reason: 'execution forbidden; decision card only' });
|
|
1211
|
+
}
|
|
1212
|
+
if (!isRescueAllowedAction(requested)) {
|
|
1213
|
+
entry.status = 'refused';
|
|
1214
|
+
entry.reason = `action not permitted: ${requested}`;
|
|
1215
|
+
append(path, entry);
|
|
1216
|
+
observe('refused', { seat, item: item.id, action: requested, reason: entry.reason });
|
|
1217
|
+
return entry;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
802
1220
|
const run = deps.run ?? defaultRun;
|
|
803
1221
|
let outcome: string | undefined;
|
|
804
1222
|
try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
|
|
@@ -838,7 +1256,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
838
1256
|
observe('skipped-budget', { seat, item, reason: budget.reason });
|
|
839
1257
|
const parent = (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat];
|
|
840
1258
|
// Dedupe only on a DELIVERED escalation, so a failed send is retried on the next turn (TC harvest · review round 1).
|
|
841
|
-
if (parent && parent !== seat &&
|
|
1259
|
+
if (parent && parent !== seat && config.mode === 'live-safe' && !isRescueAllowedAction('alert')) {
|
|
1260
|
+
observe('refused', { seat, item: item.id, action: 'alert', reason: 'policy denied budget alert' });
|
|
1261
|
+
} else if (parent && parent !== seat && !ledger.some((prior) => prior.status === 'skipped-budget' && prior.escalated === true && prior.reason === budget.reason
|
|
842
1262
|
&& prior.item && itemKey(prior.item) === itemKey(item) && seatDay(new Date(prior.ts ?? prior.at)) === seatDay(now))) {
|
|
843
1263
|
const line = `[${seat} → ${parent}] ${item.id}: ${budget.reason}`;
|
|
844
1264
|
try {
|
|
@@ -866,7 +1286,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
866
1286
|
const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
|
|
867
1287
|
'--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}\n역할: ${inputs.role}`, '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
|
|
868
1288
|
'--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
|
|
869
|
-
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--agent', 'seat-loop', '--json']);
|
|
1289
|
+
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--json']);
|
|
870
1290
|
const decision: unknown = JSON.parse(raised.trim());
|
|
871
1291
|
if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
|
|
872
1292
|
throw new Error('decisions raise returned no decision id');
|
|
@@ -876,6 +1296,26 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
876
1296
|
observe('hitl', { seat, item });
|
|
877
1297
|
return entry;
|
|
878
1298
|
}
|
|
1299
|
+
if (config.mode === 'live-safe') {
|
|
1300
|
+
let queued: { id: string };
|
|
1301
|
+
try {
|
|
1302
|
+
queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
// Same key, different task body: never adopt the other work. Refuse this turn; the key frees once that item finishes.
|
|
1305
|
+
if (!/idempotency key collision/.test(String(error))) throw error;
|
|
1306
|
+
entry.status = 'refused';
|
|
1307
|
+
entry.reason = 'AUTOQ idempotency key collision: a queued item with this key carries a different task';
|
|
1308
|
+
append(path, entry);
|
|
1309
|
+
observe('refused', { seat, item: item.id, action: 'launch', reason: entry.reason });
|
|
1310
|
+
return entry;
|
|
1311
|
+
}
|
|
1312
|
+
if (!queued || typeof queued.id !== 'string' || !/^hq-[0-9a-f-]{36}$/i.test(queued.id)) throw new Error('AUTOQ returned no queue id');
|
|
1313
|
+
entry.status = 'queued';
|
|
1314
|
+
entry.queueId = queued.id;
|
|
1315
|
+
append(path, entry);
|
|
1316
|
+
observe('queued', { seat, item, queueId: queued.id });
|
|
1317
|
+
return entry;
|
|
1318
|
+
}
|
|
879
1319
|
const output = await run(['harness', 'say', action.text, '--substrate', 'pod', '--pod-pool', config.podPool ?? 'pool-node-b@node-b:8', '--base', 'main', '--json']);
|
|
880
1320
|
const results: unknown = JSON.parse(output.trim());
|
|
881
1321
|
if (!Array.isArray(results) || results.length === 0 || results.some((result) =>
|
|
@@ -900,6 +1340,15 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
900
1340
|
throw error;
|
|
901
1341
|
} finally {
|
|
902
1342
|
// A turn ends even when there is no picked item or a launch fails.
|
|
1343
|
+
try { judgeSeatNeighbors(seat, config, deps, stateRoot, (deps.now ?? (() => new Date()))()); }
|
|
1344
|
+
catch (error) {
|
|
1345
|
+
observe('neighbor-judgment-failed', { seat, error: String(error).slice(0, 200) });
|
|
1346
|
+
const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
|
|
1347
|
+
for (const neighbor of neighbors) {
|
|
1348
|
+
try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: 'unknown', action: null, mode: config.mode }); }
|
|
1349
|
+
catch { /* Observation cannot change the turn. */ }
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
903
1352
|
try { escalateStalls(seat, deps, config, (deps.now ?? (() => new Date()))()); }
|
|
904
1353
|
catch (error) {
|
|
905
1354
|
debug.log('org.stall', 'escalate-error', { seat, error: String(error).slice(0, 200) }, { level: 'warn' });
|