elanous 0.2.21 → 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-08b716476f1d50af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-88459e0903da2803.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-d781be3e3a9cfd74.js → page-d85c43f46f58716b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-b0a80997903852d8.js → page-4e645600579ae952.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-2b1263165630d9c8.js → page-87edbfd19117823b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-e2d6bed48ac20120.js → page-cbaf66b1e1804adf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-111cabe4d1fd69ae.js → page-80ed511e02c81412.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-544dd4bd4847eeb7.js → page-9f5610caaa06178c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-3ee191319bf97d87.js → page-ec8f3575e339de74.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a449f1ed472b81c2.js → page-2a19c5cd3b562580.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9045094d6d3b5cd0.js → page-b6a09cde2e07521a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-c06673596849ef45.js → page-f7eae1518ca0c353.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{layout-9f81233f29dcd409.js → layout-8462d6a543cbb2d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-abd86cd832aaa011.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8c3f168fc3f91939.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-7a12f61be4a579d1.js → page-786fb4504d0f2fa9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-c85b0589591a99c9.js → not-found-75209f14597dcb8f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-725a4da02dd129eb.js → page-bf30affe3a85af7e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-21bec4afeb2f0917.js → page-76cf838050a8d681.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-dc39ee7a36d26385.js → page-7ec8e289b4c14df6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-84ae759f76053672.js → page-8c8ac17d52be2a79.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-3db58ddb61fe2be9.js → page-d359ab048d63a9d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-c5315e30f3528dc4.js → page-b8b345c880bf78d1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-2fe368efb6de3b6a.js → page-f151cc10ad026940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-c69762987b751174.js → page-bf495a1e284f4e00.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9abcfac07e02a542.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-7f8d7fcdf058a845.js → page-69cb93d1a2e57e0e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-22163556cd38bc88.js → page-72ec543928d67c3e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
- package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/catalog/catalog.yaml +1 -1
- package/graphs/drafts/recipes.yaml +2 -2
- package/graphs/test-diet/recipes.yaml +3 -0
- package/graphs/test-diet/test-diet.yaml +3 -1
- package/package.json +1 -1
- package/scripts/f-session-monitor.ts +69 -0
- package/scripts/hq/standby-snapshot.ts +7 -3
- package/scripts/knowledge-compress.ts +11 -0
- package/scripts/nightly-docops.ts +90 -3
- package/scripts/research-coo-admin-dates.ts +39 -0
- package/scripts/skills/public-check.ts +7 -2
- package/scripts/smart-merge/resync-shadow.ts +5 -3
- package/scripts/test-diet/lib.ts +81 -1
- package/scripts/test-diet/node.ts +48 -2
- package/scripts/verify-coo-admin-evidence.ts +83 -0
- package/src/acp/client.ts +4 -2
- package/src/agent/run-context.ts +4 -0
- package/src/agent-env/profile.ts +25 -5
- package/src/agent-mission/driver.ts +26 -12
- package/src/agent-mission/supervisor-metrics.ts +17 -2
- package/src/agent-substrate/acp-reviewer.ts +12 -5
- package/src/agent-substrate/pr-reviewer.ts +11 -4
- package/src/agent-substrate/review-intent.ts +102 -3
- package/src/autopilot/build/llm-conflict-merge.ts +22 -8
- package/src/autopilot/pr-manager.ts +18 -8
- package/src/away/release-watch.ts +47 -8
- package/src/briefing/brief-items.ts +104 -11
- package/src/chat/index.ts +1 -0
- package/src/cli/brief-cli.ts +20 -2
- package/src/cli/config-classify.ts +83 -0
- package/src/cli/control-cli.ts +14 -0
- package/src/cli/decisions-cli.ts +26 -16
- package/src/cli/logs-model-input.ts +1 -1
- package/src/cli/pr-cli.ts +65 -8
- package/src/cli/release-cli.ts +21 -4
- package/src/cli/tasks-cli.ts +108 -1
- package/src/connectors/types.ts +6 -6
- package/src/context-bus/context-now-surfaces.ts +18 -0
- package/src/context-bus/coord-events.ts +32 -4
- package/src/context-bus/long-term-memory.ts +23 -6
- package/src/context-bus/nightly-condense.ts +169 -21
- package/src/control/control-status.ts +87 -0
- package/src/control-plane/light-member.ts +5 -3
- package/src/control-plane/member.ts +172 -7
- package/src/core-turn/run-goal-loop.ts +22 -1
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
- package/src/decisions/decision-cards.ts +20 -1
- package/src/decisions/decision-ledger.ts +11 -8
- package/src/decisions/web-push-decision-cards.ts +19 -3
- package/src/discord-self-message.ts +2 -2
- package/src/domains/coo-admin-dates.evidence.json +876 -0
- package/src/domains/coo-admin-tool.ts +48 -14
- package/src/domains/ops-status.ts +53 -0
- package/src/domains/outbound-alert.ts +24 -1
- package/src/exec-requests/planner.ts +6 -0
- package/src/graph-kinds/registry.ts +6 -6
- package/src/graph-runner/graph-cli.ts +34 -2
- package/src/graph-runner/graph-run-api.ts +7 -0
- package/src/graph-runner/graph-tick.ts +73 -12
- package/src/graph-runner/runner.ts +43 -11
- package/src/graph-unify/wf-to-graph.ts +79 -0
- package/src/graph-wizard/generate.ts +34 -2
- package/src/graph-wizard/steps.ts +13 -0
- package/src/harness/harness-cli-command.ts +121 -23
- package/src/harness/harness-queue.ts +58 -14
- package/src/harness/launch-advice.ts +23 -0
- package/src/index.ts +36 -7
- package/src/knowledge/conclusion-compress.ts +93 -0
- package/src/knowledge/know-find.ts +9 -2
- package/src/knowledge/query.ts +20 -0
- package/src/llm.ts +26 -16
- package/src/loops/neighbor-runtime.ts +88 -0
- package/src/loops/orchestrator/author-depth.ts +29 -11
- package/src/loops/orchestrator/overlap.ts +7 -1
- package/src/loops/orchestrator/resync-shadow.ts +1 -1
- package/src/loops/orchestrator/tick.ts +35 -2
- package/src/loops/orchestrator/traffic.ts +2 -0
- package/src/loops/registry.ts +5 -0
- package/src/market/publish.ts +17 -1
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/mss/logging/redaction.ts +17 -0
- package/src/nexus/api/draft-metrics.ts +22 -8
- package/src/nexus/api/graph-peer-edit.ts +77 -0
- package/src/nexus/api/graph-wizard-api.ts +9 -1
- package/src/nexus/api/graphs-api.ts +145 -7
- package/src/nexus/api/http-server.ts +93 -8
- package/src/nexus/api/plugins-market.ts +24 -0
- package/src/nexus/index.ts +3 -2
- package/src/oauth/claude-pty-maturity.ts +24 -0
- package/src/oauth/fallback-chain.ts +27 -14
- package/src/plugins/maker/market-bundle.ts +9 -0
- package/src/plugins/maker/plugin-maker.ts +9 -3
- package/src/plugins/maker/wizard-generate.ts +73 -9
- package/src/policy/llm-policy.ts +11 -6
- package/src/release-loop/checklist-board.ts +27 -0
- package/src/release-loop/checklist.ts +38 -0
- package/src/seat-address/seat-address.ts +1 -0
- package/src/seat-loop/seat-loop.ts +174 -20
- package/src/seat-loop/seat-report.ts +29 -1
- package/src/self-dev/dev-cli.ts +90 -8
- package/src/self-dev/dev-pipeline.ts +20 -1
- package/src/self-dev/draft-sweep.ts +45 -8
- package/src/self-dev/orchestrate.ts +3 -0
- package/src/self-dev/review-provider-fallback.ts +83 -0
- package/src/self-dev/run-supervisor.ts +5 -1
- package/src/self-implement/harness-policy-gates.ts +7 -5
- package/src/self-implement/host-regate.ts +28 -14
- package/src/self-implement/l8-merge-queue.ts +80 -8
- package/src/self-implement/orchestrator.ts +68 -27
- package/src/self-implement/seams.ts +5 -3
- package/src/self-implement/self-implement-cli.ts +4 -0
- package/src/shell-env-bootstrap.ts +19 -4
- package/src/steward/triage.ts +25 -8
- package/src/storage/file-lock.ts +33 -4
- package/src/task-agent/completion-evidence.ts +31 -3
- package/src/task-agent/cover.ts +61 -0
- package/src/task-agent/live-moves.ts +126 -3
- package/src/task-agent/shadow.ts +19 -4
- package/src/task-agent/task-hand.ts +2 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
- package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
- package/src/telegram-commands.ts +8 -0
- package/src/telegram-project-command.ts +6 -0
- package/src/telegram.ts +2 -2
- package/src/typecheck-ratchet.ts +2 -1
- package/src/user-config.ts +35 -4
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +24 -2
- package/src/workflow-runtime/triggers/registry.ts +27 -0
- package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
- package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8cc707bd09a497d9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-a6eb73b8a4840f1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6c0b88068c143f15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +0 -1
- package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +0 -1
- /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
4
|
|
|
5
5
|
/** Research is descriptive. Credential values are never an input to the generator. */
|
|
@@ -16,7 +16,7 @@ const SLUG = /^[a-z0-9][a-z0-9-]{1,31}$/;
|
|
|
16
16
|
const FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
17
17
|
|
|
18
18
|
/** Materialize a research draft; its graph steps fail until the requested processing is implemented. */
|
|
19
|
-
export function generateWizardFiles(dir: string, slug: string, draft: WizardResearchDraft): string[] {
|
|
19
|
+
export function generateWizardFiles(dir: string, slug: string, draft: WizardResearchDraft, options: { regenerate?: boolean } = {}): string[] {
|
|
20
20
|
if (!SLUG.test(slug)) throw new Error(`invalid wizard plugin name: ${slug}`);
|
|
21
21
|
if (!draft || typeof draft.description !== 'string' || !draft.description.trim()) throw new Error('wizard description is required');
|
|
22
22
|
if (draft.connectors !== undefined && !Array.isArray(draft.connectors)) throw new Error('invalid wizard connectors');
|
|
@@ -48,21 +48,30 @@ export function generateWizardFiles(dir: string, slug: string, draft: WizardRese
|
|
|
48
48
|
}
|
|
49
49
|
const manifestPath = join(dir, 'plugin.json');
|
|
50
50
|
const existing = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
|
51
|
-
name: string; extensions: { 'ai.elanous': { connectors?: unknown[]; graphs?: string[]; researchDraft?: boolean } };
|
|
51
|
+
name: string; description?: string; extensions: { 'ai.elanous': { connectors?: unknown[]; graphs?: string[]; researchDraft?: boolean } };
|
|
52
52
|
} : undefined;
|
|
53
53
|
if (existing && (existing.name !== slug || !existing.extensions?.['ai.elanous'])) {
|
|
54
54
|
throw new Error('wizard scaffold name or extension mismatch');
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const skillPath = join(dir, `skills/${slug}/SKILL.md`);
|
|
57
|
+
const readmePath = join(dir, 'README.md');
|
|
58
|
+
const wizardReadme = options.regenerate && existsSync(readmePath) &&
|
|
59
|
+
readFileSync(readmePath, 'utf8').includes('Research draft only. Connector declarations contain credential names, not values.');
|
|
60
|
+
if (options.regenerate) {
|
|
61
|
+
if (!existing || existing.extensions['ai.elanous'].researchDraft !== true || !existsSync(skillPath)) {
|
|
62
|
+
throw new Error('wizard regeneration requires an existing research draft');
|
|
63
|
+
}
|
|
64
|
+
} else if (existsSync(skillPath)) {
|
|
57
65
|
throw new Error(`wizard file already exists: skills/${slug}/SKILL.md`);
|
|
58
66
|
}
|
|
59
67
|
const scaffoldConnectors = existing?.extensions['ai.elanous'].connectors;
|
|
60
|
-
if (scaffoldConnectors !== undefined && (!Array.isArray(scaffoldConnectors) || scaffoldConnectors.length > 0)) {
|
|
68
|
+
if (!options.regenerate && scaffoldConnectors !== undefined && (!Array.isArray(scaffoldConnectors) || scaffoldConnectors.length > 0)) {
|
|
61
69
|
throw new Error('wizard cannot replace existing scaffold connectors');
|
|
62
70
|
}
|
|
63
71
|
const manifest = existing ?? { name: slug, version: '0.1.0', description: draft.description,
|
|
64
72
|
extensions: { 'ai.elanous': { graphs: [`./graphs/${slug}.yaml`],
|
|
65
73
|
capabilities: ['fs:workdir', 'proc:bun', 'proc:elanous'], connectors: [] as unknown[], researchDraft: true } } };
|
|
74
|
+
if (options.regenerate) manifest.description = draft.description;
|
|
66
75
|
manifest.extensions['ai.elanous'].connectors = connectors;
|
|
67
76
|
manifest.extensions['ai.elanous'].researchDraft = true;
|
|
68
77
|
|
|
@@ -90,14 +99,69 @@ process.exitCode = 1;
|
|
|
90
99
|
['README.md', `# ${slug}\n\n${draft.description}\n\nResearch draft only. Connector declarations contain credential names, not values. The graph cannot perform the requested processing yet: implement and validate its steps, then remove extensions["ai.elanous"].researchDraft from plugin.json before installing or running it. No messages are sent.\n`],
|
|
91
100
|
[`skills/${slug}/SKILL.md`, `---\n${stringifyYaml({ name: slug, description: draft.skill?.description ?? draft.description, requires: [...new Set(requires)] })}---\n\n${draft.skill?.instructions ?? draft.description}\n`],
|
|
92
101
|
];
|
|
93
|
-
const generated = existing ? files.filter(([path]) => path === 'plugin.json' || path.startsWith('skills/')
|
|
102
|
+
const generated = existing ? files.filter(([path]) => path === 'plugin.json' || path.startsWith('skills/') ||
|
|
103
|
+
(options.regenerate && wizardReadme && path === 'README.md')) : files;
|
|
94
104
|
for (const [path] of generated) {
|
|
95
|
-
if (path !== 'plugin.json' && existsSync(join(dir, path)))
|
|
105
|
+
if (path !== 'plugin.json' && !(options.regenerate && (path === 'README.md' || path.startsWith('skills/'))) && existsSync(join(dir, path))) {
|
|
106
|
+
throw new Error(`wizard file already exists: ${path}`);
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
for (const [path, content] of generated) {
|
|
98
110
|
const target = join(dir, path);
|
|
99
111
|
mkdirSync(join(target, '..'), { recursive: true });
|
|
100
|
-
writeFileSync(target, content, { flag: path === 'plugin.json'
|
|
112
|
+
writeFileSync(target, content, { flag: existing && (path === 'plugin.json' || options.regenerate) ? 'w' : 'wx' });
|
|
101
113
|
}
|
|
102
114
|
return generated.map(([path]) => path);
|
|
103
115
|
}
|
|
116
|
+
|
|
117
|
+
/** Browser wizard drafts are stored outside the install ledger. Only wizard-owned directories are listed. */
|
|
118
|
+
export function listWizardDrafts(parent: string): Array<{ name: string; description: string; draft: WizardResearchDraft }> {
|
|
119
|
+
if (!existsSync(parent)) return [];
|
|
120
|
+
return readdirSync(parent, { withFileTypes: true }).filter(entry => entry.isDirectory() && SLUG.test(entry.name)).flatMap(entry => {
|
|
121
|
+
const dir = join(parent, entry.name);
|
|
122
|
+
try {
|
|
123
|
+
const manifestPath = join(dir, 'plugin.json');
|
|
124
|
+
const skillDir = join(dir, 'skills', entry.name);
|
|
125
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
126
|
+
if (lstatSync(manifestPath).isSymbolicLink() || lstatSync(skillDir).isSymbolicLink() || lstatSync(skillPath).isSymbolicLink()) return [];
|
|
127
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
|
128
|
+
name?: string; description?: string; extensions?: { 'ai.elanous'?: { researchDraft?: boolean; connectors?: Array<{ id: string; fields: Array<{ name: string }> }> } } };
|
|
129
|
+
if (manifest.name !== entry.name || manifest.extensions?.['ai.elanous']?.researchDraft !== true) return [];
|
|
130
|
+
const skill = readFileSync(skillPath, 'utf8');
|
|
131
|
+
const parts = /^---\n([\s\S]*?)---\n\n([\s\S]*)$/.exec(skill);
|
|
132
|
+
if (!parts || typeof manifest.description !== 'string') return [];
|
|
133
|
+
const meta = parseYaml(parts[1]!) as { description?: string; requires?: string[] };
|
|
134
|
+
return [{ name: entry.name, description: manifest.description, draft: {
|
|
135
|
+
description: manifest.description,
|
|
136
|
+
connectors: manifest.extensions['ai.elanous'].connectors?.map(connector => ({ id: connector.id, credentials: connector.fields.map(field => ({ name: field.name })) })) ?? [],
|
|
137
|
+
skill: { description: meta.description ?? manifest.description, instructions: parts[2]!.trimEnd(), requires: meta.requires ?? [] },
|
|
138
|
+
} }];
|
|
139
|
+
} catch { return []; }
|
|
140
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function saveWizardDraft(parent: string, slug: string, draft: WizardResearchDraft, regenerate = false): string[] {
|
|
144
|
+
if (!SLUG.test(slug)) throw new Error(`invalid wizard plugin name: ${slug}`);
|
|
145
|
+
const requested = resolve(parent);
|
|
146
|
+
if (existsSync(requested) && lstatSync(requested).isSymbolicLink()) throw new Error('wizard parent cannot be a symlink');
|
|
147
|
+
// Ancestors may be symlinks (macOS /var -> /private/var); compare the draft directory against the real parent.
|
|
148
|
+
const base = existsSync(requested) ? realpathSync(requested) : requested;
|
|
149
|
+
const dir = resolve(base, slug);
|
|
150
|
+
if (!dir.startsWith(`${base}${sep}`)) throw new Error('invalid wizard directory');
|
|
151
|
+
if (existsSync(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory() || realpathSync(dir) !== dir)) {
|
|
152
|
+
throw new Error('wizard directory must be a local directory');
|
|
153
|
+
}
|
|
154
|
+
if (regenerate && existsSync(join(dir, 'skills')) && lstatSync(join(dir, 'skills')).isSymbolicLink()) {
|
|
155
|
+
throw new Error('wizard skills cannot be a symlink');
|
|
156
|
+
}
|
|
157
|
+
if (!regenerate && existsSync(dir)) throw new Error('wizard directory already exists');
|
|
158
|
+
if (regenerate && existsSync(join(dir, 'skills', slug)) && lstatSync(join(dir, 'skills', slug)).isSymbolicLink()) {
|
|
159
|
+
throw new Error('wizard skills cannot be a symlink');
|
|
160
|
+
}
|
|
161
|
+
if (regenerate && ['plugin.json', 'README.md', `skills/${slug}/SKILL.md`].some(file => {
|
|
162
|
+
const path = join(dir, file);
|
|
163
|
+
return existsSync(path) && lstatSync(path).isSymbolicLink();
|
|
164
|
+
})) throw new Error('wizard regeneration cannot follow symlinks');
|
|
165
|
+
if (!existsSync(base) && !regenerate) mkdirSync(base, { recursive: true });
|
|
166
|
+
return generateWizardFiles(dir, slug, draft, { regenerate });
|
|
167
|
+
}
|
package/src/policy/llm-policy.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { isScalar, parseDocument } from 'yaml';
|
|
|
4
4
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
5
5
|
import { DEFAULT_FALLBACK_CHAIN, isFallbackStep, type FallbackStep } from '../oauth/fallback-chain.js';
|
|
6
6
|
import { isCodexQuotaPolicy } from '../oauth/codex-quota-policy.js';
|
|
7
|
-
import { DEFAULT_BUDGET_GATE_MAX_USED_PERCENT, MODEL_ROLES, RUNTIME_LLM_PROVIDER_NAMES, parseRoleLlmEntry, type RoleLlmConfig } from '../user-config.js';
|
|
7
|
+
import { DEFAULT_BUDGET_GATE_MAX_USED_PERCENT, MODEL_ROLES, RUNTIME_LLM_PROVIDER_NAMES, parseRoleLlmEntry, parseSubscriptionReviewerSpec, type RoleLlmConfig } from '../user-config.js';
|
|
8
8
|
|
|
9
9
|
export interface LlmPolicy {
|
|
10
10
|
version: 1;
|
|
@@ -22,7 +22,7 @@ export interface LlmPolicy {
|
|
|
22
22
|
grants: Array<{ account: string; amount: number; expires: string; source: string }>;
|
|
23
23
|
pace?: { targetPerDay: number; until: string; why?: string };
|
|
24
24
|
};
|
|
25
|
-
caps: { codex: { harness: number; pod: number; tox: number; intake: number; headroom: number }; grok: { weekly: number; tox: number; intake: number } };
|
|
25
|
+
caps: { codex: { harness: number; pod: number; tox: number; intake: number; headroom: number }; grok: { weekly: number; tox: number; intake: number }; claude: { harness: number } };
|
|
26
26
|
resetCredits: { redeem: 'human' };
|
|
27
27
|
alerts: { lowRemainingPercent: number; resetCreditExpiryDays: number; creditsStep: number };
|
|
28
28
|
}
|
|
@@ -71,6 +71,7 @@ export function defaultLlmPolicy(): LlmPolicy {
|
|
|
71
71
|
caps: {
|
|
72
72
|
codex: { harness: DEFAULT_BUDGET_GATE_MAX_USED_PERCENT['openai-codex']!, pod: 95, tox: 60, intake: 80, headroom: 15 },
|
|
73
73
|
grok: { weekly: DEFAULT_BUDGET_GATE_MAX_USED_PERCENT.grok!, tox: 50, intake: 48 },
|
|
74
|
+
claude: { harness: 60 },
|
|
74
75
|
},
|
|
75
76
|
resetCredits: { redeem: 'human' },
|
|
76
77
|
alerts: { lowRemainingPercent: 10, resetCreditExpiryDays: 7, creditsStep: 5000 },
|
|
@@ -103,8 +104,10 @@ export function validateLlmPolicy(input: unknown): LlmPolicyValidation {
|
|
|
103
104
|
const grants = (v: unknown) => Array.isArray(v) && v.every(g => object(g)
|
|
104
105
|
&& Object.keys(g).every(k => ['account', 'amount', 'expires', 'source'].includes(k))
|
|
105
106
|
&& nonempty(g.account) && positive(g.amount) && date(g.expires) && nonempty(g.source));
|
|
106
|
-
const roles = (v: unknown) => object(v) && Object.entries(v).every(([role, spec]) =>
|
|
107
|
-
|
|
107
|
+
const roles = (v: unknown) => object(v) && Object.entries(v).every(([role, spec]) => role === 'reviewer'
|
|
108
|
+
? !!parseSubscriptionReviewerSpec(spec)
|
|
109
|
+
: MODEL_ROLES.some(r => r === role) && object(spec)
|
|
110
|
+
&& Object.keys(spec).every(k => ['provider', 'tier', 'model'].includes(k)) && parseRoleLlmEntry(spec).ok);
|
|
108
111
|
check(input, 'policy', {
|
|
109
112
|
version: (v: unknown) => v === 1,
|
|
110
113
|
default: { provider: (v: unknown) => nonempty(v) && RUNTIME_LLM_PROVIDER_NAMES.some(p => p === v), model: nonempty },
|
|
@@ -122,7 +125,7 @@ export function validateLlmPolicy(input: unknown): LlmPolicyValidation {
|
|
|
122
125
|
&& Object.keys(v).every(k => ['targetPerDay', 'until', 'why'].includes(k)),
|
|
123
126
|
},
|
|
124
127
|
caps: { codex: { harness: percent, pod: percent, tox: percent, intake: percent, headroom: percent },
|
|
125
|
-
grok: { weekly: percent, tox: percent, intake: percent } },
|
|
128
|
+
grok: { weekly: percent, tox: percent, intake: percent }, claude: { harness: percent } },
|
|
126
129
|
resetCredits: { redeem: (v: unknown) => v === 'human' },
|
|
127
130
|
alerts: { lowRemainingPercent: percent, resetCreditExpiryDays: positive, creditsStep: positive },
|
|
128
131
|
});
|
|
@@ -191,7 +194,9 @@ export function loadLlmPolicy(opts: LoadLlmPolicyOptions = {}): LoadedLlmPolicy
|
|
|
191
194
|
add('llm.provider', llm.provider, base, 'provider', v => nonempty(v) && RUNTIME_LLM_PROVIDER_NAMES.some(p => p === v));
|
|
192
195
|
add('llm.model', llm.model, base, 'model', nonempty);
|
|
193
196
|
if (Object.keys(base).length) overlay.default = base;
|
|
194
|
-
add('roleLlm', legacy.roleLlm, overlay, 'roles', v => object(v) && Object.entries(v).every(([r, spec]) =>
|
|
197
|
+
add('roleLlm', legacy.roleLlm, overlay, 'roles', v => object(v) && Object.entries(v).every(([r, spec]) => r === 'reviewer'
|
|
198
|
+
? !!parseSubscriptionReviewerSpec(spec)
|
|
199
|
+
: MODEL_ROLES.some(k => k === r) && parseRoleLlmEntry(spec).ok));
|
|
195
200
|
add('llm.fallbackChain', llm.fallbackChain, overlay, 'fallback', v => Array.isArray(v) && v.length > 0 && v.every(isFallbackStep) && new Set(v).size === v.length);
|
|
196
201
|
const codex: Obj = {};
|
|
197
202
|
add('llm.codexAccountOrder', llm.codexAccountOrder, codex, 'order', v => Array.isArray(v) && v.every(nonempty));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { checklistDevVersion, listChecklist, summarizeChecklist, type Checklist } from './checklist.js';
|
|
3
|
+
|
|
4
|
+
/** One text board shared by the Telegram and TUI slash surfaces. */
|
|
5
|
+
export function renderChecklistBoard(checklist: Checklist): string {
|
|
6
|
+
const { green, yellow, red, done, blocked } = summarizeChecklist(checklist);
|
|
7
|
+
const { bySeat } = summarizeChecklist({
|
|
8
|
+
...checklist,
|
|
9
|
+
items: checklist.items.filter((item) => item.status === 'yellow'),
|
|
10
|
+
});
|
|
11
|
+
return [
|
|
12
|
+
`판 ${checklist.version} · 초록 ${green} · 노랑 ${yellow} · 빨강 ${red} · 끝 ${done}`,
|
|
13
|
+
`빨강 칸: ${blocked.join(', ') || '-'}`,
|
|
14
|
+
`자리별 노랑: ${Object.entries(bySeat).sort(([a], [b]) => a.localeCompare(b)).map(([seat, count]) => `${seat} ${count}`).join(' · ') || '-'}`,
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read and render the selected board without turning failures into zero counts. */
|
|
19
|
+
export function checklistBoardSlash(args: readonly string[], read: (version: string) => Checklist = listChecklist): string {
|
|
20
|
+
if (args.length > 1) return '사용법: /board [판]';
|
|
21
|
+
try {
|
|
22
|
+
return renderChecklistBoard(read(args[0] ?? checklistDevVersion()));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
debug.log('release.checklist', 'board-read-failed', { version: args[0] ?? null, error: error instanceof Error ? error.message : String(error) });
|
|
25
|
+
return '판 체크리스트를 못 읽었다 — 「없다」가 아니다';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -175,6 +175,44 @@ export function cellsReferencingDoc(doc: string, checklists: readonly Checklist[
|
|
|
175
175
|
return rows;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** 문서 절과 판 칸의 `#절` 인용을 양방향으로 대조한다. 원장은 전달받은 스냅샷만 읽는다. */
|
|
179
|
+
export function lintDocRefs(docPath: string, docText: string, checklists: readonly Checklist[]): {
|
|
180
|
+
sections: string[];
|
|
181
|
+
uncoveredSections: string[];
|
|
182
|
+
danglingRefs: Array<{ version: string; id: string; ref: string }>;
|
|
183
|
+
} {
|
|
184
|
+
const path = splitRef(docPath.trim()).path;
|
|
185
|
+
const sections: string[] = [];
|
|
186
|
+
let fence: { marker: string; length: number } | null = null;
|
|
187
|
+
for (const line of docText.split(/\r?\n/)) {
|
|
188
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
189
|
+
if (fence) {
|
|
190
|
+
if (marker?.[0] === fence.marker && marker.length >= fence.length && /^\s*$/.test(line.slice(line.indexOf(marker) + marker.length))) fence = null;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (marker) { fence = { marker: marker[0]!, length: marker.length }; continue; }
|
|
194
|
+
const heading = /^ {0,3}#{2,4}[ \t]+(\S+)/.exec(line);
|
|
195
|
+
if (heading) {
|
|
196
|
+
const section = heading[1]!.replace(/[.:]$/, '');
|
|
197
|
+
if (!sections.includes(section)) sections.push(section);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const known = new Set(sections);
|
|
201
|
+
const cited = new Set<string>();
|
|
202
|
+
const danglingRefs: Array<{ version: string; id: string; ref: string }> = [];
|
|
203
|
+
for (const data of checklists) {
|
|
204
|
+
for (const item of data.items) {
|
|
205
|
+
for (const ref of item.refs ?? []) {
|
|
206
|
+
const got = splitRef(ref);
|
|
207
|
+
if (got.path !== path || !got.section) continue;
|
|
208
|
+
if (known.has(got.section)) cited.add(got.section);
|
|
209
|
+
else danglingRefs.push({ version: data.version, id: item.id, ref });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { sections, uncoveredSections: sections.filter((section) => !cited.has(section)), danglingRefs };
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
/** `| 판 | 칸 | 상태 | 절 | 제목 |` 마크다운 표 — 행이 없으면 머리줄 둘만. */
|
|
179
217
|
export function renderRefsStatus(rows: readonly DocRefRow[]): string {
|
|
180
218
|
const cell = (value: string) => value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
@@ -9,6 +9,7 @@ export type SeatEntry = {
|
|
|
9
9
|
export type SeatAddress = { seats: string[]; body: string };
|
|
10
10
|
|
|
11
11
|
const registry: readonly SeatEntry[] = trackData.tracks.filter((entry) => 'title' in entry);
|
|
12
|
+
export function registeredSeats(): readonly SeatEntry[] { return registry; }
|
|
12
13
|
const addressLine = /^@([A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*)(?:[ \t]+|\r?\n|(?![\s\S]))/gm;
|
|
13
14
|
|
|
14
15
|
/** Parse an address only when its @ begins a line; leave the instruction text intact. */
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { Database } from 'bun:sqlite';
|
|
5
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
|
|
8
8
|
import { debug } from '../debug/log.js';
|
|
9
9
|
import { openSurfaceEventsDb, recordEvent, surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
10
10
|
import { judgeNeighbor } from '../loops/neighbors.js';
|
|
11
|
+
import { checkLoopNeighbors, emitLoopHeartbeat, SEAT_NEIGHBORS } from '../loops/neighbor-runtime.js';
|
|
11
12
|
import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
|
|
12
13
|
import { isRescueAllowedAction } from '../steward/rescue.js';
|
|
13
14
|
import { DecisionLedger, type DecisionEntry, type DecisionOption, type Recommendation } from '../decisions/decision-ledger.js';
|
|
@@ -20,6 +21,7 @@ import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
|
|
|
20
21
|
import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
21
22
|
import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
|
|
22
23
|
import { PersonaRegistry } from '../persona/registry.js';
|
|
24
|
+
import { registeredSeats, resolveSeat } from '../seat-address/seat-address.js';
|
|
23
25
|
import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
|
|
24
26
|
import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
|
|
25
27
|
import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
|
|
@@ -29,7 +31,7 @@ import { readSchedules, releasedVersion } from '../release-loop/feature-store.js
|
|
|
29
31
|
import { runOpDecide, type OpDecideCandidate } from './op-decide.js';
|
|
30
32
|
|
|
31
33
|
// `kind`/`createdAt` are the V3 shadow-compare keys (내부 문서 `METHOD-v3-shadow-compare-2026-10-02` · MK 10-02 18:54).
|
|
32
|
-
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 };
|
|
34
|
+
export type SeatItem = { source: 'request' | 'checklist' | 'hook' | 'seat-question' | 'idle'; 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; idleRung?: 1 | 2 | 3 | 4 | 5 };
|
|
33
35
|
export type SeatInputs = { requests: SeatItem[]; checklist: SeatItem[]; role: string };
|
|
34
36
|
// V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door, wait or skipped-*) · `reason` (matched rule or forbidden word).
|
|
35
37
|
export type SeatAction = 'decision' | 'harness' | 'wait' | 'seat-question' | 'seat-answer' | 'skipped-budget' | 'skipped-empty';
|
|
@@ -44,7 +46,7 @@ export type TcCandidate =
|
|
|
44
46
|
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
45
47
|
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
46
48
|
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 };
|
|
47
|
-
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'resolved' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate | OpDecideCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
49
|
+
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'resolved' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate | OpDecideCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number; emptyQueueMinutes?: number };
|
|
48
50
|
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
|
|
49
51
|
|
|
50
52
|
export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
@@ -93,6 +95,8 @@ export type SeatDeps = {
|
|
|
93
95
|
pullRequests?: () => Promise<TcPullRequest[]>;
|
|
94
96
|
ledgerFiles?: (directory: string) => string[];
|
|
95
97
|
lockContended?: () => void;
|
|
98
|
+
/** Override the heartbeat timer in isolated long-turn tests; production uses the seat contract's interval. */
|
|
99
|
+
heartbeatIntervalMs?: number;
|
|
96
100
|
/** Supply a question only when this seat lacks evidence for the selected work item. */
|
|
97
101
|
inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
|
|
98
102
|
/** Supply the receiving seat's answer; absent evidence must not invent a reply. */
|
|
@@ -570,8 +574,8 @@ function opDecidePass(config: SeatLoopConfig, deps: SeatDeps, now: Date, path: s
|
|
|
570
574
|
mode,
|
|
571
575
|
ledger: cardLedger(stateRoot, deps, now),
|
|
572
576
|
runOwner: deps.runOwner ?? ((runId) => {
|
|
573
|
-
owners ??= new Map((
|
|
574
|
-
readSeatLedger(owner, deps, now).filter((row) => row.runId).map((row) => [row.runId!, owner] as const)));
|
|
577
|
+
owners ??= new Map(registeredSeats().filter(({ id }) => isSeatId(id)).flatMap(({ id: owner }) =>
|
|
578
|
+
readSeatLedger(owner, deps, now).filter((row) => row.runId).map((row) => [row.runId!, owner as SeatId] as const)));
|
|
575
579
|
return owners.get(runId);
|
|
576
580
|
}),
|
|
577
581
|
routeToSeat: (to, card, reason) => {
|
|
@@ -691,9 +695,11 @@ function readTcChecklist(root: string): Array<{ version: string; id: string; tit
|
|
|
691
695
|
if (!existsSync(path)) return [];
|
|
692
696
|
const db = new Database(path, { readonly: true, strict: true });
|
|
693
697
|
try {
|
|
698
|
+
const seats = registeredSeats().filter(({ id }) => isSeatId(id)).map(({ id }) => id);
|
|
699
|
+
if (!seats.length) return [];
|
|
694
700
|
return db.query(`SELECT a.version, f.id, COALESCE(a.title_override, f.title) AS title, a.status, a.owner
|
|
695
|
-
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN (
|
|
696
|
-
ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
|
|
701
|
+
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN (${seats.map(() => '?').join(', ')})
|
|
702
|
+
ORDER BY a.version, f.id`).all(...seats) as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
|
|
697
703
|
} finally { db.close(); }
|
|
698
704
|
}
|
|
699
705
|
|
|
@@ -946,13 +952,48 @@ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' |
|
|
|
946
952
|
const kind = rule?.kind ?? 'harness';
|
|
947
953
|
const reason = rule?.reason;
|
|
948
954
|
observe('plan', { kind, reason: reason ?? '' });
|
|
949
|
-
|
|
955
|
+
const text = seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`;
|
|
956
|
+
// Observed once per plan (not per queue comparison): how many target paths the launched ask carries.
|
|
957
|
+
if (seat && kind === 'harness' && text.startsWith('대상 경로: ')) observe('target-paths', { item: item.id, count: text.split('\n', 1)[0]!.split(' · ').length });
|
|
958
|
+
return { kind, text, ...(reason ? { reason } : {}) };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** A path counts only when it resolves to a regular file whose real location stays inside `root` (symlinks out are refused). */
|
|
962
|
+
export function repoFileExists(root: string = repoRoot): (path: string) => boolean {
|
|
963
|
+
return (path) => {
|
|
964
|
+
try {
|
|
965
|
+
const absolute = resolve(root, path);
|
|
966
|
+
const real = relative(realpathSync(root), realpathSync(absolute));
|
|
967
|
+
return !!real && real !== '..' && !real.startsWith(`..${sep}`) && !isAbsolute(real) && statSync(absolute).isFile();
|
|
968
|
+
} catch { return false; }
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Repository paths already written in a checklist item's title or evidence (dedup · first-seen order · at most six).
|
|
973
|
+
export function seatTargetPaths(item: SeatItem, isFile: (path: string) => boolean = repoFileExists()): string[] {
|
|
974
|
+
const paths: string[] = [];
|
|
975
|
+
const text = `${item.title}\n${item.evidence ?? ''}`;
|
|
976
|
+
const matches = text.matchAll(/(?:^|[^A-Za-z0-9_./-])((?:src|apps|scripts|test|docs|bin|graphs|packs|plugins)\/[A-Za-z0-9_./-]+)/g);
|
|
977
|
+
for (const match of matches) {
|
|
978
|
+
const path = match[1]!.replace(/[./]+$/, '');
|
|
979
|
+
if (path.split('/').includes('..')) continue;
|
|
980
|
+
const fromRoot = relative(repoRoot, resolve(repoRoot, path));
|
|
981
|
+
if (!fromRoot || fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot) || !isFile(path)) continue;
|
|
982
|
+
if (!paths.includes(path)) paths.push(path);
|
|
983
|
+
if (paths.length === 6) break;
|
|
984
|
+
}
|
|
985
|
+
return paths;
|
|
950
986
|
}
|
|
951
987
|
|
|
952
988
|
// The launched sentence names the seat, the source and the release so same-named items stay distinct.
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
989
|
+
// Checklist items whose title/evidence already name repository files get a leading `대상 경로:` line
|
|
990
|
+
// (the label parseAskTargetPathHints reads); without paths the text is byte-identical to the old one-liner.
|
|
991
|
+
export function seatTaskText(seat: string, item: SeatItem, isFile?: (path: string) => boolean): string {
|
|
992
|
+
const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : item.source === 'idle' ? `쉬는 시간 사다리 ${item.id} · ${item.idleRung}단계${item.version ? ` · ${item.version}` : ''}` : item.source === 'hook' ? `웹훅 작업 카드 ${item.id}` : `자리 요청 ${item.id}`;
|
|
993
|
+
const text = `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
|
|
994
|
+
if (item.source !== 'checklist') return text;
|
|
995
|
+
const paths = seatTargetPaths(item, isFile);
|
|
996
|
+
return paths.length ? `대상 경로: ${paths.join(' · ')}\n${text}` : text;
|
|
956
997
|
}
|
|
957
998
|
|
|
958
999
|
function xcheckEvent(event: string, seat: string, neighbor: string, item: string, outcome: string): void {
|
|
@@ -986,8 +1027,10 @@ function neighborLastSeen(root: string, isolated: boolean, now: Date, wanted: Re
|
|
|
986
1027
|
}
|
|
987
1028
|
|
|
988
1029
|
function neighborSeat(id: string): string | null {
|
|
989
|
-
const match = /^(
|
|
990
|
-
|
|
1030
|
+
const match = /^([A-Za-z][A-Za-z0-9_-]*)-seat$/i.exec(id);
|
|
1031
|
+
if (!match) return null;
|
|
1032
|
+
const resolved = resolveSeat(match[1]!)?.id;
|
|
1033
|
+
return resolved && isSeatId(resolved) ? resolved : null;
|
|
991
1034
|
}
|
|
992
1035
|
|
|
993
1036
|
function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps, root: string, now: Date): void {
|
|
@@ -1034,8 +1077,89 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
|
|
|
1034
1077
|
}
|
|
1035
1078
|
}
|
|
1036
1079
|
|
|
1080
|
+
const IDLE_WORK: Record<SeatId, string> = {
|
|
1081
|
+
MK: '문서 최적화: 현재 문서의 탐색·명료성·검색 유입을 실물 근거로 개선',
|
|
1082
|
+
UX: '멀티 서피스: 실제 채널별 사용자 흐름을 대조하고 결함을 개선',
|
|
1083
|
+
TC: '하니스 자가 치유: 실패 원장을 대조하여 재현 가능한 결함을 수리',
|
|
1084
|
+
OP: '보충 저작: 판 체크리스트의 빠진 칸을 근거와 반증 기준으로 보완',
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
function idleCell(seat: string, rung: 1 | 2 | 3 | 4 | 5, id: string, title: string, now: Date, version?: string, owner?: string, evidence?: string): SeatItem {
|
|
1088
|
+
return { source: 'idle', id, title, text: title, idleRung: rung, seat: owner ?? seat,
|
|
1089
|
+
...(version ? { version } : {}), ...(evidence ? { evidence } : {}), asOf: now.toISOString() };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function emptyQueueMinutes(ledger: readonly SeatEntry[], now: Date): number {
|
|
1093
|
+
const previousIndex = [...ledger].reverse().findIndex((row) => row.emptyQueueMinutes !== undefined);
|
|
1094
|
+
const workIndex = [...ledger].reverse().findIndex((row) => row.item && row.item.source !== 'idle' && row.status !== 'skipped-empty');
|
|
1095
|
+
const previousPosition = previousIndex < 0 ? -1 : ledger.length - 1 - previousIndex;
|
|
1096
|
+
const workPosition = workIndex < 0 ? -1 : ledger.length - 1 - workIndex;
|
|
1097
|
+
const previous = ledger[previousPosition];
|
|
1098
|
+
const lastWork = ledger[workPosition];
|
|
1099
|
+
const previousAt = previous ? Date.parse(previous.at) : NaN;
|
|
1100
|
+
const workAt = lastWork ? Date.parse(lastWork.at) : NaN;
|
|
1101
|
+
const start = Number.isFinite(previousAt) && Number.isFinite(workAt) ? Math.max(previousAt, workAt)
|
|
1102
|
+
: Number.isFinite(previousAt) ? previousAt : workAt;
|
|
1103
|
+
if (!Number.isFinite(start)) return 0;
|
|
1104
|
+
const prior = previousPosition > workPosition ? previous!.emptyQueueMinutes! : 0;
|
|
1105
|
+
return prior + Math.max(0, (now.getTime() - start) / 60_000);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function pickIdleWork(seat: SeatId, deps: SeatDeps, ledger: readonly SeatEntry[], now: Date, root: string, shadow: boolean): SeatItem | null {
|
|
1109
|
+
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
1110
|
+
const schedules = (deps.schedules ?? (() => readSchedules(releaseRoot)))()
|
|
1111
|
+
.filter((row) => /^\d+\.\d+\.\d+$/.test(row.version) && Number.isFinite(Date.parse(row.cutAt)))
|
|
1112
|
+
.sort((a, b) => versionOrder(a.version, b.version));
|
|
1113
|
+
const shipped = releasedVersion(releaseRoot);
|
|
1114
|
+
const current = schedules.find((row) => Date.parse(row.cutAt) > now.getTime() && (!shipped || versionOrder(row.version, shipped) > 0));
|
|
1115
|
+
const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items);
|
|
1116
|
+
const handled = handledKeys(ledger, shadow, root, !shadow);
|
|
1117
|
+
const available = (item: SeatItem) => eligibleItem(item, ledger, handled, shadow);
|
|
1118
|
+
// The normal queue handles open owned cells first; a landed yellow cell is revisited here for physical rebuttal.
|
|
1119
|
+
for (const row of schedules.filter((row) => row.version === current?.version || Date.parse(row.cutAt) <= now.getTime() || (shipped && versionOrder(row.version, shipped) <= 0))) {
|
|
1120
|
+
for (const cell of itemsOf(row.version).filter((cell) => cell.owner === seat && cell.status === 'yellow' && /착지|merged|병합/i.test(cell.evidence ?? '')).sort((a, b) => a.id.localeCompare(b.id))) {
|
|
1121
|
+
const item = idleCell(seat, 2, cell.id, `착지 후 노랑 실물 반증: ${cell.id} · ${cell.title}`, now, row.version, seat, cell.evidence);
|
|
1122
|
+
item.evidenceHash = createHash('sha256').update(cell.evidence ?? '').digest('hex');
|
|
1123
|
+
if (available(item)) return item;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const next = schedules.find((row) => current && versionOrder(row.version, current.version) > 0);
|
|
1127
|
+
if (next) {
|
|
1128
|
+
const title = `${next.version} 다음 판 칸 저작: ${seat} 자리의 자율성·효율성·동시성을 높일 칸을 근거와 실물 반증부터 작성`;
|
|
1129
|
+
const item = idleCell(seat, 3, `author:${seat}`, title, now, next.version);
|
|
1130
|
+
if (available(item)) return item;
|
|
1131
|
+
}
|
|
1132
|
+
const recurring = idleCell(seat, 4, `recurring:${seat}:${seatDay(now)}`, IDLE_WORK[seat], now);
|
|
1133
|
+
if (available(recurring)) return recurring;
|
|
1134
|
+
const queue = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root);
|
|
1135
|
+
const candidates: Array<{ owner: SeatId; item: SeatItem; pressure: number }> = [];
|
|
1136
|
+
for (const owner of ['OP', 'MK', 'UX', 'TC'] as const) {
|
|
1137
|
+
if (owner === seat) continue;
|
|
1138
|
+
const ownerLedger = readSeatLedger(owner, deps, now);
|
|
1139
|
+
const busy = queue.filter((row) => row.seat === owner && ['queued', 'launching', 'launched'].includes(row.status)).length;
|
|
1140
|
+
for (const row of schedules.filter((row) => Date.parse(row.cutAt) > now.getTime() && (!shipped || versionOrder(row.version, shipped) > 0))) {
|
|
1141
|
+
for (const cell of itemsOf(row.version).filter((cell) => cell.owner === owner && cell.status === 'yellow')) {
|
|
1142
|
+
const evidence = cell.evidence ?? '';
|
|
1143
|
+
const runIds = [...evidence.matchAll(RUN_REFERENCE)].map(([id]) => id.toLowerCase());
|
|
1144
|
+
if (runIds.length) {
|
|
1145
|
+
const observed = (deps.cutoffRuns ?? ((ids: readonly string[]) => queryRunningRuns({ runIds: ids, caller: 'seat-loop.idle' })))(runIds);
|
|
1146
|
+
if (observed.completeness !== 'complete' || observed.pty.unreadable.length || observed.entries.some((entry) => entry.status !== 'ended-unclosed')) continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (ownerLedger.some((entry) => entry.item?.source === 'checklist' && entry.item.id === cell.id && entry.item.version === row.version
|
|
1149
|
+
&& ['queued', 'attempting', 'outcome-unknown', 'launched'].includes(entry.status))) continue;
|
|
1150
|
+
if (queue.some((entry) => entry.seat === owner && ['queued', 'launching', 'launched'].includes(entry.status)
|
|
1151
|
+
&& entry.input.includes(`체크리스트 칸 ${cell.id} ·`))) continue;
|
|
1152
|
+
const item = idleCell(seat, 5, `borrow:${owner}:${cell.id}`, `${row.version} ${cell.id} · ${cell.title} — ${seat} 지원 (주인 ${owner} 유지)`, now, row.version, owner, evidence);
|
|
1153
|
+
item.evidenceHash = createHash('sha256').update(evidence).digest('hex');
|
|
1154
|
+
if (available(item)) candidates.push({ owner, item, pressure: busy + itemsOf(row.version).filter((entry) => entry.owner === owner && entry.status === 'yellow').length });
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return candidates.sort((a, b) => b.pressure - a.pressure || a.owner.localeCompare(b.owner) || a.item.id.localeCompare(b.item.id))[0]?.item ?? null;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1037
1161
|
export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
1038
|
-
if (
|
|
1162
|
+
if (!isSeatId(seat) || !registeredSeats().some(({ id }) => id === seat)) throw new Error(`unknown seat: ${seat}`);
|
|
1039
1163
|
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
|
|
1040
1164
|
if (config.mode === 'off') {
|
|
1041
1165
|
observe('skipped-off', { seat });
|
|
@@ -1060,6 +1184,25 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1060
1184
|
} catch (error) { observe('heartbeat-write-failed', { seat, error: String(error).slice(0, 200) }); }
|
|
1061
1185
|
const recorded = readSeatLedger(seat, deps, now);
|
|
1062
1186
|
const ledger = config.mode === 'live-safe' ? reconciledQueueLedger(recorded, stateRoot, deps) : recorded;
|
|
1187
|
+
const loopId = seat === 'MK' ? 'cmo-seat' : `${seat.toLowerCase()}-seat`;
|
|
1188
|
+
const heartbeatEveryMs = (SEAT_NEIGHBORS[seat]?.find((peer) => peer.id !== 'orchestrator')?.heartbeat.everyMinutes ?? 15) * 60_000;
|
|
1189
|
+
const leaseMs = heartbeatEveryMs * 2;
|
|
1190
|
+
const pulse = (health: 'healthy' | 'degraded', at: Date): void => {
|
|
1191
|
+
try { emitLoopHeartbeat(stateRoot, loopId, health, at, new Date(at.getTime() + leaseMs).toISOString()); }
|
|
1192
|
+
catch (error) { observe('neighbor-runtime-failed', { seat, error: String(error).slice(0, 200) }); }
|
|
1193
|
+
};
|
|
1194
|
+
pulse('healthy', (deps.now ?? (() => new Date()))());
|
|
1195
|
+
const watchNeighbors = (at: Date): void => {
|
|
1196
|
+
try { checkLoopNeighbors(stateRoot, loopId, SEAT_NEIGHBORS[seat] ?? [], at); }
|
|
1197
|
+
catch (error) { observe('neighbor-runtime-failed', { seat, error: String(error).slice(0, 200) }); }
|
|
1198
|
+
};
|
|
1199
|
+
// A long turn keeps both duties periodic: renew our lease and watch neighbors (incidents are idempotent).
|
|
1200
|
+
const heartbeatTimer = setInterval(() => {
|
|
1201
|
+
const at = (deps.now ?? (() => new Date()))();
|
|
1202
|
+
pulse('healthy', at);
|
|
1203
|
+
watchNeighbors(at);
|
|
1204
|
+
}, deps.heartbeatIntervalMs ?? heartbeatEveryMs / 2);
|
|
1205
|
+
heartbeatTimer.unref();
|
|
1063
1206
|
if (seat === 'OP') opDecidePass(config, deps, now, path, stateRoot, ledger);
|
|
1064
1207
|
let turnError: unknown;
|
|
1065
1208
|
try {
|
|
@@ -1259,8 +1402,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1259
1402
|
checklist: inputs.checklist.filter((candidate) => !ledger.some((row) => row.status === 'awaiting-resolution'
|
|
1260
1403
|
&& row.item && itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate))) }, ledger,
|
|
1261
1404
|
{ shadow: config.mode === 'shadow', liveSafe: config.mode === 'live-safe', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1405
|
+
const ownQueueEmpty = !pendingQuestion && !ordinary;
|
|
1406
|
+
const idleMinutes = ownQueueEmpty ? emptyQueueMinutes(ledger, now) : undefined;
|
|
1407
|
+
// IDLE-LADDER: off unless loops.seat.idleLadder is set; 'shadow' records the idle pick only, 'live' lets live-safe/on launch it.
|
|
1408
|
+
const idleShadow = config.mode === 'shadow' || config.idleLadder !== 'live';
|
|
1409
|
+
const item = pendingQuestion ?? ordinary ?? (ownQueueEmpty && !lastDecisionFailure && (config.idleLadder === 'shadow' || config.idleLadder === 'live')
|
|
1410
|
+
? pickIdleWork(seat as SeatId, deps, ledger, now, stateRoot, idleShadow) : null);
|
|
1411
|
+
const appendBase = deps.append ?? defaultAppend;
|
|
1412
|
+
const append: typeof appendBase = (file, row) => appendBase(file, idleMinutes === undefined ? row : { ...row, emptyQueueMinutes: idleMinutes });
|
|
1264
1413
|
const revisedProposal = item?.source === 'checklist' && ledger.some((row) => row.status === 'awaiting-resolution'
|
|
1265
1414
|
&& row.item && itemKey(row.item) === itemKey(item) && itemSnapshot(row.item) !== itemSnapshot(item));
|
|
1266
1415
|
if (config.mode === 'live-safe' && !pendingQuestion) {
|
|
@@ -1448,11 +1597,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1448
1597
|
}
|
|
1449
1598
|
if (!item && config.mode === 'live-safe' && tcDowngrade) return tcDowngrade;
|
|
1450
1599
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
|
|
1600
|
+
...(idleMinutes === undefined ? {} : { emptyQueueMinutes: idleMinutes }),
|
|
1451
1601
|
...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
|
|
1452
|
-
if (config.mode === 'shadow' || !item) {
|
|
1602
|
+
if (config.mode === 'shadow' || !item || (item.source === 'idle' && idleShadow)) {
|
|
1453
1603
|
if (!item && lastDecisionFailure) return lastDecisionFailure;
|
|
1454
1604
|
append(path, entry);
|
|
1455
|
-
if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
|
|
1605
|
+
if (config.mode === 'shadow' || item?.source === 'idle') observe('shadow', { seat, item, status: entry.status });
|
|
1456
1606
|
return entry;
|
|
1457
1607
|
}
|
|
1458
1608
|
if (planned!.kind === 'wait') {
|
|
@@ -1669,8 +1819,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1669
1819
|
turnError = error;
|
|
1670
1820
|
throw error;
|
|
1671
1821
|
} finally {
|
|
1822
|
+
clearInterval(heartbeatTimer);
|
|
1672
1823
|
// A turn ends even when there is no picked item or a launch fails.
|
|
1673
|
-
|
|
1824
|
+
const endedAt = (deps.now ?? (() => new Date()))();
|
|
1825
|
+
pulse(turnError === undefined ? 'healthy' : 'degraded', endedAt);
|
|
1826
|
+
watchNeighbors(endedAt);
|
|
1827
|
+
try { judgeSeatNeighbors(seat, config, deps, stateRoot, endedAt); }
|
|
1674
1828
|
catch (error) {
|
|
1675
1829
|
observe('neighbor-judgment-failed', { seat, error: String(error).slice(0, 200) });
|
|
1676
1830
|
const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
|