elanous 0.2.10 → 0.2.12
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/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-54b32bd27726588d.js → page-5c05b0da4bc1392c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-793fa536b3a6ef37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-e297866051595d81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/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 -0
- package/apps/pwa/out/inside/index.txt +31 -0
- 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/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 -0
- package/apps/pwa/out/outputs/index.txt +30 -0
- 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 -24
- 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 -24
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/drafts/draft-cleanup.yaml +19 -0
- package/graphs/drafts/recipes.yaml +23 -0
- package/graphs/intake/intake-daily.yaml +2 -1
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/ops/secret-exposure.yaml +24 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/video/explainer-line.yaml +2 -2
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/plugins/elanous-hwp/README.md +25 -0
- package/plugins/elanous-hwp/cli.ts +22 -0
- package/plugins/elanous-hwp/converter.ts +237 -0
- package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
- package/plugins/elanous-hwp/plugin.json +16 -0
- package/plugins/elanous-hwp/plugin.ts +40 -0
- package/scripts/ci-public-export-leak-gate.ts +75 -14
- package/scripts/ci-public-leak-gate.ts +44 -9
- package/scripts/context-hooks/claude-stop.sh +4 -0
- package/scripts/context-hooks/emit.ts +24 -0
- package/scripts/context-hooks/post-tool-use.sh +4 -0
- package/scripts/coord-channel-watch.sh +12 -0
- package/scripts/coord-requests-bridge.ts +143 -0
- package/scripts/field-feed/lib.ts +4 -0
- package/scripts/fleet/node-setup.sh +20 -3
- package/scripts/homebrew-formula-bump.ts +69 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/marketing/measure-site-numbers.ts +169 -0
- package/scripts/measure-gate-shards.ts +309 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/mission-request-judge.ts +31 -1
- package/scripts/morning-report.ts +2 -2
- package/scripts/opportunity-scan.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +21 -4
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/cut-branch.ts +84 -0
- package/scripts/release-loop/gate-node.ts +22 -15
- package/scripts/release-loop/mac-smoke-node.ts +229 -0
- package/scripts/release-loop/node-verdict.ts +3 -3
- package/scripts/release-loop/ops-upgrade-node.ts +47 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/tui-node-checks.ts +42 -0
- package/scripts/release-loop/tui-sim-node.ts +32 -8
- package/scripts/release-loop/unattended-release.ts +5 -3
- package/scripts/release-loop/version-node.ts +74 -13
- package/scripts/signal-digest-cycle.ts +2 -7
- package/scripts/signal-digest-photo.ts +11 -0
- package/scripts/tech1-public-measure.ts +94 -0
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-diet/node.ts +1 -1
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/tui-regress.ts +2 -2
- package/scripts/ux/demo-rehearsal.ts +247 -0
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +80 -0
- package/skills/diagram-master/SKILL.md +366 -0
- package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
- package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
- package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
- package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
- package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
- package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
- package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
- package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
- package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
- package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
- package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
- package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
- package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
- package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
- package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
- package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
- package/skills/diagram-master/references/examples/README.md +69 -0
- package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
- package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
- package/skills/diagram-master/references/pyproject.toml +16 -0
- package/skills/diagram-master/references/quality-checklist.md +33 -0
- package/skills/diagram-master/references/routing-guide.md +164 -0
- package/skills/diagram-master/references/uv.lock +1356 -0
- package/skills/google-workspace/SKILL.md +33 -0
- package/skills/google-workspace/scripts/gws-safe.sh +177 -0
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/lecture-note-digitizer/SKILL.md +926 -0
- package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
- package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
- package/skills/lecture-note-digitizer/references/html-template.md +167 -0
- package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
- package/skills/omni-crawl/SKILL.md +21 -11
- package/skills/omni-crawl/scripts/main.ts +6 -6
- package/skills/omni-crawl/src/router.ts +3 -3
- package/skills/omni-digest/SKILL.md +17 -9
- package/skills/omni-digest/digest.md +2 -2
- package/skills/omni-digest/src/fetch.ts +30 -3
- package/skills/pitch-deck/SKILL.md +2 -2
- package/skills/pitch-deck/template.html +2 -2
- package/skills/youtube-master/SKILL.md +163 -0
- package/skills/youtube-master/package-lock.json +558 -0
- package/skills/youtube-master/package.json +13 -0
- package/skills/youtube-master/references/study-note-template.md +124 -0
- package/skills/youtube-master/scripts/main.ts +435 -0
- package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
- package/skills/youtube-master/src/audio.ts +63 -0
- package/skills/youtube-master/src/domain-staging.ts +50 -0
- package/skills/youtube-master/src/env.ts +36 -0
- package/skills/youtube-master/src/obsidian.ts +155 -0
- package/skills/youtube-master/src/router.ts +110 -0
- package/skills/youtube-master/src/stt.ts +199 -0
- package/skills/youtube-master/src/study-note.ts +175 -0
- package/skills/youtube-master/src/summarize.ts +358 -0
- package/skills/youtube-master/src/transcript.ts +76 -0
- package/skills/youtube-master/src/types.ts +91 -0
- package/skills/youtube-master/src/util.ts +93 -0
- package/skills/youtube-master/src/youtube.ts +70 -0
- package/skills/youtube-master/tsconfig.json +13 -0
- package/src/acp/server.ts +5 -2
- package/src/agent-mission/driver.ts +215 -23
- package/src/agent-mission/pty-decision.ts +71 -0
- package/src/agent-mission/review-loop.ts +30 -0
- package/src/autopilot/build/llm-conflict-merge.ts +16 -3
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/tool-registry.ts +2 -0
- package/src/boot/daemon-prompt-turn.ts +5 -0
- package/src/boot/daemon-runtime.ts +9 -3
- package/src/boot/daemon-tools/web-search.ts +3 -0
- package/src/card-followup/core.ts +109 -0
- package/src/chat/index.ts +2 -0
- package/src/cli/drive-input-jail.ts +101 -0
- package/src/cli/hooks-cli.ts +17 -3
- package/src/cli/make-cli.ts +43 -0
- package/src/cli/persona-cli.ts +98 -9
- package/src/cli/plugin-cli.ts +31 -22
- package/src/cli/pod-cli.ts +36 -9
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +51 -15
- package/src/cli/pty-drive-cli.ts +41 -7
- package/src/cli/pty-drive-verdict.ts +40 -0
- package/src/cli/release-cli.ts +63 -10
- package/src/cli/setup-cli.ts +43 -1
- package/src/cli/skills-cli.ts +17 -1
- package/src/context-bus/claude-hooks-install.ts +10 -0
- package/src/context-bus/context-now-surfaces.ts +65 -0
- package/src/context-bus/context-now.ts +104 -0
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/external-events.ts +52 -0
- package/src/context-bus/long-term-memory.ts +108 -0
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +17 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +31 -1
- package/src/decisions/decision-cards.ts +10 -3
- package/src/decisions/decision-ledger.ts +13 -5
- package/src/discord/sprint21-runtime.ts +5 -1
- package/src/discord-self-message.ts +32 -0
- package/src/discord-slash-wire.ts +5 -1
- package/src/discord.ts +17 -1
- package/src/domains/core-tools.ts +3 -2
- package/src/domains/outbound-alert.ts +105 -32
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/domains/telegram-kind-route.ts +51 -13
- package/src/exec-requests/default-outputs.ts +25 -0
- package/src/exec-requests/planner.ts +10 -2
- package/src/exec-requests/runner.ts +20 -6
- package/src/exec-requests/store.ts +3 -0
- package/src/execution-loop/launch-gate.ts +109 -0
- package/src/field/field-reel.ts +5 -1
- package/src/field-note/index.ts +201 -0
- package/src/field-note/telegram.ts +142 -0
- package/src/field-note/transcribe.ts +248 -0
- package/src/graph-kinds/registry.ts +3 -3
- package/src/graph-runner/graph-cli.ts +61 -1
- package/src/graph-runner/graph-grow-llm.ts +47 -0
- package/src/graph-runner/graph-grow.ts +50 -0
- package/src/graph-runner/runner.ts +329 -33
- package/src/grok/agent-search.ts +8 -2
- package/src/guardian/secret-exposure-scan.ts +101 -0
- package/src/harness/harness-cli-command.ts +375 -13
- package/src/harness/harness-pod-dispatch.ts +43 -23
- package/src/harness/harness-seams.ts +38 -4
- package/src/harness/harness-stop.ts +23 -4
- package/src/harness/launch-registry.ts +91 -0
- package/src/harness/process-garbage.ts +5 -3
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/dispatch.ts +82 -0
- package/src/hooks/expose.ts +254 -0
- package/src/hooks/github-shadow.ts +108 -0
- package/src/hooks/providers.ts +71 -5
- package/src/hooks/queue.ts +1 -1
- package/src/hooks/receiver.ts +94 -16
- package/src/index.ts +76 -14
- package/src/intake-plane/categorize.ts +18 -46
- package/src/intake-plane/discord-seat-work.ts +38 -2
- package/src/intake-plane/front-route-classifier.ts +33 -39
- package/src/intake-plane/persona-answer.ts +30 -0
- package/src/intake-plane/runtime-callables.ts +8 -1
- package/src/intake-plane/telegram-seat-work.ts +36 -2
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/intake-plane/wish-folder.ts +67 -0
- package/src/intent-gate/gate.ts +30 -10
- package/src/live/detail-switch.ts +5 -0
- package/src/llm/difficulty.ts +57 -0
- package/src/llm/judge-layer.ts +101 -0
- package/src/llm/mission-router.ts +33 -19
- package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
- package/src/loops/contract/examples/steward.loop.yaml +50 -0
- package/src/loops/contract/loop-agent.schema.json +138 -0
- package/src/loops/contract/validate.ts +64 -0
- package/src/loops/registry.ts +20 -6
- package/src/market/signed-index.ts +4 -0
- package/src/maturity/bot-command-maturity.ts +45 -0
- package/src/maturity/feature-maturity.ts +27 -4
- package/src/model-tier/index.ts +2 -2
- package/src/model-tier/tier-resolver.ts +20 -20
- package/src/model-tier/types.ts +6 -6
- package/src/mss/logging/log-store.ts +15 -0
- package/src/nexus/api/config-model-tier.ts +5 -4
- package/src/nexus/api/fabric-plans.ts +53 -0
- package/src/nexus/api/harness-api.ts +5 -1
- package/src/nexus/api/health.ts +2 -0
- package/src/nexus/api/http-server.ts +67 -8
- package/src/nexus/api/inside-events.ts +226 -0
- package/src/nexus/api/meta-api.ts +21 -8
- package/src/nexus/api/missions.ts +6 -1
- package/src/nexus/api/ops-seats.ts +33 -0
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/outputs-route.ts +44 -0
- package/src/nexus/api/personas.ts +95 -53
- package/src/nexus/api/seat-requests.ts +44 -2
- package/src/nexus/api/sessions-store.ts +17 -38
- package/src/nexus/api/sub-seat-charters.ts +34 -0
- package/src/nexus/api/tabs-mutations.ts +3 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/trace.ts +18 -6
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
- package/src/nexus/api/workflows.ts +132 -6
- package/src/nexus/index.ts +2 -0
- package/src/nexus/kinds/webterm.ts +2 -1
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/nexus/webterm/pty.ts +6 -2
- package/src/onboarding/cli-auth-status.ts +80 -0
- package/src/onboarding/voice-ai.ts +3 -3
- package/src/org/seat-budget.ts +48 -0
- package/src/org/stall-escalation.ts +63 -0
- package/src/outputs/ledger.ts +120 -0
- package/src/persona/persona-todo.ts +79 -0
- package/src/persona/presets.ts +123 -2
- package/src/persona/team-set.ts +155 -0
- package/src/plugins/install/plugin-install.ts +14 -0
- package/src/plugins/maker/market-bundle.ts +110 -0
- package/src/plugins/maker/plugin-maker.ts +254 -11
- package/src/plugins/maker/wizard-generate.ts +103 -0
- package/src/pod-lease/admission.ts +150 -0
- package/src/pod-lease/dependency-state.ts +85 -0
- package/src/pod-lease/host-lease.ts +127 -0
- package/src/project/project-suggestion.ts +16 -0
- package/src/project/session-context.ts +24 -0
- package/src/release-loop/checklist.ts +17 -5
- package/src/release-loop/feature-store.ts +29 -13
- package/src/release-loop/landed-but-yellow.ts +48 -0
- package/src/release-loop/next-md-merge.ts +58 -0
- package/src/release-loop/release-note.ts +59 -4
- package/src/seat-dispatch/seat-ask.ts +130 -0
- package/src/seat-dispatch/seat-questions.ts +100 -0
- package/src/seat-loop/seat-loop.ts +709 -29
- package/src/seat-loop/seat-report.ts +37 -2
- package/src/self-dev/dev-cli.ts +4 -1
- package/src/self-dev/dev-pipeline.ts +37 -1
- package/src/self-dev/draft-sweep.ts +79 -40
- package/src/self-dev/draft-triage-rules.ts +62 -7
- package/src/self-dev/fabric-plan-core.ts +297 -0
- package/src/self-dev/orchestrate.ts +36 -4
- package/src/self-dev/release-path-guard.ts +62 -0
- package/src/self-dev/shard-aggregate.ts +230 -0
- package/src/self-dev/shard-placement.ts +60 -0
- package/src/self-dev/shard-plan.ts +48 -0
- package/src/self-dev/shard-pod-run.ts +77 -0
- package/src/self-dev/shard-run.ts +105 -0
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-baseline.ts +1 -1
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-cli.ts +40 -1
- package/src/self-implement/gate-scope.ts +44 -13
- package/src/self-implement/graph-variant.ts +171 -0
- package/src/self-implement/harness-policy-gates.ts +1 -1
- package/src/self-implement/headless-elanous-driver.ts +27 -3
- package/src/self-implement/host-regate.ts +22 -1
- package/src/self-implement/importer-test-index.ts +2 -1
- package/src/self-implement/l8-merge-queue.ts +130 -0
- package/src/self-implement/orchestrator.ts +257 -9
- package/src/self-implement/route-consumer-test-index.ts +122 -0
- package/src/self-implement/seams.ts +72 -11
- package/src/self-implement/test-seams.ts +2 -0
- package/src/self-implement/uncommitted-work.ts +59 -0
- package/src/session/index.ts +36 -13
- package/src/skills/gift-install.ts +215 -0
- package/src/skills/tools/web-search.ts +4 -1
- package/src/steward/launch.ts +42 -26
- package/src/steward/steward-cards.ts +17 -9
- package/src/steward/triage.ts +46 -14
- package/src/task-cards/card-cli.ts +25 -1
- package/src/task-orchestrator/mission.ts +5 -0
- package/src/task-orchestrator/store.ts +12 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +43 -17
- package/src/task-orchestrator/surfaces/pod-lease.ts +156 -19
- package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +222 -40
- package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
- package/src/task-orchestrator/types.ts +2 -0
- package/src/telegram-card-followup.ts +16 -96
- package/src/telegram-commands.ts +31 -1
- package/src/telegram-project-command.ts +145 -0
- package/src/telegram-report.ts +21 -17
- package/src/telegram.ts +61 -8
- package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
- package/src/user-config.ts +110 -24
- package/src/version/packed-revision.json +1 -1
- package/src/web-search/grok.ts +7 -1
- package/src/web-search/index.ts +28 -1
- package/src/web-search/provider.ts +4 -0
- package/src/web-search/x-intent.ts +5 -0
- package/src/workflow-runtime/executor.ts +277 -47
- package/src/workflow-runtime/index.ts +1 -0
- package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
- package/src/workflow-runtime/plugin-kind-node.ts +1 -1
- package/src/workflow-runtime/schema.ts +79 -4
- package/src/workflow-runtime/types.ts +35 -4
- package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-cb6debea56cce4fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-e5bea36f183b25e6.js +0 -1
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +0 -1
- /package/apps/pwa/out/_next/static/{2DsiDfDCks44g1cPUMhiW → -6XrAoD3eU7NyMHAE4xPS}/_ssgManifest.js +0 -0
|
@@ -1,35 +1,79 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
4
|
import { Database } from 'bun:sqlite';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
7
|
import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
|
|
7
8
|
import { debug } from '../debug/log.js';
|
|
9
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
8
10
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
9
|
-
import { getUserConfig, type SeatLoopConfig } from '../user-config.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
|
|
12
|
+
import { checkSeatBudget, type RunningSeatPod } from '../org/seat-budget.js';
|
|
13
|
+
import { DEFAULT_STALL_PARENTS, findStalls } from '../org/stall-escalation.js';
|
|
14
|
+
import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
|
|
15
|
+
import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
16
|
+
import { nextPersonaTodo, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
|
|
17
|
+
import { PersonaRegistry } from '../persona/registry.js';
|
|
18
|
+
import { answerSeat, askSeat, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatQuestions, type SeatId } from '../seat-dispatch/seat-questions.js';
|
|
19
|
+
import { queryRunningRuns } from '../self-implement/running-runs.js';
|
|
20
|
+
import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
21
|
+
import { readSchedules, releasedVersion } from '../release-loop/feature-store.js';
|
|
12
22
|
|
|
13
23
|
// `kind`/`createdAt` are the V3 shadow-compare keys (내부 문서 `METHOD-v3-shadow-compare-2026-10-02` · MK 10-02 18:54).
|
|
14
|
-
export type SeatItem = { source: 'request' | 'checklist'; kind?: 'request' | 'cell'; id: string; title: string; text: string; version?: string; queuedAt?: string; createdAt?: string };
|
|
24
|
+
export type SeatItem = { source: 'request' | 'checklist' | 'hook' | 'seat-question'; kind?: 'request' | 'cell' | 'hook-task' | 'seat-question'; id: string; title: string; text: string; evidence?: string; status?: string; version?: string; seat?: string; evidenceHash?: string; asOf?: string; queuedAt?: string; createdAt?: string; from?: SeatId };
|
|
15
25
|
export type SeatInputs = { requests: SeatItem[]; checklist: SeatItem[]; role: string };
|
|
16
|
-
// V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door or skipped-*) · `reason` (
|
|
17
|
-
export type SeatAction = 'decision' | 'harness' | 'skipped-budget' | 'skipped-empty';
|
|
18
|
-
export type
|
|
26
|
+
// V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door, wait or skipped-*) · `reason` (matched rule or forbidden word).
|
|
27
|
+
export type SeatAction = 'decision' | 'harness' | 'wait' | 'seat-question' | 'seat-answer' | 'skipped-budget' | 'skipped-empty';
|
|
28
|
+
export type SeatInquiry = { to: SeatId | 'CEO'; question: string };
|
|
29
|
+
export type SeatReply = { answer: string } | { to: 'CEO'; question: string };
|
|
30
|
+
export type OpCandidate =
|
|
31
|
+
| { kind: 'release-readiness'; version: string | null; verdict: 'ready' | 'not-ready' | 'no-open-release'; cutAt?: string; landBy?: string | null; red: string[]; undecided: string[]; blocked: string[] }
|
|
32
|
+
| { kind: 'unassigned-cell'; version: string | null; id: string | null; title?: string; status?: string; verdict: 'assign' | 'none' }
|
|
33
|
+
| { kind: 'decision-delegation'; id: string | null; title?: string; category?: DecisionEntry['category']; dueAt?: string; verdict: 'review-delegation' | 'none' };
|
|
34
|
+
export type TcCandidate =
|
|
35
|
+
| { 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[] };
|
|
37
|
+
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 };
|
|
19
39
|
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
|
|
40
|
+
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null };
|
|
20
41
|
export type SeatDeps = {
|
|
42
|
+
personaConfig?: PersonaLoopConfig;
|
|
43
|
+
personaRegistry?: PersonaRegistry;
|
|
44
|
+
personaDir?: string;
|
|
45
|
+
appendPersona?: (path: string, entry: PersonaShadowEntry) => void;
|
|
21
46
|
root?: string;
|
|
22
47
|
repo?: string;
|
|
23
48
|
config?: SeatLoopConfig;
|
|
49
|
+
budgetConfig?: Pick<UserConfig, 'org'>;
|
|
50
|
+
running?: () => readonly RunningSeatPod[];
|
|
51
|
+
escalate?: (parent: string, line: string) => Promise<void>;
|
|
24
52
|
now?: () => Date;
|
|
25
53
|
read?: (path: string) => string;
|
|
26
54
|
run?: (args: string[]) => Promise<string>;
|
|
27
55
|
append?: (path: string, entry: SeatEntry) => void;
|
|
28
56
|
versions?: () => string[];
|
|
57
|
+
/** 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). */
|
|
58
|
+
schedules?: () => Array<{ version: string; cutAt: string; landBy?: string | null }>;
|
|
29
59
|
/** Checklist items of one version — default reads the release ledger DB (checklist.json is only a legacy import source since REL5b). */
|
|
30
|
-
checklistItems?: (version: string) => Array<{ id: string; title: string; status: string; owner?: string | null }>;
|
|
60
|
+
checklistItems?: (version: string) => Array<{ id: string; title: string; status: string; owner?: string | null; disposition?: string; evidence?: string }>;
|
|
61
|
+
/** Chronological release-ledger events for a checklist id; injected for isolated release fixtures. */
|
|
62
|
+
checklistHistory?: (id: string) => ReturnType<typeof checklistHistory>;
|
|
63
|
+
/** Full checklist snapshot including history, for stall detection. */
|
|
64
|
+
stallChecklist?: (version: string) => Checklist;
|
|
65
|
+
/** Inject a mailbox write for isolated failure tests; production uses the seat message store. */
|
|
66
|
+
stallDelivery?: (message: MessageEnvelope, key: string, root: string) => boolean;
|
|
67
|
+
/** Open cards from the decision ledger; injected only for isolated ledger tests. */
|
|
68
|
+
pendingDecisions?: () => Array<Pick<DecisionEntry, 'id' | 'title' | 'category' | 'dueAt'>>;
|
|
69
|
+
/** Read-only PR snapshot for TC judgment; defaults to GitHub CLI reads. */
|
|
70
|
+
pullRequests?: () => Promise<TcPullRequest[]>;
|
|
31
71
|
ledgerFiles?: (directory: string) => string[];
|
|
32
72
|
lockContended?: () => void;
|
|
73
|
+
/** Supply a question only when this seat lacks evidence for the selected work item. */
|
|
74
|
+
inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
|
|
75
|
+
/** Supply the receiving seat's answer; absent evidence must not invent a reply. */
|
|
76
|
+
reply?: (seat: SeatId, question: string, from: SeatId) => Promise<SeatReply | null> | SeatReply | null;
|
|
33
77
|
};
|
|
34
78
|
|
|
35
79
|
const repoRoot = resolve(import.meta.dir, '../..');
|
|
@@ -84,6 +128,48 @@ export function seatLedgerPath(seat: string, root: string, now: Date): string {
|
|
|
84
128
|
return join(root, 'seat-loop', seat, `${seatDay(now)}.jsonl`);
|
|
85
129
|
}
|
|
86
130
|
|
|
131
|
+
export function personaShadowLedgerPath(personaId: string, root: string, now: Date): string {
|
|
132
|
+
// Use the todo module's ID guard for this second per-persona filesystem path too.
|
|
133
|
+
personaTodoPath(root, personaId);
|
|
134
|
+
return join(root, 'persona-loop', personaId, `${seatDay(now)}.jsonl`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<PersonaShadowEntry[]> {
|
|
138
|
+
if ((deps.personaConfig ?? getUserConfig().loops?.persona)?.enabled !== true) return [];
|
|
139
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
140
|
+
const dir = deps.personaDir ?? (deps.root ? join(root, 'personas') : resolveStatePersonaDir());
|
|
141
|
+
const registry = deps.personaRegistry ?? new PersonaRegistry();
|
|
142
|
+
if (!deps.personaRegistry) {
|
|
143
|
+
const loaded = await loadLayeredPersonaDirs(registry, deps.root ? [dir] : [dir, resolveRepositoryPersonaDir()]);
|
|
144
|
+
for (const error of loaded.errors) observe('persona-load-error', { error });
|
|
145
|
+
}
|
|
146
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
147
|
+
const entries: PersonaShadowEntry[] = [];
|
|
148
|
+
for (const profile of registry.list().sort((a, b) => a.personaId.localeCompare(b.personaId))) {
|
|
149
|
+
const personaId = profile.personaId;
|
|
150
|
+
const { todos, errors } = readPersonaTodos(dir, personaId);
|
|
151
|
+
for (const error of errors) observe('persona-todo-error', { personaId, error });
|
|
152
|
+
const todo = nextPersonaTodo(todos);
|
|
153
|
+
const entry: PersonaShadowEntry = { personaId, ts: now.toISOString(), status: todo ? 'shadow' : 'skipped-empty', todo };
|
|
154
|
+
const path = personaShadowLedgerPath(personaId, root, now);
|
|
155
|
+
if (deps.appendPersona) deps.appendPersona(path, entry);
|
|
156
|
+
else {
|
|
157
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
158
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
159
|
+
}
|
|
160
|
+
observe('persona-shadow', { personaId, status: entry.status, todoId: todo?.id ?? null });
|
|
161
|
+
entries.push(entry);
|
|
162
|
+
}
|
|
163
|
+
return entries;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
167
|
+
const result = await runSeatLoopOnce(seat, deps);
|
|
168
|
+
try { await runPersonaShadowOnce(deps); }
|
|
169
|
+
catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
87
173
|
function rows(text: string): Record<string, unknown>[] {
|
|
88
174
|
return text.split('\n').filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
89
175
|
}
|
|
@@ -93,17 +179,104 @@ function versionOrder(a: string, b: string): number {
|
|
|
93
179
|
return (aa[0]! - bb[0]!) || (aa[1]! - bb[1]!) || (aa[2]! - bb[2]!);
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
|
|
182
|
+
function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
|
|
183
|
+
|
|
184
|
+
function itemSnapshot(item: SeatItem): string {
|
|
185
|
+
return JSON.stringify([item.title, item.text, item.evidence ?? null, item.status ?? null]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function decisionRef(seat: string, item: SeatItem): string {
|
|
189
|
+
return `seat-loop:${seat}:${encodeURIComponent(itemKey(item))}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEntry | undefined {
|
|
193
|
+
const ref = decisionRef(seat, item);
|
|
194
|
+
return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).find((card) => card.refs?.includes(ref));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 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> {
|
|
199
|
+
const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
|
|
200
|
+
return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
|
|
201
|
+
|| (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
|
|
202
|
+
|| (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
|
|
203
|
+
|| (!inquiryRow(entry) && entry.status === 'outcome-unknown')
|
|
204
|
+
|| (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
|
|
205
|
+
later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
|
|
206
|
+
&& later.item.version === entry.item?.version && later.item.id === entry.item?.id
|
|
207
|
+
&& later.item.title === entry.item?.title && later.item.evidenceHash === entry.item?.evidenceHash)))
|
|
208
|
+
|| (shadow && entry.status === 'shadow' && entry.action !== 'wait' && entry.item?.source !== 'checklist')
|
|
209
|
+
|| (entry.status === 'launched' && entry.item?.source !== 'checklist'))
|
|
210
|
+
.filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string')
|
|
211
|
+
.map((entry) => itemKey(entry.item)));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function eligibleItem(item: SeatItem, ledger: readonly SeatEntry[], handled: ReadonlySet<string>, shadow: boolean): boolean {
|
|
215
|
+
const key = itemKey(item);
|
|
216
|
+
if (handled.has(key)) return false;
|
|
217
|
+
const lastWait = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key
|
|
218
|
+
&& (entry.status === 'wait' || (shadow && entry.status === 'shadow' && entry.action === 'wait')));
|
|
219
|
+
return !lastWait?.item || itemSnapshot(lastWait.item) !== itemSnapshot(item);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const CHECKLIST_HANDLED_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
223
|
+
|
|
224
|
+
// Same-version snapshots are handled here; carried work requires an explicit release-ledger move.
|
|
225
|
+
export function alreadyHandled(item: SeatItem, ledger: readonly SeatEntry[]): boolean {
|
|
226
|
+
if (item.source !== 'checklist' || !item.seat || !item.asOf || item.evidenceHash === undefined) return false;
|
|
227
|
+
const now = Date.parse(item.asOf);
|
|
228
|
+
if (!Number.isFinite(now)) return false;
|
|
229
|
+
return ledger.some((entry) => {
|
|
230
|
+
const at = Date.parse(entry.at || entry.ts || '');
|
|
231
|
+
return entry.seat === item.seat && at >= now - CHECKLIST_HANDLED_WINDOW_MS && at <= now
|
|
232
|
+
// 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))
|
|
234
|
+
&& entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version === item.version
|
|
235
|
+
&& entry.item.title === item.title && entry.item.evidenceHash === item.evidenceHash;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function readSeatLedger(seat: string, deps: SeatDeps, now: Date): SeatEntry[] {
|
|
240
|
+
const directory = dirname(seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now));
|
|
241
|
+
const files = deps.ledgerFiles?.(directory) ?? (existsSync(directory) ? readdirSync(directory) : []);
|
|
242
|
+
return files.filter((name) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(name) && name <= `${seatDay(now)}.jsonl`)
|
|
243
|
+
.flatMap((name) => rows((deps.read ?? defaultRead)(join(directory, name))) as SeatEntry[]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger?: readonly SeatEntry[]): Promise<SeatInputs> {
|
|
97
247
|
const read = deps.read ?? defaultRead;
|
|
98
248
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
249
|
+
const activeLedger = ledger ?? readSeatLedger(seat, deps, (deps.now ?? (() => new Date()))());
|
|
250
|
+
const consumed = handledKeys(activeLedger, (deps.config ?? getUserConfig().loops?.seat)?.mode === 'shadow', root);
|
|
99
251
|
const requests = new Map<string, Record<string, unknown>>();
|
|
100
252
|
for (const row of rows(read(join(root, 'seat-requests', 'requests.jsonl')))) {
|
|
101
253
|
if (typeof row.key === 'string') requests.set(row.key, row);
|
|
102
254
|
}
|
|
103
|
-
const pending =
|
|
255
|
+
const pending: SeatItem[] = seatQuestions(root, seat as SeatId)
|
|
256
|
+
.filter((message) => !hasSeatAnswer(root, message) && !consumed.has(`seat-question::${message.id}`))
|
|
257
|
+
.map((message) => ({ source: 'seat-question', kind: 'seat-question', id: String(message.id), title: message.body, text: message.body,
|
|
258
|
+
from: message.from as SeatId, queuedAt: message.createdAt, createdAt: message.createdAt }));
|
|
259
|
+
const ordinary: SeatItem[] = [...requests.values()]
|
|
104
260
|
.filter((row) => row.seat === seat && (row.status === 'pending' || row.status === 'queued') && typeof row.text === 'string')
|
|
105
|
-
.map((row) => ({ source: 'request' as const, kind: 'request' as const, id: String(row.key), title: String(row.text), text: String(row.text), queuedAt: String(row.queuedAt ?? ''), createdAt: String(row.queuedAt ?? '') }))
|
|
261
|
+
.map((row) => ({ source: 'request' as const, kind: 'request' as const, id: String(row.key), title: String(row.text), text: String(row.text), ...(typeof row.evidence === 'string' ? { evidence: row.evidence } : {}), queuedAt: String(row.queuedAt ?? ''), createdAt: String(row.queuedAt ?? '') }))
|
|
106
262
|
.sort((a, b) => a.queuedAt.localeCompare(b.queuedAt) || a.id.localeCompare(b.id));
|
|
263
|
+
const store = openMsgStore(join(root, 'msg', 'messages.db'));
|
|
264
|
+
const hookMessages: ReturnType<typeof store.listByRecipient> = [];
|
|
265
|
+
try {
|
|
266
|
+
let cursor = 0;
|
|
267
|
+
while (true) {
|
|
268
|
+
const batch = store.listByRecipient(seat, cursor, 1000);
|
|
269
|
+
hookMessages.push(...batch.filter(message => message.kind === 'hook-task'));
|
|
270
|
+
if (batch.length < 1000) break;
|
|
271
|
+
cursor = batch[batch.length - 1]!.id;
|
|
272
|
+
}
|
|
273
|
+
} finally { store.close(); }
|
|
274
|
+
for (const message of hookMessages) {
|
|
275
|
+
const item: SeatItem = { source: 'hook', kind: 'hook-task', id: String(message.id), title: message.body, text: message.body, queuedAt: message.createdAt, createdAt: message.createdAt };
|
|
276
|
+
if (eligibleItem(item, activeLedger, consumed, (deps.config ?? getUserConfig().loops?.seat)?.mode === 'shadow')) ordinary.push(item);
|
|
277
|
+
}
|
|
278
|
+
ordinary.sort((a, b) => (a.queuedAt ?? '').localeCompare(b.queuedAt ?? '') || a.id.localeCompare(b.id));
|
|
279
|
+
pending.push(...ordinary);
|
|
107
280
|
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
108
281
|
const versions = deps.versions ?? (() => {
|
|
109
282
|
const dir = join(releaseRoot, 'release');
|
|
@@ -113,16 +286,42 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}): Promi
|
|
|
113
286
|
return [...new Set([...fromDirs, ...fromSchedule])].filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
114
287
|
});
|
|
115
288
|
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
|
|
289
|
+
let shipped: string | undefined;
|
|
290
|
+
try { shipped = releasedVersion(releaseRoot); }
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.warn(`seat loop: cannot read published versions; retaining previous order (${String(error).slice(0, 200)})`);
|
|
293
|
+
}
|
|
294
|
+
// UX J2 criterion (10-03): only versions whose cut is still ahead are open; if the schedule can't be read, pick no cell.
|
|
295
|
+
const asOf = (deps.now ?? (() => new Date()))().toISOString();
|
|
296
|
+
const nowMs = Date.parse(asOf);
|
|
297
|
+
let open: Set<string> | null = null;
|
|
298
|
+
try {
|
|
299
|
+
const rows = (deps.schedules ?? (() => readSchedules()))();
|
|
300
|
+
open = new Set(rows.filter((row) => Number.isFinite(Date.parse(row.cutAt)) && Date.parse(row.cutAt) > nowMs).map((row) => row.version));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.warn(`seat loop: cannot read release schedules; no checklist cell is picked (${String(error).slice(0, 200)})`);
|
|
303
|
+
debug.log('seat.loop', 'schedule-unreadable', { seat, error: String(error).slice(0, 200) });
|
|
304
|
+
}
|
|
116
305
|
const checklist: SeatItem[] = [];
|
|
117
|
-
for (const version of versions().sort(versionOrder)) {
|
|
306
|
+
for (const version of open === null ? [] : versions().sort(versionOrder)) {
|
|
307
|
+
if (!open!.has(version)) {
|
|
308
|
+
debug.log('seat.loop', 'skip-closed-version', { seat, version });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
118
311
|
let items: ReturnType<typeof itemsOf>;
|
|
119
312
|
try { items = itemsOf(version); } catch (error) {
|
|
120
313
|
debug.log('seat.loop', 'checklist-unreadable', { seat, version, error: String(error).slice(0, 200) });
|
|
121
314
|
continue;
|
|
122
315
|
}
|
|
316
|
+
if (shipped && versionOrder(version, shipped) <= 0) {
|
|
317
|
+
const cells = items.filter((item) => item.owner === seat && (item.status === 'yellow' || item.status === 'red')).length;
|
|
318
|
+
debug.log('seat.loop', 'skip-shipped-version', { version, cells });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
123
321
|
for (const item of items) {
|
|
124
322
|
if (item.owner === seat && (item.status === 'yellow' || item.status === 'red')) {
|
|
125
|
-
checklist.push({ source: 'checklist', kind: 'cell', version, id: item.id, title: item.title, text: item.title
|
|
323
|
+
checklist.push({ source: 'checklist', kind: 'cell', version, seat, asOf, id: item.id, title: item.title, text: item.title, status: item.status,
|
|
324
|
+
...(item.evidence ? { evidence: item.evidence } : {}), evidenceHash: createHash('sha256').update(item.evidence ?? '').digest('hex') });
|
|
126
325
|
}
|
|
127
326
|
}
|
|
128
327
|
}
|
|
@@ -130,24 +329,280 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}): Promi
|
|
|
130
329
|
return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
|
|
131
330
|
}
|
|
132
331
|
|
|
133
|
-
function
|
|
332
|
+
function opCandidates(deps: SeatDeps, now: Date): OpCandidate[] {
|
|
333
|
+
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
334
|
+
const shipped = releasedVersion(releaseRoot);
|
|
335
|
+
let schedules: ReturnType<NonNullable<SeatDeps['schedules']>>;
|
|
336
|
+
try { schedules = (deps.schedules ?? (() => readSchedules(releaseRoot)))(); }
|
|
337
|
+
catch (error) {
|
|
338
|
+
observe('schedule-unreadable', { seat: 'OP', error: String(error).slice(0, 200) });
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
const open = schedules.filter((row) => /^\d+\.\d+\.\d+$/.test(row.version)
|
|
342
|
+
&& Number.isFinite(Date.parse(row.cutAt)) && Date.parse(row.cutAt) > now.getTime()
|
|
343
|
+
&& (!shipped || versionOrder(row.version, shipped) > 0)).sort((a, b) => versionOrder(a.version, b.version));
|
|
344
|
+
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
|
|
345
|
+
const releases = open.map((schedule) => ({ schedule, items: itemsOf(schedule.version).sort((a, b) => a.id.localeCompare(b.id)) }));
|
|
346
|
+
const first = releases[0];
|
|
347
|
+
const red = first?.items.filter((item) => item.status === 'red').map((item) => item.id) ?? [];
|
|
348
|
+
const undecided = first?.items.filter((item) => item.status === 'yellow' && !item.disposition).map((item) => item.id) ?? [];
|
|
349
|
+
const blocked = first?.items.filter((item) => item.status === 'yellow' && item.disposition === 'block').map((item) => item.id) ?? [];
|
|
350
|
+
const ready = first !== undefined && first.items.length > 0 && first.items.every((item) =>
|
|
351
|
+
item.status === 'green' || item.status === 'done'
|
|
352
|
+
|| (item.status === 'yellow' && (item.disposition === 'move' || item.disposition === 'known-issue')));
|
|
353
|
+
const unassigned = releases.flatMap(({ schedule, items }) => items
|
|
354
|
+
.filter((item) => !item.owner && (item.status === 'yellow' || item.status === 'red'))
|
|
355
|
+
.map((item): OpCandidate => ({ kind: 'unassigned-cell', version: schedule.version, id: item.id,
|
|
356
|
+
title: item.title, status: item.status, verdict: 'assign' })));
|
|
357
|
+
const pending = (deps.pendingDecisions ?? (() => new DecisionLedger({ stateDir: deps.root }).list({ status: 'open' })))()
|
|
358
|
+
.sort((a, b) => (a.dueAt ?? '\uffff').localeCompare(b.dueAt ?? '\uffff') || a.id.localeCompare(b.id))[0];
|
|
359
|
+
return [
|
|
360
|
+
{ kind: 'release-readiness', version: first?.schedule.version ?? null,
|
|
361
|
+
verdict: !first ? 'no-open-release' : ready ? 'ready' : 'not-ready',
|
|
362
|
+
...(first ? { cutAt: first.schedule.cutAt, landBy: first.schedule.landBy ?? null } : {}), red, undecided, blocked },
|
|
363
|
+
...(unassigned.length ? unassigned : [{ kind: 'unassigned-cell' as const, version: null, id: null, verdict: 'none' as const }]),
|
|
364
|
+
{ kind: 'decision-delegation', id: pending?.id ?? null,
|
|
365
|
+
...(pending ? { title: pending.title, category: pending.category, ...(pending.dueAt ? { dueAt: pending.dueAt } : {}) } : {}),
|
|
366
|
+
verdict: pending ? 'review-delegation' : 'none' },
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const TC_APPROVAL_WAIT_MS = 2 * 60 * 60 * 1000;
|
|
371
|
+
|
|
372
|
+
function readTcChecklist(root: string): Array<{ version: string; id: string; title: string; status: string; owner: string | null }> {
|
|
373
|
+
const path = join(root, 'release', 'features.sqlite');
|
|
374
|
+
if (!existsSync(path)) return [];
|
|
375
|
+
const db = new Database(path, { readonly: true, strict: true });
|
|
376
|
+
try {
|
|
377
|
+
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 = 'yellow' AND a.owner = 'TC'
|
|
379
|
+
ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
|
|
380
|
+
} finally { db.close(); }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
type TcTimelineEvent = { event: string; created_at: string };
|
|
384
|
+
|
|
385
|
+
export function tcApprovalWaitAt(pr: TcPullRequest, timeline: readonly TcTimelineEvent[]): { readyAt: string; approvalWaitAt: string } | undefined {
|
|
386
|
+
if (pr.state !== 'OPEN' || pr.isDraft || pr.reviewDecision !== 'REVIEW_REQUIRED') return undefined;
|
|
387
|
+
const events = timeline.filter((event) => typeof event.created_at === 'string' && Number.isFinite(Date.parse(event.created_at)))
|
|
388
|
+
.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
389
|
+
let readyAt: string | undefined = pr.createdAt;
|
|
390
|
+
let waitAt: string | undefined = pr.createdAt;
|
|
391
|
+
for (const event of events) {
|
|
392
|
+
if (event.event === 'converted_to_draft') { readyAt = undefined; waitAt = undefined; }
|
|
393
|
+
if (event.event === 'ready_for_review') { readyAt = event.created_at; waitAt = event.created_at; }
|
|
394
|
+
if (event.event === 'review_requested' && readyAt && Date.parse(event.created_at) >= Date.parse(readyAt)) waitAt = event.created_at;
|
|
395
|
+
}
|
|
396
|
+
return readyAt && waitAt && Number.isFinite(Date.parse(readyAt)) && Date.parse(waitAt) >= Date.parse(readyAt)
|
|
397
|
+
? { readyAt, approvalWaitAt: waitAt } : undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function readTcPullRequests(query: (args: string[]) => Promise<string> = async (args) => {
|
|
401
|
+
const { stdout } = await exec('gh', args, { cwd: repoRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 64 * 1024 * 1024 });
|
|
402
|
+
return stdout;
|
|
403
|
+
}): Promise<TcPullRequest[]> {
|
|
404
|
+
const pages: unknown = JSON.parse(await query(['api', '--paginate', '--slurp', 'repos/{owner}/{repo}/pulls?state=all&per_page=100']));
|
|
405
|
+
if (!Array.isArray(pages) || !pages.every((page) => Array.isArray(page))) throw new Error('PR list is not an array of pages');
|
|
406
|
+
const prs = pages.flatMap((page: unknown[]) => page).filter((pr) => pr && typeof pr === 'object'
|
|
407
|
+
&& ((pr as Record<string, unknown>).state === 'open' || typeof (pr as Record<string, unknown>).merged_at === 'string'));
|
|
408
|
+
const results: TcPullRequest[] = [];
|
|
409
|
+
for (let offset = 0; offset < prs.length; offset += 10) {
|
|
410
|
+
results.push(...await Promise.all(prs.slice(offset, offset + 10).map(async (pr): Promise<TcPullRequest> => {
|
|
411
|
+
const listing = pr as Record<string, unknown>;
|
|
412
|
+
if (typeof listing.number !== 'number' || typeof listing.title !== 'string' || typeof listing.created_at !== 'string'
|
|
413
|
+
|| typeof listing.draft !== 'boolean' || (listing.state !== 'open' && listing.state !== 'closed')) {
|
|
414
|
+
throw new Error('incomplete PR record');
|
|
415
|
+
}
|
|
416
|
+
const details: unknown = listing.state === 'open'
|
|
417
|
+
? JSON.parse(await query(['pr', 'view', String(listing.number), '--json', 'number,title,body,state,isDraft,createdAt,mergedAt,files,reviewDecision']))
|
|
418
|
+
: { number: listing.number, title: listing.title, body: listing.body, state: 'MERGED', isDraft: listing.draft,
|
|
419
|
+
createdAt: listing.created_at, mergedAt: listing.merged_at, files: [] };
|
|
420
|
+
if (!details || typeof details !== 'object' || Array.isArray(details)) throw new Error('invalid PR record');
|
|
421
|
+
const row = details as Record<string, unknown>;
|
|
422
|
+
if (typeof row.number !== 'number' || typeof row.title !== 'string' || typeof row.createdAt !== 'string'
|
|
423
|
+
|| typeof row.isDraft !== 'boolean' || (row.state !== 'OPEN' && row.state !== 'MERGED' && row.state !== 'CLOSED')) {
|
|
424
|
+
throw new Error('incomplete PR record');
|
|
425
|
+
}
|
|
426
|
+
const paths = Array.isArray(row.files) ? row.files.flatMap((file: unknown) =>
|
|
427
|
+
file && typeof file === 'object' && typeof (file as { path?: unknown }).path === 'string' ? [(file as { path: string }).path] : []) : [];
|
|
428
|
+
const result: TcPullRequest = { number: row.number, title: row.title, body: typeof row.body === 'string' ? row.body : '',
|
|
429
|
+
state: row.state, isDraft: row.isDraft, createdAt: row.createdAt,
|
|
430
|
+
mergedAt: typeof row.mergedAt === 'string' ? row.mergedAt : null, paths,
|
|
431
|
+
reviewDecision: typeof row.reviewDecision === 'string' ? row.reviewDecision : undefined };
|
|
432
|
+
if (result.state === 'OPEN' && !result.isDraft && result.reviewDecision === 'REVIEW_REQUIRED'
|
|
433
|
+
&& paths.some((path) => path.startsWith('release/public/') || path.startsWith('scripts/public-export') || path.startsWith('src/market/'))) {
|
|
434
|
+
try {
|
|
435
|
+
const pages: unknown = JSON.parse(await query(['api', '--paginate', '--slurp', `repos/{owner}/{repo}/issues/${result.number}/timeline`]));
|
|
436
|
+
if (!Array.isArray(pages) || !pages.every((page) => Array.isArray(page))) throw new Error('PR timeline is not an array of pages');
|
|
437
|
+
const timeline: unknown[] = pages.flatMap((page: unknown[]) => page);
|
|
438
|
+
const transitions = timeline.filter((event): event is Record<string, unknown> => event !== null && typeof event === 'object'
|
|
439
|
+
&& ['converted_to_draft', 'ready_for_review', 'review_requested'].includes((event as Record<string, unknown>).event as string));
|
|
440
|
+
if (!transitions.every((event) => typeof event.created_at === 'string' && Number.isFinite(Date.parse(event.created_at)))) {
|
|
441
|
+
throw new Error('invalid PR timeline transition date');
|
|
442
|
+
}
|
|
443
|
+
Object.assign(result, tcApprovalWaitAt(result, transitions as TcTimelineEvent[]));
|
|
444
|
+
} catch (error) { observe('tc-pr-timeline-unreadable', { pr: result.number, error: String(error).slice(0, 200) }); }
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
})));
|
|
448
|
+
}
|
|
449
|
+
return results;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function tcCandidates(deps: SeatDeps, now: Date): Promise<TcCandidate[]> {
|
|
453
|
+
let prs: TcPullRequest[];
|
|
454
|
+
// An injected instance root must not fetch unrelated live GitHub state.
|
|
455
|
+
try { prs = await (deps.pullRequests ?? (deps.root ? async () => [] : readTcPullRequests))(); }
|
|
456
|
+
catch (error) {
|
|
457
|
+
observe('tc-pr-unreadable', { seat: 'TC', error: String(error).slice(0, 200) });
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
const merged = prs.filter((pr) => pr.state === 'MERGED' && pr.mergedAt && Number.isFinite(Date.parse(pr.mergedAt)) && Date.parse(pr.mergedAt) <= now.getTime());
|
|
461
|
+
const candidates: TcCandidate[] = [];
|
|
462
|
+
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
463
|
+
try {
|
|
464
|
+
const cells = merged.length === 0 ? [] : deps.checklistItems && deps.schedules
|
|
465
|
+
? deps.schedules().flatMap(({ version }) => deps.checklistItems!(version).map((cell) => ({ ...cell, version })))
|
|
466
|
+
: readTcChecklist(releaseRoot);
|
|
467
|
+
for (const cell of cells) {
|
|
468
|
+
if (cell.owner !== 'TC' || cell.status !== 'yellow') continue;
|
|
469
|
+
for (const pr of merged) {
|
|
470
|
+
const id = cell.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
471
|
+
const reference = new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`);
|
|
472
|
+
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! });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch (error) { observe('tc-checklist-unreadable', { seat: 'TC', error: String(error).slice(0, 200) }); }
|
|
478
|
+
for (const pr of prs) {
|
|
479
|
+
if (pr.state !== 'OPEN' || pr.isDraft || pr.reviewDecision !== 'REVIEW_REQUIRED'
|
|
480
|
+
|| !pr.paths?.some((path) => path.startsWith('release/public/') || path.startsWith('scripts/public-export') || path.startsWith('src/market/'))) continue;
|
|
481
|
+
const readyAt = pr.readyAt;
|
|
482
|
+
const age = now.getTime() - Date.parse(pr.approvalWaitAt ?? '');
|
|
483
|
+
if (!readyAt || !Number.isFinite(Date.parse(readyAt)) || Date.parse(readyAt) > now.getTime()
|
|
484
|
+
|| !Number.isFinite(age) || age < TC_APPROVAL_WAIT_MS) continue;
|
|
485
|
+
candidates.push({ kind: 'publication-pr-approval-wait', pr: pr.number, title: pr.title, readyAt, approvalWaitAt: pr.approvalWaitAt!, paths: pr.paths });
|
|
486
|
+
}
|
|
487
|
+
return candidates;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function deliverStall(message: MessageEnvelope, key: string, root: string): boolean {
|
|
491
|
+
const store = openMsgStore(join(root, 'msg', 'messages.db'));
|
|
492
|
+
try {
|
|
493
|
+
// Persist the exact stall identity alongside its message, atomically across processes.
|
|
494
|
+
store.db.exec('BEGIN IMMEDIATE');
|
|
495
|
+
try {
|
|
496
|
+
store.db.exec('CREATE TABLE IF NOT EXISTS seat_stall_deliveries (key TEXT PRIMARY KEY)');
|
|
497
|
+
const inserted = store.db.query('INSERT OR IGNORE INTO seat_stall_deliveries (key) VALUES (?)').run(key);
|
|
498
|
+
if (inserted.changes === 0) { store.db.exec('COMMIT'); return false; }
|
|
499
|
+
store.append(message);
|
|
500
|
+
store.db.exec('COMMIT');
|
|
501
|
+
return true;
|
|
502
|
+
} catch (error) { store.db.exec('ROLLBACK'); throw error; }
|
|
503
|
+
} finally { store.close(); }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, now: Date): void {
|
|
507
|
+
const root = deps.root ?? releaseLedgerRoot();
|
|
508
|
+
const schedules = (deps.schedules ?? (() => readSchedules(root)))();
|
|
509
|
+
const shipped = releasedVersion(root);
|
|
510
|
+
const failures: Error[] = [];
|
|
511
|
+
for (const schedule of schedules) {
|
|
512
|
+
if (!/^\d+\.\d+\.\d+$/.test(schedule.version)
|
|
513
|
+
|| !Number.isFinite(Date.parse(schedule.cutAt)) || Date.parse(schedule.cutAt) <= now.getTime()
|
|
514
|
+
|| (shipped && versionOrder(schedule.version, shipped) <= 0)) continue;
|
|
515
|
+
try {
|
|
516
|
+
// Same ledger root as the schedules and shipped version above (TC harvest · review round 3).
|
|
517
|
+
const checklist = (deps.stallChecklist ?? ((version: string) => listChecklist(version, deps.root)))(schedule.version);
|
|
518
|
+
for (const stall of findStalls(checklist, config, now)) {
|
|
519
|
+
if (stall.from !== seat) continue;
|
|
520
|
+
const { item, from, to, stalledMin, startedAt, reason } = stall;
|
|
521
|
+
const data = { item: item.id, from, to, stalledMin, version: schedule.version, reason };
|
|
522
|
+
if (config.mode === 'shadow') {
|
|
523
|
+
debug.log('org.stall', 'escalate', { ...data, mode: 'shadow' });
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const key = JSON.stringify([schedule.version, item.id, from, to, reason, startedAt]);
|
|
527
|
+
const message: MessageEnvelope = { from, to, kind: 'stall-escalation',
|
|
528
|
+
body: `[stall:${key}] ${schedule.version} 체크리스트 ${item.id} · ${item.title} · ${reason} · ${stalledMin}분 정체 (${from} → ${to})` };
|
|
529
|
+
try {
|
|
530
|
+
const sent = (deps.stallDelivery ?? deliverStall)(message, key, deps.root ?? effectiveInstanceRoot());
|
|
531
|
+
if (sent) debug.log('org.stall', 'escalate', data);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
failures.push(new Error(`stall escalation ${schedule.version}/${item.id}: ${String(error)}`, { cause: error }));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} catch (error) {
|
|
537
|
+
failures.push(new Error(`stall checklist ${schedule.version}: ${String(error)}`, { cause: error }));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (failures.length) throw new AggregateError(failures, `stall escalation failed for ${seat}: ${failures.map((error) => error.message).join('; ')}`);
|
|
541
|
+
}
|
|
134
542
|
|
|
135
543
|
// In shadow mode a shadowed item counts as handled, so a rehearsal day walks the queue like the seat would.
|
|
136
|
-
export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean } = {}): SeatItem | null {
|
|
137
|
-
const handled =
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
546
|
+
return [...inputs.requests, ...inputs.checklist].find((item) => {
|
|
547
|
+
if (alreadyHandled(item, ledger)) {
|
|
548
|
+
debug.log('seat.loop', 'skip-handled', { id: item.id });
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (item.source === 'checklist' && item.version && item.seat && item.asOf && item.evidenceHash !== undefined && opts.history
|
|
552
|
+
&& ledger.some((entry) => entry.seat === item.seat && entry.status === 'shadow'
|
|
553
|
+
&& entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version !== item.version)) {
|
|
554
|
+
// Only an actual move into this version identifies a carried cell; unrelated releases may reuse an id.
|
|
555
|
+
const moves = opts.history(item.id).filter((event) => event.field === 'move'
|
|
556
|
+
&& typeof event.from === 'string' && typeof event.to === 'string' && Date.parse(event.at) <= Date.parse(item.asOf!));
|
|
557
|
+
const latest = moves.at(-1);
|
|
558
|
+
if (latest?.to === item.version) {
|
|
559
|
+
const processed = [...ledger].reverse().find((entry) => entry.seat === item.seat
|
|
560
|
+
&& entry.status === 'shadow' && !entry.inquiry && entry.action !== 'wait'
|
|
561
|
+
&& entry.item?.source === 'checklist' && entry.item.id === item.id);
|
|
562
|
+
if (processed && processed.item && processed.item.version === latest.from && processed.item.title === item.title
|
|
563
|
+
&& processed.item.evidenceHash === item.evidenceHash
|
|
564
|
+
&& Date.parse(processed.at || processed.ts || '') >= Date.parse(item.asOf) - CHECKLIST_HANDLED_WINDOW_MS
|
|
565
|
+
&& Date.parse(processed.at || processed.ts || '') <= Date.parse(item.asOf)
|
|
566
|
+
&& Date.parse(processed.at || processed.ts || '') <= Date.parse(latest.at)) {
|
|
567
|
+
debug.log('seat.loop', 'skip-carried', { id: item.id, from: latest.from, version: item.version });
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return eligibleItem(item, ledger, handled, opts.shadow === true);
|
|
573
|
+
}) ?? null;
|
|
140
574
|
}
|
|
141
575
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
576
|
+
const SEAT_ACTION_RULES: ReadonlyArray<{ kind: 'wait' | 'harness' | 'decision'; signal: RegExp; source: 'content' | 'task'; resolvedBy?: RegExp }> = [
|
|
577
|
+
{ kind: 'wait', signal: /실측\s*대기|(?:사람|실제)\s*기기(?!\s*(?:(?:실측|측정|확인)\s*)?완료)(?:\s*(?:실측|측정|확인|대기))?|실물\s*측정(?!\s*완료)/iu, source: 'content', resolvedBy: /(?:실측|측정|(?:사람|실제)\s*기기|실물\s*측정)\s*완료/iu },
|
|
578
|
+
{ kind: 'wait', signal: /\u{1F451}\s*확인\s*대기/iu, source: 'content', resolvedBy: /\u{1F451}\s*확인\s*완료/iu },
|
|
579
|
+
{ kind: 'decision', signal: TRACK_AGENT_FORBIDDEN_ACTION_REGEX, source: 'task' },
|
|
580
|
+
{ kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'content' },
|
|
581
|
+
{ kind: 'harness', signal: /(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록|운영\s*반영)(?:를?\s*위한)?\s*(?:준비|초안|원고|자료|구현|점검|테스트)|(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록)(?:용|를?\s*위한)\s*(?:초안|원고|자료|코드|기능)/iu, source: 'task' },
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
export function planAction(item: SeatItem, seat?: string): { kind: 'decision' | 'harness' | 'wait'; text: string; reason?: string } {
|
|
585
|
+
const task = `${item.id}\n${item.title}\n${item.text}`;
|
|
586
|
+
const content = `${task}\n${item.evidence ?? ''}`;
|
|
587
|
+
const preparation = SEAT_ACTION_RULES.find(({ kind }) => kind === 'harness')!;
|
|
588
|
+
const prepared = [...content.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
|
|
589
|
+
const rule = SEAT_ACTION_RULES.map(({ kind, signal, source, resolvedBy }) => {
|
|
590
|
+
const text = source === 'content' ? content : task;
|
|
591
|
+
if (resolvedBy?.test(item.evidence ?? '')) return null;
|
|
592
|
+
const hits = [...text.matchAll(new RegExp(signal.source, `${signal.flags.replace('g', '')}g`))];
|
|
593
|
+
const hit = kind === 'decision' ? hits.find((match) => !prepared.some((prep) =>
|
|
594
|
+
match.index! >= prep.index! && match.index! + match[0].length <= prep.index! + prep[0].length)) : hits[0];
|
|
595
|
+
return hit ? { kind, reason: hit[0] } : null;
|
|
596
|
+
}).find((hit) => hit !== null);
|
|
597
|
+
const kind = rule?.kind ?? 'harness';
|
|
598
|
+
const reason = rule?.reason;
|
|
599
|
+
observe('plan', { kind, reason: reason ?? '' });
|
|
600
|
+
return { kind, text: seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`, ...(reason ? { reason } : {}) };
|
|
146
601
|
}
|
|
147
602
|
|
|
148
603
|
// The launched sentence names the seat, the source and the release so same-named items stay distinct.
|
|
149
604
|
export function seatTaskText(seat: string, item: SeatItem): string {
|
|
150
|
-
const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : `자리 요청 ${item.id}`;
|
|
605
|
+
const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : item.source === 'hook' ? `웹훅 작업 카드 ${item.id}` : `자리 요청 ${item.id}`;
|
|
151
606
|
return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
|
|
152
607
|
}
|
|
153
608
|
|
|
@@ -162,21 +617,188 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
162
617
|
const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
|
|
163
618
|
const directory = dirname(path);
|
|
164
619
|
return withSeatLock(directory, async () => {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
620
|
+
const ledger = readSeatLedger(seat, deps, now);
|
|
621
|
+
const stateRoot = deps.root ?? effectiveInstanceRoot();
|
|
622
|
+
let turnError: unknown;
|
|
623
|
+
try {
|
|
624
|
+
if (seat === 'TC') {
|
|
625
|
+
const candidates = await tcCandidates(deps, now);
|
|
626
|
+
for (const candidate of candidates) {
|
|
627
|
+
if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
|
|
628
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
|
|
629
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
630
|
+
observe('tc-shadow-candidate', { seat, candidate });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (seat === 'OP' && !seatQuestions(stateRoot, 'OP').some((question) =>
|
|
634
|
+
!hasSeatAnswer(stateRoot, question)
|
|
635
|
+
&& !handledKeys(ledger, config.mode !== 'on' || (config.questions ?? 'shadow') !== 'on', stateRoot).has(`seat-question::${question.id}`))) {
|
|
636
|
+
const append = deps.append ?? defaultAppend;
|
|
637
|
+
const candidates = opCandidates(deps, now);
|
|
638
|
+
const assigned = candidates.filter((candidate): candidate is Extract<OpCandidate, { kind: 'unassigned-cell' }> => candidate.kind === 'unassigned-cell' && candidate.verdict === 'assign');
|
|
639
|
+
if (assigned.length) {
|
|
640
|
+
const previous = new Map<string, Extract<OpCandidate, { kind: 'unassigned-cell' }>>();
|
|
641
|
+
for (const row of ledger) {
|
|
642
|
+
const candidate = row.candidate;
|
|
643
|
+
if (candidate?.kind !== 'unassigned-cell') continue;
|
|
644
|
+
if (candidate.verdict === 'none' && !candidate.id) previous.clear();
|
|
645
|
+
else if (candidate.id) previous.set(JSON.stringify([candidate.version, candidate.id]), candidate);
|
|
646
|
+
}
|
|
647
|
+
const active = new Set(assigned.map((candidate) => JSON.stringify([candidate.version, candidate.id])));
|
|
648
|
+
for (const [key, candidate] of previous) {
|
|
649
|
+
if (candidate.verdict === 'assign' && !active.has(key)) candidates.push({ kind: 'unassigned-cell', version: candidate.version, id: candidate.id, verdict: 'none' });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const previousDecisions = new Set<string>();
|
|
653
|
+
for (const row of ledger) {
|
|
654
|
+
const candidate = row.candidate;
|
|
655
|
+
if (candidate?.kind !== 'decision-delegation') continue;
|
|
656
|
+
if (candidate.verdict === 'none' && !candidate.id) previousDecisions.clear();
|
|
657
|
+
else if (candidate.id) {
|
|
658
|
+
if (candidate.verdict === 'none') previousDecisions.delete(candidate.id);
|
|
659
|
+
else previousDecisions.add(candidate.id);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const currentDecision = candidates.find((candidate): candidate is Extract<OpCandidate, { kind: 'decision-delegation' }> =>
|
|
663
|
+
candidate.kind === 'decision-delegation' && candidate.verdict === 'review-delegation');
|
|
664
|
+
if (currentDecision) {
|
|
665
|
+
for (const id of previousDecisions) {
|
|
666
|
+
if (id !== currentDecision.id) candidates.push({ kind: 'decision-delegation', id, verdict: 'none' });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate }));
|
|
670
|
+
for (const entry of entries) {
|
|
671
|
+
const candidate = entry.candidate!;
|
|
672
|
+
const previous = [...ledger].reverse().find((row) => row.candidate?.kind === candidate.kind &&
|
|
673
|
+
(candidate.kind === 'release-readiness' || row.candidate?.kind === candidate.kind && (
|
|
674
|
+
candidate.kind === 'unassigned-cell' && row.candidate.kind === 'unassigned-cell'
|
|
675
|
+
? (candidate.verdict === 'none' && !candidate.id && row.candidate.verdict === 'none' && !row.candidate.id)
|
|
676
|
+
|| row.candidate.version === candidate.version && row.candidate.id === candidate.id
|
|
677
|
+
: candidate.kind === 'decision-delegation' && row.candidate.kind === 'decision-delegation'
|
|
678
|
+
&& row.candidate.id === candidate.id)));
|
|
679
|
+
if (previous && JSON.stringify(previous.candidate) === JSON.stringify(candidate)) continue;
|
|
680
|
+
append(path, entry);
|
|
681
|
+
observe('op-judgment-shadow', { seat, candidate: entry.candidate, status: entry.status });
|
|
682
|
+
}
|
|
683
|
+
return entries[0]!;
|
|
684
|
+
}
|
|
685
|
+
const inputs = await gatherSeatInputs(seat, deps, ledger);
|
|
686
|
+
const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
|
|
687
|
+
const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
|
|
688
|
+
const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question' && !questionHandled.has(itemKey(candidate)));
|
|
689
|
+
const ordinary = pickNext({ ...inputs, requests: inputs.requests.filter((candidate) => candidate.source !== 'seat-question') }, ledger,
|
|
690
|
+
{ shadow: config.mode === 'shadow', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
|
|
691
|
+
const item = pendingQuestion ?? ordinary;
|
|
692
|
+
const append = deps.append ?? defaultAppend;
|
|
693
|
+
// 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 && (item.source === 'seat-question' || deps.inquire || (config.mode === 'on' && config.questions !== undefined))) {
|
|
695
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
696
|
+
const unfinished = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
|
|
697
|
+
&& (row.status === 'attempting' || row.status === 'outcome-unknown') && (row.action === 'seat-question' || row.action === 'seat-answer'));
|
|
698
|
+
const inquiry = item.source === 'seat-question' ? null : unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
|
|
699
|
+
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
700
|
+
`[${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
|
+
const value: unknown = typeof output?.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
702
|
+
if (value === null) return null;
|
|
703
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid seat inquiry');
|
|
704
|
+
const record = value as Record<string, unknown>;
|
|
705
|
+
if ((record.to === 'CEO' || (typeof record.to === 'string' && isSeatId(record.to) && record.to !== asking))
|
|
706
|
+
&& typeof record.question === 'string' && record.question.trim()) return { to: record.to as SeatId | 'CEO', question: record.question };
|
|
707
|
+
throw new Error('invalid seat inquiry');
|
|
708
|
+
} catch (error) {
|
|
709
|
+
// A failed judgment call must not break the record-only path — no question this turn.
|
|
710
|
+
observe('inquire-failed', { seat: asking, item: work.id, error: String(error).slice(0, 200) });
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}))(seat as SeatId, item);
|
|
714
|
+
const reply = item.source === 'seat-question' && item.from && (questionsMode === 'on' || deps.reply || config.questions !== undefined)
|
|
715
|
+
? unfinished?.answer ? { answer: unfinished.answer } : unfinished?.inquiry ? unfinished.inquiry : await (deps.reply ?? (async (receiver: SeatId, question: string, from: SeatId) => {
|
|
716
|
+
const evidence = inputs.checklist.map((candidate) =>
|
|
717
|
+
`${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})`).join('\n').slice(0, 8_000);
|
|
718
|
+
const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
|
|
719
|
+
`[${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
|
+
const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
|
|
721
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid seat reply');
|
|
722
|
+
const record = value as Record<string, unknown>;
|
|
723
|
+
if (typeof record.answer === 'string' && record.answer.trim()) return { answer: record.answer };
|
|
724
|
+
if (record.to === 'CEO' && typeof record.question === 'string' && record.question.trim()) return { to: 'CEO' as const, question: record.question };
|
|
725
|
+
throw new Error('invalid seat reply');
|
|
726
|
+
}))(seat as SeatId, item.text, item.from) : null;
|
|
727
|
+
if (item.source === 'seat-question' && reply && 'to' in reply && reply.to !== 'CEO') {
|
|
728
|
+
throw new Error('seat reply may only answer or escalate to CEO');
|
|
729
|
+
}
|
|
730
|
+
if (item.source === 'seat-question' || inquiry) {
|
|
731
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), item, status: 'shadow',
|
|
732
|
+
action: item.source === 'seat-question' ? 'seat-answer' : inquiry?.to === 'CEO' ? 'decision' : 'seat-question',
|
|
733
|
+
...(inquiry ? { inquiry } : {}), ...(reply && 'answer' in reply ? { answer: reply.answer } : {}) };
|
|
734
|
+
if (item.source === 'seat-question' && reply && 'to' in reply) {
|
|
735
|
+
entry.inquiry = reply;
|
|
736
|
+
entry.action = 'decision';
|
|
737
|
+
}
|
|
738
|
+
if (questionsMode !== 'on') {
|
|
739
|
+
append(path, entry);
|
|
740
|
+
observe('question-shadow', { seat, item, inquiry: entry.inquiry, answered: !!entry.answer });
|
|
741
|
+
return entry;
|
|
742
|
+
}
|
|
743
|
+
if (item.source === 'seat-question' && !reply) {
|
|
744
|
+
entry.status = 'awaiting-answer';
|
|
745
|
+
append(path, entry);
|
|
746
|
+
return entry;
|
|
747
|
+
}
|
|
748
|
+
append(path, { ...entry, status: 'attempting' });
|
|
749
|
+
try {
|
|
750
|
+
if (item.source === 'seat-question' && reply && 'answer' in reply) {
|
|
751
|
+
const question = { id: Number(item.id), from: item.from!, to: seat, body: item.text, kind: 'seat-question', createdAt: item.createdAt ?? '' };
|
|
752
|
+
if (!hasSeatAnswer(root, question)) answerSeat(root, question, reply.answer);
|
|
753
|
+
entry.status = 'answered';
|
|
754
|
+
} else {
|
|
755
|
+
const ask = entry.inquiry!;
|
|
756
|
+
if (!ask.question.trim()) throw new Error('empty seat question');
|
|
757
|
+
if (ask.to === 'CEO') {
|
|
758
|
+
const prior = decisionReceipt(root, seat, item);
|
|
759
|
+
if (!prior) {
|
|
760
|
+
const run = deps.run ?? defaultRun;
|
|
761
|
+
await run(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
|
|
762
|
+
'--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
|
|
763
|
+
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
764
|
+
'--skip-recommend', '근거가 부족하다', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
|
|
765
|
+
}
|
|
766
|
+
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
767
|
+
entry.status = 'hitl';
|
|
768
|
+
} else {
|
|
769
|
+
if (!isSeatId(ask.to) || ask.to === seat) throw new Error('invalid recipient seat');
|
|
770
|
+
const key = `ask:${seat}:${itemKey(item)}`;
|
|
771
|
+
const delivered = deliveredSeatQuestion(root, key);
|
|
772
|
+
if (delivered && (delivered.from !== seat || delivered.to !== ask.to || delivered.body !== ask.question.trim())) {
|
|
773
|
+
throw new Error('seat delivery retry changed its recorded question');
|
|
774
|
+
}
|
|
775
|
+
askSeat(root, seat as SeatId, ask.to, ask.question, key);
|
|
776
|
+
entry.status = 'asked';
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
append(path, entry);
|
|
780
|
+
observe('question-outcome', { seat, item, status: entry.status });
|
|
781
|
+
return entry;
|
|
782
|
+
} catch (error) {
|
|
783
|
+
append(path, { ...entry, status: 'outcome-unknown' });
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
170
788
|
if (item) observe('picked', { seat, item, date: seatDay(now) });
|
|
171
789
|
const planned = item ? planAction(item, seat) : undefined;
|
|
172
790
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
|
|
173
791
|
...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
|
|
174
|
-
const append = deps.append ?? defaultAppend;
|
|
175
792
|
if (config.mode === 'shadow' || !item) {
|
|
176
793
|
append(path, entry);
|
|
177
794
|
if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
|
|
178
795
|
return entry;
|
|
179
796
|
}
|
|
797
|
+
if (planned!.kind === 'wait') {
|
|
798
|
+
entry.status = 'wait';
|
|
799
|
+
append(path, entry);
|
|
800
|
+
return entry;
|
|
801
|
+
}
|
|
180
802
|
const run = deps.run ?? defaultRun;
|
|
181
803
|
let outcome: string | undefined;
|
|
182
804
|
try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
|
|
@@ -190,6 +812,52 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
190
812
|
return entry;
|
|
191
813
|
}
|
|
192
814
|
const action = planned!;
|
|
815
|
+
if (action.kind === 'harness') {
|
|
816
|
+
const launchedRunIds = new Set(ledger.flatMap((row) => row.seat === seat && row.status === 'launched' && row.runId ? [row.runId] : []));
|
|
817
|
+
let running: readonly RunningSeatPod[] = [];
|
|
818
|
+
let observationUnavailable = false;
|
|
819
|
+
try {
|
|
820
|
+
if (deps.running) running = deps.running();
|
|
821
|
+
else if (launchedRunIds.size > 0) {
|
|
822
|
+
const observed = queryRunningRuns({ runIds: [...launchedRunIds], caller: 'seat-loop' });
|
|
823
|
+
if (observed.completeness !== 'complete' || observed.pty.unreadable.length > 0) observationUnavailable = true;
|
|
824
|
+
running = observed.entries
|
|
825
|
+
.filter((row) => launchedRunIds.has(row.runId) && (row.status === 'running' || row.status === 'probable-running' || row.status === 'unknown'))
|
|
826
|
+
// Seat launches always use `--substrate pod` (see the harness say call), so the substrate is known from the launch;
|
|
827
|
+
// only liveness can be uncertain — probable-running/unknown stay «unknown», never counted as confirmed Pods.
|
|
828
|
+
.map((row) => ({ seat, substrate: 'pod', status: row.status === 'running' ? 'running' : 'unknown' }));
|
|
829
|
+
}
|
|
830
|
+
} catch { observationUnavailable = true; }
|
|
831
|
+
const budget = observationUnavailable
|
|
832
|
+
? { allowed: false as const, reason: `${seat} concurrent Pods budget cannot be verified (running observation unavailable)` }
|
|
833
|
+
: checkSeatBudget({ seat, now, ledger, running, ...(deps.budgetConfig ? { config: deps.budgetConfig } : {}) });
|
|
834
|
+
if (!budget.allowed) {
|
|
835
|
+
entry.status = 'skipped-budget';
|
|
836
|
+
entry.action = 'skipped-budget';
|
|
837
|
+
entry.reason = budget.reason;
|
|
838
|
+
observe('skipped-budget', { seat, item, reason: budget.reason });
|
|
839
|
+
const parent = (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat];
|
|
840
|
+
// 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 && !ledger.some((prior) => prior.status === 'skipped-budget' && prior.escalated === true && prior.reason === budget.reason
|
|
842
|
+
&& prior.item && itemKey(prior.item) === itemKey(item) && seatDay(new Date(prior.ts ?? prior.at)) === seatDay(now))) {
|
|
843
|
+
const line = `[${seat} → ${parent}] ${item.id}: ${budget.reason}`;
|
|
844
|
+
try {
|
|
845
|
+
if (deps.escalate) await deps.escalate(parent, line);
|
|
846
|
+
else {
|
|
847
|
+
const store = openMsgStore(join(deps.root ?? effectiveInstanceRoot(), 'msg', 'messages.db'));
|
|
848
|
+
try { store.append({ from: seat, to: parent, body: line, kind: 'seat-budget-escalation' }); }
|
|
849
|
+
finally { store.close(); }
|
|
850
|
+
}
|
|
851
|
+
entry.escalated = true;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
entry.escalated = false;
|
|
854
|
+
observe('budget-escalation-failed', { seat, parent, item: item.id, error: String(error).slice(0, 200) });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
append(path, entry);
|
|
858
|
+
return entry;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
193
861
|
// Persist the intent before crossing the process boundary: an unreturned call may already have acted.
|
|
194
862
|
append(path, { ...entry, status: 'attempting' });
|
|
195
863
|
try {
|
|
@@ -227,5 +895,17 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
227
895
|
append(path, { ...entry, status: 'outcome-unknown' });
|
|
228
896
|
throw error;
|
|
229
897
|
}
|
|
898
|
+
} catch (error) {
|
|
899
|
+
turnError = error;
|
|
900
|
+
throw error;
|
|
901
|
+
} finally {
|
|
902
|
+
// A turn ends even when there is no picked item or a launch fails.
|
|
903
|
+
try { escalateStalls(seat, deps, config, (deps.now ?? (() => new Date()))()); }
|
|
904
|
+
catch (error) {
|
|
905
|
+
debug.log('org.stall', 'escalate-error', { seat, error: String(error).slice(0, 200) }, { level: 'warn' });
|
|
906
|
+
if (turnError !== undefined) throw new AggregateError([turnError, error], `seat turn failed (${String(turnError)}); stall escalation failed (${String(error)}) for ${seat}`);
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
230
910
|
}, deps.lockContended);
|
|
231
911
|
}
|